diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9df02e41..97491f6f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,6 +9,7 @@ plugins {
dependencies {
implementation(projects.feature.home)
implementation(projects.feature.account)
+ implementation(projects.feature.projects)
implementation(projects.feature.chat)
implementation(projects.feature.newproject)
implementation(projects.feature.experience)
diff --git a/app/src/main/java/com/useai/logit/ScreenProviderImpl.kt b/app/src/main/java/com/useai/logit/ScreenProviderImpl.kt
index 2bdd5d96..907e9c9c 100644
--- a/app/src/main/java/com/useai/logit/ScreenProviderImpl.kt
+++ b/app/src/main/java/com/useai/logit/ScreenProviderImpl.kt
@@ -10,11 +10,14 @@ import com.useai.feature.experience.ExperienceCreateScreen
import com.useai.feature.experience.ExperienceDetailScreen
import com.useai.feature.newproject.NewProjectBasicInfoScreen
import com.useai.feature.newproject.NewProjectQuestionScreen
+import com.useai.feature.projects.ProjectsScreen
class ScreenProviderImpl: ScreenProvider {
override fun homeScreen(): Screen = HomeScreen
- override fun accountScreen(): Screen = AccountScreen()
+ override fun accountScreen(): Screen = AccountScreen
+
+ override fun projectsScreen(): Screen = ProjectsScreen
override fun newProjectBasicInfoScreen(): Screen = NewProjectBasicInfoScreen
diff --git a/app/src/main/java/com/useai/logit/navigation/TopLevelNavItem.kt b/app/src/main/java/com/useai/logit/navigation/TopLevelNavItem.kt
index fdb8a692..113d3832 100644
--- a/app/src/main/java/com/useai/logit/navigation/TopLevelNavItem.kt
+++ b/app/src/main/java/com/useai/logit/navigation/TopLevelNavItem.kt
@@ -5,10 +5,10 @@ import androidx.annotation.StringRes
import com.slack.circuit.runtime.screen.Screen
import com.useai.core.designsystem.R
import com.useai.core.designsystem.icon.LogitIcons
-import com.useai.feature.chat.ChatScreen
import com.useai.feature.experience.ExperienceScreen
import com.useai.feature.home.HomeScreen
import com.useai.feature.newproject.NewProjectBasicInfoScreen
+import com.useai.feature.projects.ProjectsScreen
import com.useai.feature.report.ReportScreen
data class TopLevelNavItem(
@@ -21,7 +21,7 @@ data class TopLevelNavItem(
fun fromScreen(screen: Screen): TopLevelNavItem {
return when (screen) {
is HomeScreen -> HOME
- is ChatScreen -> COVER_LETTER
+ is ProjectsScreen -> PROJECTS
is NewProjectBasicInfoScreen -> NEW_PROJECT
is ExperienceScreen -> EXPERIENCE
is ReportScreen -> REPORT
@@ -38,7 +38,7 @@ val HOME = TopLevelNavItem(
titleTextId = R.string.home_title,
)
-val COVER_LETTER = TopLevelNavItem(
+val PROJECTS = TopLevelNavItem(
selectedIconId = LogitIcons.PaperSelected,
unselectedIconId = LogitIcons.PaperDefault,
iconTextId = R.string.cover_letter_title,
diff --git a/app/src/main/java/com/useai/logit/ui/Root.kt b/app/src/main/java/com/useai/logit/ui/Root.kt
index 546d4aaa..751b57b2 100644
--- a/app/src/main/java/com/useai/logit/ui/Root.kt
+++ b/app/src/main/java/com/useai/logit/ui/Root.kt
@@ -26,11 +26,11 @@ import com.useai.core.designsystem.component.LogitNavigationBarItem
import com.useai.core.designsystem.component.snackbar.LocalLogitSnackbarHostState
import com.useai.core.designsystem.component.snackbar.LogitSnackbarHost
import com.useai.core.designsystem.theme.LogitTheme
-import com.useai.feature.chat.ChatScreen
import com.useai.feature.experience.ExperienceScreen
import com.useai.feature.home.HomeScreen
import com.useai.feature.newproject.NewProjectBasicInfoScreen
import com.useai.feature.newproject.NewProjectQuestionScreen
+import com.useai.feature.projects.ProjectsScreen
import com.useai.feature.report.ReportScreen
import com.useai.logit.RootScreen
import com.useai.logit.navigation.TopLevelNavItem
@@ -48,7 +48,7 @@ fun Root(
val screens = remember {
listOf(
HomeScreen,
- ChatScreen(""),
+ ProjectsScreen,
NewProjectBasicInfoScreen,
ExperienceScreen,
ReportScreen,
diff --git a/core/designsystem/src/main/res/values/dimens.xml b/core/designsystem/src/main/res/values/dimens.xml
index dcd5c327..0b8adf66 100644
--- a/core/designsystem/src/main/res/values/dimens.xml
+++ b/core/designsystem/src/main/res/values/dimens.xml
@@ -2,6 +2,7 @@
8dp
20dp
+ 20dp
44dp
35dp
12dp
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index f084b4e0..311cb6ea 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -11,11 +11,13 @@
%s님의 경험 유형
관련경험 %d개
- 프로젝트 목록
자기소개서를 생성해보세요
자기소개서 작성
%s %s
+ 프로젝트 목록
+ %d개
+
자기소개서 업데이트
어떤 내용을 만들어볼까요?
Q%s
@@ -106,5 +108,6 @@
로짓 로고
사용자 프로필
+ 프로젝트 생성
diff --git a/core/navigation/src/main/java/com/useai/core/navigation/ScreenProvider.kt b/core/navigation/src/main/java/com/useai/core/navigation/ScreenProvider.kt
index f4c3c584..32c4f243 100644
--- a/core/navigation/src/main/java/com/useai/core/navigation/ScreenProvider.kt
+++ b/core/navigation/src/main/java/com/useai/core/navigation/ScreenProvider.kt
@@ -5,6 +5,7 @@ import com.slack.circuit.runtime.screen.Screen
interface ScreenProvider {
fun homeScreen(): Screen
fun accountScreen(): Screen
+ fun projectsScreen(): Screen
fun newProjectBasicInfoScreen(): Screen
fun newProjectQuestionScreen(
companyName: String,
diff --git a/core/ui/src/main/kotlin/com/useai/core/ui/AppHeader.kt b/core/ui/src/main/kotlin/com/useai/core/ui/AppHeader.kt
index e4a60820..ce9d66b6 100644
--- a/core/ui/src/main/kotlin/com/useai/core/ui/AppHeader.kt
+++ b/core/ui/src/main/kotlin/com/useai/core/ui/AppHeader.kt
@@ -3,6 +3,7 @@ package com.useai.core.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -15,44 +16,47 @@ 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.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.useai.core.designsystem.R
@Composable
fun AppHeader(
modifier: Modifier = Modifier,
- onClickProfile: () -> Unit,
+ title: @Composable () -> Unit,
+ iconPainter: Painter,
+ iconDescription: String?,
+ iconSize: Dp,
+ onIconClick: () -> Unit,
+ paddingValues: PaddingValues = PaddingValues(
+ horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal),
+ )
) {
Row(
modifier = modifier
.fillMaxWidth()
- .padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
+ .padding(paddingValues),
verticalAlignment = Alignment.CenterVertically,
) {
- Image(
- painter = painterResource(R.drawable.ic_symbol_word),
- contentDescription = stringResource(R.string.content_description_app_logo),
- modifier = Modifier
- .height(28.dp)
- .width(85.dp),
- )
+ title()
Spacer(Modifier.weight(1f))
Box(
modifier = Modifier.size(dimensionResource(R.dimen.app_header_user_profile_area_size)),
contentAlignment = Alignment.Center,
) {
Image(
- painter = painterResource(R.drawable.ic_app_user),
- contentDescription = stringResource(R.string.content_description_user_profile),
+ painter = iconPainter,
+ contentDescription = iconDescription,
modifier = Modifier
- .size(dimensionResource(R.dimen.app_header_user_profile_image_size))
+ .size(iconSize)
.clip(CircleShape)
.clickable(
- onClick = onClickProfile,
+ onClick = onIconClick,
),
)
}
@@ -63,6 +67,18 @@ fun AppHeader(
@Composable
private fun AppHeaderPreview() {
AppHeader(
- onClickProfile = {}
+ title = {
+ Image(
+ painter = painterResource(R.drawable.ic_symbol_word),
+ contentDescription = stringResource(R.string.content_description_app_logo),
+ modifier = Modifier
+ .height(28.dp)
+ .width(85.dp),
+ )
+ },
+ iconPainter = painterResource(R.drawable.ic_app_user),
+ iconDescription = stringResource(R.string.content_description_user_profile),
+ iconSize = dimensionResource(R.dimen.app_header_user_profile_image_size),
+ onIconClick = {}
)
}
diff --git a/core/ui/src/main/kotlin/com/useai/core/ui/project/EmptyProjectList.kt b/core/ui/src/main/kotlin/com/useai/core/ui/project/EmptyProjectList.kt
new file mode 100644
index 00000000..f5df766a
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/useai/core/ui/project/EmptyProjectList.kt
@@ -0,0 +1,69 @@
+package com.useai.core.ui.project
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.useai.core.designsystem.R
+import com.useai.core.designsystem.component.button.LogitPrimaryButton
+import com.useai.core.designsystem.theme.LogitTheme
+
+@Composable
+fun EmptyProjectList(
+ modifier: Modifier = Modifier,
+ onClickCreateProject: () -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 42.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_empty_state),
+ contentDescription = null,
+ modifier = Modifier.size(80.dp),
+ )
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.home_empty_project_phrase),
+ style = LogitTheme.typography.body6_2,
+ color = LogitTheme.colors.gray100,
+ )
+ Spacer(Modifier.height(17.dp))
+ LogitPrimaryButton(
+ text = stringResource(R.string.home_new_project),
+ onClick = {
+ onClickCreateProject()
+ },
+ textStyle = LogitTheme.typography.body6_2,
+ shape = RoundedCornerShape(8.dp),
+ contentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp),
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun EmptyProjectListPreview() {
+ LogitTheme {
+ EmptyProjectList(
+ modifier = Modifier.background(LogitTheme.colors.white),
+ onClickCreateProject = {},
+ )
+ }
+}
diff --git a/core/ui/src/main/kotlin/com/useai/core/ui/project/ProjectList.kt b/core/ui/src/main/kotlin/com/useai/core/ui/project/ProjectList.kt
new file mode 100644
index 00000000..e2dc741c
--- /dev/null
+++ b/core/ui/src/main/kotlin/com/useai/core/ui/project/ProjectList.kt
@@ -0,0 +1,145 @@
+package com.useai.core.ui.project
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+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.lazy.LazyListScope
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.useai.core.designsystem.R
+import com.useai.core.designsystem.theme.LogitTheme
+import com.useai.core.model.project.ProjectListItem
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+@Composable
+fun LazyListScope.ProjectList(
+ projects: List,
+ onClickProject: (String) -> Unit,
+) {
+ itemsIndexed(
+ items = projects,
+ key = { _, project -> project.id }
+ ) { index, project ->
+ ProjectItem(
+ modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
+ project = project,
+ onClick = {
+ onClickProject(project.id)
+ },
+ )
+ if (index < projects.lastIndex) {
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
+ thickness = 1.dp,
+ color = LogitTheme.colors.gray70
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProjectItem(
+ modifier: Modifier = Modifier,
+ project: ProjectListItem,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(
+ onClick = onClick,
+ )
+ .padding(vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier
+ .size(width = 3.dp, height = 24.dp)
+ .background(LogitTheme.colors.primary70)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ text = stringResource(
+ R.string.home_project_list_item_title_format,
+ project.company,
+ project.jobPosition
+ ),
+ style = LogitTheme.typography.body6_2,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ text = project.dueDate.toString(),
+ style = LogitTheme.typography.body7_4,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun ProjectListPreview() {
+ LogitTheme {
+ LazyColumn(
+ modifier = Modifier.background(LogitTheme.colors.white)
+ ) {
+ item {
+ this@LazyColumn.ProjectList(
+ projects = listOf(
+ ProjectListItem(
+ id = "1",
+ company = "카카오페이",
+ jobPosition = "디자인 어시스턴트 어쩌구 저쩌구 어쩌구 저쩌구",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ProjectListItem(
+ id = "2",
+ company = "네이버",
+ jobPosition = "프론트엔드 개발",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ProjectListItem(
+ id = "3",
+ company = "토스",
+ jobPosition = "iOS 개발",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ),
+ onClickProject = {},
+ )
+ }
+ }
+ }
+}
diff --git a/feature/account/src/main/kotlin/com/useai/feature/account/AccountScreen.kt b/feature/account/src/main/kotlin/com/useai/feature/account/AccountScreen.kt
index ed9175ef..4788802e 100644
--- a/feature/account/src/main/kotlin/com/useai/feature/account/AccountScreen.kt
+++ b/feature/account/src/main/kotlin/com/useai/feature/account/AccountScreen.kt
@@ -19,7 +19,7 @@ import dagger.hilt.android.components.ActivityRetainedComponent
import kotlinx.parcelize.Parcelize
@Parcelize
-class AccountScreen : Screen {
+data object AccountScreen : Screen {
data class State(
val userName: String,
val reportNotificationEnabled: Boolean,
diff --git a/feature/home/src/main/kotlin/com/useai/feature/home/ui/Home.kt b/feature/home/src/main/kotlin/com/useai/feature/home/ui/Home.kt
index ce9d2468..0deafdea 100644
--- a/feature/home/src/main/kotlin/com/useai/feature/home/ui/Home.kt
+++ b/feature/home/src/main/kotlin/com/useai/feature/home/ui/Home.kt
@@ -1,37 +1,25 @@
package com.useai.feature.home.ui
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.slack.circuit.codegen.annotations.CircuitInject
import com.useai.core.designsystem.R
-import com.useai.core.designsystem.component.button.LogitPrimaryButton
import com.useai.core.designsystem.theme.LogitTheme
import com.useai.core.model.project.ProjectListItem
import com.useai.core.ui.AppHeader
@@ -39,6 +27,8 @@ import com.useai.core.ui.ExperienceBannerItem
import com.useai.core.ui.ExperienceType
import com.useai.core.ui.LogitExperienceBanner
import com.useai.core.ui.LogitFormTitle
+import com.useai.core.ui.project.EmptyProjectList
+import com.useai.core.ui.project.ProjectList
import com.useai.feature.home.HomeScreen
import dagger.hilt.android.components.ActivityRetainedComponent
import java.time.LocalDate
@@ -52,11 +42,25 @@ fun Home(
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
- contentPadding = PaddingValues(bottom = 20.dp)
+ contentPadding = PaddingValues(
+ bottom = dimensionResource(R.dimen.screen_common_padding_bottom),
+ ),
) {
item {
AppHeader(
- onClickProfile = {
+ title = {
+ Image(
+ painter = painterResource(R.drawable.ic_symbol_word),
+ contentDescription = stringResource(R.string.content_description_app_logo),
+ modifier = Modifier
+ .height(28.dp)
+ .width(85.dp),
+ )
+ },
+ iconPainter = painterResource(R.drawable.ic_app_user),
+ iconDescription = stringResource(R.string.content_description_user_profile),
+ iconSize = dimensionResource(R.dimen.app_header_user_profile_image_size),
+ onIconClick = {
state.eventSink(HomeScreen.Event.AccountClicked)
}
)
@@ -79,122 +83,31 @@ fun Home(
LogitExperienceBanner(state.bannerItems)
Spacer(Modifier.height(43.dp))
LogitFormTitle(
- title = stringResource(R.string.home_project_list_title),
+ title = stringResource(R.string.projects_title),
)
Spacer(Modifier.height(16.dp))
}
}
- if (state.projects.isEmpty()) {
- item {
+ item {
+ if (state.projects.isEmpty()) {
EmptyProjectList(
- modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
onClickCreateProject = {
state.eventSink(HomeScreen.Event.NewProjectClicked)
- }
+ },
)
- }
- } else {
- itemsIndexed(
- items = state.projects,
- key = { _, project -> project.id }
- ) { index, project ->
- ProjectItem(
- modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
- project = project,
- onClick = {
- state.eventSink(HomeScreen.Event.ProjectClicked(project.id))
+ } else {
+ this@LazyColumn.ProjectList(
+ projects = state.projects,
+ onClickProject = { projectId ->
+ state.eventSink(HomeScreen.Event.ProjectClicked(projectId))
}
)
- if (index < state.projects.lastIndex) {
- HorizontalDivider(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal)),
- thickness = 1.dp,
- color = LogitTheme.colors.gray70
- )
- }
}
}
}
}
-@Composable
-private fun EmptyProjectList(
- modifier: Modifier = Modifier,
- onClickCreateProject: () -> Unit,
-) {
- Column(
- modifier = modifier
- .fillMaxWidth()
- .padding(vertical = 42.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(
- painter = painterResource(R.drawable.ic_empty_state),
- contentDescription = null,
- modifier = Modifier.size(80.dp),
- )
- Spacer(Modifier.height(16.dp))
- Text(
- text = stringResource(R.string.home_empty_project_phrase),
- style = LogitTheme.typography.body6_2,
- color = LogitTheme.colors.gray100,
- )
- Spacer(Modifier.height(17.dp))
- LogitPrimaryButton(
- text = stringResource(R.string.home_new_project),
- onClick = {
- onClickCreateProject()
- },
- textStyle = LogitTheme.typography.body6_2,
- shape = RoundedCornerShape(8.dp),
- contentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp),
- )
- }
-}
-
-@Composable
-private fun ProjectItem(
- modifier: Modifier = Modifier,
- project: ProjectListItem,
- onClick: () -> Unit,
-) {
- Row(
- modifier = modifier
- .fillMaxWidth()
- .clickable(
- onClick = onClick,
- )
- .padding(vertical = 16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Box(
- modifier = Modifier
- .size(width = 3.dp, height = 24.dp)
- .background(LogitTheme.colors.primary70)
- )
- Spacer(Modifier.width(12.dp))
- Text(
- text = stringResource(
- R.string.home_project_list_item_title_format,
- project.company,
- project.jobPosition
- ),
- style = LogitTheme.typography.body6_2,
- modifier = Modifier.weight(1f),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- Spacer(Modifier.width(12.dp))
- Text(
- text = project.dueDate.toString(),
- style = LogitTheme.typography.body7_4,
- )
- }
-}
-
@Preview
@Composable
private fun HomeWithEmptyProjectPreview() {
diff --git a/feature/projects/.gitignore b/feature/projects/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/projects/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/projects/build.gradle.kts b/feature/projects/build.gradle.kts
new file mode 100644
index 00000000..225f6972
--- /dev/null
+++ b/feature/projects/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ alias(libs.plugins.logit.android.library.common)
+ alias(libs.plugins.logit.compose.common)
+ alias(libs.plugins.logit.hilt)
+ alias(libs.plugins.logit.circuit)
+ alias(libs.plugins.logit.feature.common.dependencies)
+}
+
+android {
+ namespace = "com.useai.feature.projects"
+}
diff --git a/feature/projects/consumer-rules.pro b/feature/projects/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/projects/proguard-rules.pro b/feature/projects/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/projects/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/projects/src/androidTest/java/com/useai/feature/projects/ExampleInstrumentedTest.kt b/feature/projects/src/androidTest/java/com/useai/feature/projects/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..b5018138
--- /dev/null
+++ b/feature/projects/src/androidTest/java/com/useai/feature/projects/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.useai.feature.projects
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.useai.feature.projects.test", appContext.packageName)
+ }
+}
diff --git a/feature/projects/src/main/AndroidManifest.xml b/feature/projects/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8bdb7e14
--- /dev/null
+++ b/feature/projects/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/feature/projects/src/main/kotlin/com/useai/feature/projects/ProjectsScreen.kt b/feature/projects/src/main/kotlin/com/useai/feature/projects/ProjectsScreen.kt
new file mode 100644
index 00000000..ad5e2f3d
--- /dev/null
+++ b/feature/projects/src/main/kotlin/com/useai/feature/projects/ProjectsScreen.kt
@@ -0,0 +1,78 @@
+package com.useai.feature.projects
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+import com.useai.core.data.repository.ProjectRepository
+import com.useai.core.model.project.ProjectListItem
+import com.useai.core.navigation.LocalScreenProvider
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.components.ActivityRetainedComponent
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object ProjectsScreen : Screen {
+ data class State(
+ val projects: List,
+ val eventSink: (Event) -> Unit = {},
+ ) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent {
+ data object NewProjectClicked : Event
+ data class ProjectClicked(
+ val projectId: String,
+ ) : Event
+ }
+}
+
+class ProjectsPresenter @AssistedInject constructor(
+ @Assisted private val navigator: Navigator,
+ private val projectRepository: ProjectRepository,
+) : Presenter {
+ @Composable
+ override fun present(): ProjectsScreen.State {
+ val projects by produceState(initialValue = emptyList()) {
+ projectRepository.getProjects() // TODO: 페이징 사용, 화면 진입 시마다 요청하지 않도록 개선 필요
+ .onSuccess { value = it }
+ .onFailure {
+ Log.e(TAG, "getProjects failed: $it")
+ }
+ }
+ val screenProvider = LocalScreenProvider.current
+
+ return ProjectsScreen.State(
+ projects = projects,
+ ) { event ->
+ when (event) {
+ ProjectsScreen.Event.NewProjectClicked -> {
+ navigator.goTo(screenProvider.newProjectBasicInfoScreen())
+ }
+
+ is ProjectsScreen.Event.ProjectClicked -> {
+ navigator.goTo(screenProvider.chatScreen(event.projectId))
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ @CircuitInject(ProjectsScreen::class, ActivityRetainedComponent::class)
+ fun interface Factory {
+ fun create(
+ navigator: Navigator,
+ ): ProjectsPresenter
+ }
+
+ companion object {
+ private val TAG = ProjectsPresenter::class.simpleName
+ }
+}
diff --git a/feature/projects/src/main/kotlin/com/useai/feature/projects/ui/Projects.kt b/feature/projects/src/main/kotlin/com/useai/feature/projects/ui/Projects.kt
new file mode 100644
index 00000000..c35ddc61
--- /dev/null
+++ b/feature/projects/src/main/kotlin/com/useai/feature/projects/ui/Projects.kt
@@ -0,0 +1,145 @@
+package com.useai.feature.projects.ui
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.useai.core.designsystem.R
+import com.useai.core.designsystem.theme.LogitTheme
+import com.useai.core.model.project.ProjectListItem
+import com.useai.core.ui.AppHeader
+import com.useai.core.ui.project.EmptyProjectList
+import com.useai.core.ui.project.ProjectList
+import com.useai.feature.projects.ProjectsScreen
+import dagger.hilt.android.components.ActivityRetainedComponent
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+@Composable
+@CircuitInject(ProjectsScreen::class, ActivityRetainedComponent::class)
+fun Projects(
+ modifier: Modifier = Modifier,
+ state: ProjectsScreen.State,
+) {
+ LazyColumn(
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ bottom = dimensionResource(R.dimen.screen_common_padding_bottom),
+ )
+ ) {
+ item {
+ AppHeader(
+ title = {
+ Text(
+ text = stringResource(R.string.projects_title),
+ style = LogitTheme.typography.body1,
+ color = LogitTheme.colors.black,
+ )
+ },
+ iconPainter = painterResource(R.drawable.ic_tab_add),
+ iconDescription = stringResource(R.string.content_description_new_project),
+ iconSize = 16.dp,
+ onIconClick = {
+ state.eventSink(ProjectsScreen.Event.NewProjectClicked)
+ },
+ paddingValues = PaddingValues(
+ start = dimensionResource(R.dimen.screen_common_padding_horizontal),
+ ),
+ )
+ }
+
+ item {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = dimensionResource(R.dimen.screen_common_padding_horizontal),
+ vertical = 6.dp
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.projects_count_format, state.projects.size),
+ style = LogitTheme.typography.body8_1,
+ color = LogitTheme.colors.gray200,
+ )
+ }
+ }
+
+ item {
+ if (state.projects.isEmpty()) {
+ EmptyProjectList(
+ onClickCreateProject = {
+ state.eventSink(ProjectsScreen.Event.NewProjectClicked)
+ }
+ )
+ } else {
+ this@LazyColumn.ProjectList(
+ projects = state.projects,
+ onClickProject = {
+ state.eventSink(ProjectsScreen.Event.ProjectClicked(it))
+ },
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun ProjectsPreview() {
+ LogitTheme {
+ Scaffold(
+ modifier = Modifier.fillMaxSize()
+ ) { paddingValues ->
+ Projects(
+ modifier = Modifier.padding(paddingValues),
+ state = ProjectsScreen.State(
+ projects = listOf(
+ ProjectListItem(
+ id = "1",
+ company = "카카오페이",
+ jobPosition = "디자인 어시스턴트 어쩌구 저쩌구 어쩌구 저쩌구",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ProjectListItem(
+ id = "2",
+ company = "네이버",
+ jobPosition = "프론트엔드 개발",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ProjectListItem(
+ id = "3",
+ company = "토스",
+ jobPosition = "iOS 개발",
+ dueDate = LocalDate.of(2026, 1, 7),
+ questionId = "",
+ totalQuestions = 0,
+ completedQuestions = 0,
+ updatedAt = LocalDateTime.now()
+ ),
+ ),
+ ),
+ )
+ }
+ }
+}
diff --git a/feature/projects/src/test/java/com/useai/feature/projects/ExampleUnitTest.kt b/feature/projects/src/test/java/com/useai/feature/projects/ExampleUnitTest.kt
new file mode 100644
index 00000000..0e325c8b
--- /dev/null
+++ b/feature/projects/src/test/java/com/useai/feature/projects/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.useai.feature.projects
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6b357ada..b3fbfda7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -46,3 +46,4 @@ include(":feature:newproject")
include(":feature:experience")
include(":feature:report")
include(":feature:account")
+include(":feature:projects")