From 8074bf3cceb1bd3b995f79b85e8fa4e35e4ccc9f Mon Sep 17 00:00:00 2001
From: Sehwan Yun <39579912+l5x5l@users.noreply.github.com>
Date: Sun, 14 Jul 2024 14:56:12 +0900
Subject: [PATCH] =?UTF-8?q?[Feature]=20#13=20=ED=8F=AC=ED=82=B7=20?=
=?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20?=
=?UTF-8?q?(#15)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [BASE] #13 feature:addpokit 모듈 생성
* [BASE] #13 이미지 선택부분을 제외한 포킷 추가 화면 구현
* [FEATURE] #13 포킷 프로필 선택부분 구현
* [CHORE] #13 ktlint 적용
* [CHORE] #13 포킷 이름 최대 글자수 상수로 분리
* [CHORE] #13 core 모듈 resource를 사용하는 부분을 alias import로 변경
---
feature/addpokit/.gitignore | 1 +
feature/addpokit/build.gradle.kts | 64 +++++
feature/addpokit/consumer-rules.pro | 0
feature/addpokit/proguard-rules.pro | 21 ++
.../addpokit/ExampleInstrumentedTest.kt | 22 ++
feature/addpokit/src/main/AndroidManifest.xml | 4 +
.../strayalpaca/addpokit/AddPokitScreen.kt | 259 ++++++++++++++++++
.../strayalpaca/addpokit/AddPokitViewModel.kt | 116 ++++++++
.../java/com/strayalpaca/addpokit/Preview.kt | 24 ++
.../components/atom/PokitProfileImage.kt | 49 ++++
.../addpokit/components/block/Toolbar.kt | 54 ++++
.../com/strayalpaca/addpokit/const/Consts.kt | 3 +
.../addpokit/model/AddPokitScreenState.kt | 29 ++
.../com/strayalpaca/addpokit/model/Pokit.kt | 15 +
.../addpokit/model/PokitProfile.kt | 13 +
.../addpokit/utils/BackPressHandler.kt | 35 +++
.../addpokit/src/main/res/values/string.xml | 11 +
.../strayalpaca/addpokit/ExampleUnitTest.kt | 16 ++
18 files changed, 736 insertions(+)
create mode 100644 feature/addpokit/.gitignore
create mode 100644 feature/addpokit/build.gradle.kts
create mode 100644 feature/addpokit/consumer-rules.pro
create mode 100644 feature/addpokit/proguard-rules.pro
create mode 100644 feature/addpokit/src/androidTest/java/com/strayalpaca/addpokit/ExampleInstrumentedTest.kt
create mode 100644 feature/addpokit/src/main/AndroidManifest.xml
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/block/Toolbar.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/const/Consts.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt
create mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/BackPressHandler.kt
create mode 100644 feature/addpokit/src/main/res/values/string.xml
create mode 100644 feature/addpokit/src/test/java/com/strayalpaca/addpokit/ExampleUnitTest.kt
diff --git a/feature/addpokit/.gitignore b/feature/addpokit/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/addpokit/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/addpokit/build.gradle.kts b/feature/addpokit/build.gradle.kts
new file mode 100644
index 00000000..0c700c02
--- /dev/null
+++ b/feature/addpokit/build.gradle.kts
@@ -0,0 +1,64 @@
+plugins {
+ alias(libs.plugins.com.android.library)
+ alias(libs.plugins.org.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "com.strayalpaca.addpokit"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.orbit.compose)
+ implementation(libs.orbit.core)
+ implementation(libs.orbit.viewmodel)
+
+ implementation(project(":core:ui"))
+}
diff --git a/feature/addpokit/consumer-rules.pro b/feature/addpokit/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/addpokit/proguard-rules.pro b/feature/addpokit/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/addpokit/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/addpokit/src/androidTest/java/com/strayalpaca/addpokit/ExampleInstrumentedTest.kt b/feature/addpokit/src/androidTest/java/com/strayalpaca/addpokit/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..943ab772
--- /dev/null
+++ b/feature/addpokit/src/androidTest/java/com/strayalpaca/addpokit/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package com.strayalpaca.addpokit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * 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.strayalpaca.addpokit.test", appContext.packageName)
+ }
+}
diff --git a/feature/addpokit/src/main/AndroidManifest.xml b/feature/addpokit/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/addpokit/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt
new file mode 100644
index 00000000..120d66be
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt
@@ -0,0 +1,259 @@
+package com.strayalpaca.addpokit
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.strayalpaca.addpokit.components.atom.PokitProfileImage
+import com.strayalpaca.addpokit.components.block.Toolbar
+import com.strayalpaca.addpokit.model.AddPokitScreenState
+import com.strayalpaca.addpokit.model.AddPokitScreenStep
+import com.strayalpaca.addpokit.model.AddPokitSideEffect
+import com.strayalpaca.addpokit.model.Pokit
+import com.strayalpaca.addpokit.model.PokitProfile
+import com.strayalpaca.addpokit.model.samplePokitProfileList
+import com.strayalpaca.addpokit.utils.BackPressHandler
+import org.orbitmvi.orbit.compose.collectSideEffect
+import pokitmons.pokit.core.ui.components.atom.button.PokitButton
+import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize
+import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput
+import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList
+import pokitmons.pokit.core.ui.components.block.pokitlist.attributes.PokitListState
+import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+import pokitmons.pokit.core.ui.R.string as coreString
+
+@Composable
+fun AddPokitScreenContainer(
+ viewModel: AddPokitViewModel,
+ onBackPressed: () -> Unit,
+) {
+ val state by viewModel.container.stateFlow.collectAsState()
+ val pokitName by viewModel.pokitName.collectAsState()
+
+ val saveButtonEnable = remember {
+ derivedStateOf {
+ state.step != AddPokitScreenStep.POKIT_SAVE_LOADING &&
+ state.step != AddPokitScreenStep.POKIT_LIST_LOADING &&
+ state.pokitInputErrorMessage == null &&
+ state.pokitProfile != null
+ }
+ }
+
+ viewModel.collectSideEffect { sideEffect ->
+ when (sideEffect) {
+ AddPokitSideEffect.AddPokitSuccess -> {
+ onBackPressed()
+ }
+
+ AddPokitSideEffect.OnNavigationBack -> {
+ onBackPressed()
+ }
+ }
+ }
+
+ BackPressHandler(onBackPressed = viewModel::onBackPressed)
+
+ AddPokitScreen(
+ pokitName = pokitName,
+ state = state,
+ saveButtonEnable = saveButtonEnable.value,
+ onclickAddPokit = viewModel::savePokit,
+ inputPokitName = viewModel::inputPokitName,
+ onBackPressed = viewModel::onBackPressed,
+ hideProfileSelectBottomSheet = viewModel::hidePokitProfileSelectBottomSheet,
+ showSelectProfileBottomSheet = viewModel::showPokitProfileSelectBottomSheet,
+ selectPokitProfileImage = viewModel::selectPoktiProfile
+ )
+}
+
+@Composable
+fun AddPokitScreen(
+ pokitName: String = "",
+ state: AddPokitScreenState = AddPokitScreenState(),
+ saveButtonEnable: Boolean = true,
+ onclickAddPokit: () -> Unit = {},
+ inputPokitName: (String) -> Unit = {},
+ onBackPressed: () -> Unit = {},
+ hideProfileSelectBottomSheet: () -> Unit = {},
+ showSelectProfileBottomSheet: () -> Unit = {},
+ selectPokitProfileImage: (PokitProfile) -> Unit = {},
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Toolbar(
+ onClickBack = onBackPressed,
+ title = stringResource(id = R.string.title_add_pokit)
+ )
+
+ Box(modifier = Modifier.size(80.dp)) {
+ Image(
+ painter = painterResource(id = coreDrawable.icon_24_google),
+ contentDescription = null,
+ modifier = Modifier
+ .size(80.dp)
+ .clip(shape = RoundedCornerShape(12.dp))
+ )
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .offset(x = 7.dp, y = (-7).dp)
+ .size(24.dp)
+ .background(
+ color = PokitTheme.colors.inverseWh,
+ shape = CircleShape
+ )
+ .border(
+ width = 1.dp,
+ color = PokitTheme.colors.borderSecondary,
+ shape = CircleShape
+ )
+ .clip(
+ shape = CircleShape
+ )
+ .clickable(
+ onClick = showSelectProfileBottomSheet
+ )
+ .padding(3.dp)
+ ) {
+ Image(
+ painter = painterResource(id = coreDrawable.icon_24_edit),
+ contentDescription = "null",
+ modifier = Modifier
+ .size(18.dp),
+ colorFilter = ColorFilter.tint(
+ color = PokitTheme.colors.iconTertiary
+ )
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LabeledInput(
+ modifier = Modifier.padding(horizontal = 20.dp),
+ label = stringResource(id = R.string.pokit_name),
+ inputText = pokitName,
+ hintText = stringResource(id = R.string.placeholder_pokit_name),
+ onChangeText = inputPokitName,
+ isError = state.pokitInputErrorMessage != null,
+ sub = state.pokitInputErrorMessage?.let { stringResource(id = it.resourceId) } ?: "",
+ enable = (state.step != AddPokitScreenStep.POKIT_SAVE_LOADING),
+ maxLength = 10
+ )
+
+ Spacer(modifier = Modifier.height(28.dp))
+
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ text = stringResource(id = R.string.my_pokit),
+ style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(state.pokitList) { item: Pokit ->
+ PokitList(
+ item = item,
+ title = item.title,
+ sub = stringResource(id = coreString.pokit_count_format, item.count),
+ onClickKebab = {},
+ onClickItem = {},
+ state = PokitListState.DEFAULT
+ )
+ }
+ }
+
+ if (state.step == AddPokitScreenStep.POKIT_LIST_LOADING) {
+ CircularProgressIndicator(
+ modifier = Modifier.width(64.dp),
+ color = PokitTheme.colors.brand,
+ trackColor = PokitTheme.colors.backgroundSecondary
+ )
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ ) {
+ PokitButton(
+ text = stringResource(id = R.string.save),
+ icon = null,
+ onClick = onclickAddPokit,
+ modifier = Modifier.fillMaxWidth(),
+ size = PokitButtonSize.LARGE,
+ enable = saveButtonEnable
+ )
+ }
+
+ if (state.step == AddPokitScreenStep.SELECT_PROFILE) {
+ PokitBottomSheet(onHideBottomSheet = hideProfileSelectBottomSheet) {
+ LazyVerticalGrid(
+ modifier = Modifier.padding(vertical = 12.dp, horizontal = 40.dp),
+ columns = GridCells.Adaptive(66.dp),
+ horizontalArrangement = Arrangement.spacedBy(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(samplePokitProfileList) { profileImage ->
+ PokitProfileImage(
+ pokitProfile = profileImage,
+ onClick = selectPokitProfileImage,
+ focused = (state.pokitProfile?.id == profileImage.id)
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt
new file mode 100644
index 00000000..590775db
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt
@@ -0,0 +1,116 @@
+package com.strayalpaca.addpokit
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.strayalpaca.addpokit.const.POKIT_NAME_MAX_LENGTH
+import com.strayalpaca.addpokit.model.AddPokitScreenState
+import com.strayalpaca.addpokit.model.AddPokitScreenStep
+import com.strayalpaca.addpokit.model.AddPokitSideEffect
+import com.strayalpaca.addpokit.model.PokitInputErrorMessage
+import com.strayalpaca.addpokit.model.PokitProfile
+import com.strayalpaca.addpokit.model.samplePokitList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.postSideEffect
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+
+class AddPokitViewModel : ContainerHost, ViewModel() {
+ override val container: Container = container(AddPokitScreenState())
+
+ private val _pokitName = MutableStateFlow("")
+ val pokitName: StateFlow = _pokitName.asStateFlow()
+
+ init {
+ loadPokitList()
+ }
+
+ private fun loadPokitList() = intent {
+ viewModelScope.launch {
+ reduce {
+ state.copy(
+ step = AddPokitScreenStep.POKIT_LIST_LOADING,
+ pokitInputErrorMessage = null,
+ pokitList = emptyList()
+ )
+ }
+ // todo 포킷 리스트 로드 api 연동
+ delay(1000L)
+
+ reduce {
+ state.copy(
+ step = AddPokitScreenStep.IDLE,
+ pokitList = samplePokitList
+ )
+ }
+ }
+ }
+
+ fun inputPokitName(pokitName: String) {
+ _pokitName.update { pokitName }
+
+ intent {
+ val isInAvailableLength = pokitName.length > POKIT_NAME_MAX_LENGTH
+ val isDuplicatePokitName = state.pokitList.find { it.title == pokitName } != null
+
+ val errorMessage = if (isInAvailableLength) {
+ PokitInputErrorMessage.TEXT_LENGTH_LIMIT
+ } else if (isDuplicatePokitName) {
+ PokitInputErrorMessage.ALREADY_USED_POKIT_NAME
+ } else {
+ null
+ }
+ reduce { state.copy(pokitInputErrorMessage = errorMessage) }
+ }
+ }
+
+ fun savePokit() = intent {
+ reduce {
+ state.copy(step = AddPokitScreenStep.POKIT_SAVE_LOADING)
+ }
+ // todo 포킷 저장 api 연동
+ delay(1000L)
+ reduce {
+ state.copy(step = AddPokitScreenStep.IDLE)
+ }
+ postSideEffect(AddPokitSideEffect.AddPokitSuccess)
+ }
+
+ fun onBackPressed() = intent {
+ val currentStep = state.step
+ when (currentStep) {
+ AddPokitScreenStep.POKIT_SAVE_LOADING -> {} // discard
+ AddPokitScreenStep.SELECT_PROFILE -> {
+ reduce { state.copy(step = AddPokitScreenStep.IDLE) }
+ }
+ else -> {
+ postSideEffect(AddPokitSideEffect.OnNavigationBack)
+ }
+ }
+ }
+
+ fun showPokitProfileSelectBottomSheet() = intent {
+ reduce {
+ state.copy(step = AddPokitScreenStep.SELECT_PROFILE)
+ }
+ }
+
+ fun hidePokitProfileSelectBottomSheet() = intent {
+ reduce {
+ state.copy(step = AddPokitScreenStep.IDLE)
+ }
+ }
+
+ fun selectPoktiProfile(pokitProfile: PokitProfile) = intent {
+ reduce {
+ state.copy(pokitProfile = pokitProfile)
+ }
+ }
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt
new file mode 100644
index 00000000..dbb00c58
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt
@@ -0,0 +1,24 @@
+package com.strayalpaca.addpokit
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.strayalpaca.addpokit.model.AddPokitScreenState
+import com.strayalpaca.addpokit.model.samplePokitList
+import pokitmons.pokit.core.ui.theme.PokitTheme
+
+@Preview(showBackground = true)
+@Composable
+fun Preview() {
+ PokitTheme {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ AddPokitScreen(
+ state = AddPokitScreenState().copy(pokitList = samplePokitList)
+ )
+ }
+ }
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt
new file mode 100644
index 00000000..91579c71
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt
@@ -0,0 +1,49 @@
+package com.strayalpaca.addpokit.components.atom
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.strayalpaca.addpokit.model.PokitProfile
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+
+@Composable
+fun PokitProfileImage(
+ pokitProfile: PokitProfile,
+ onClick: (PokitProfile) -> Unit,
+ focused: Boolean = false,
+) {
+ val activeStrokeColor = PokitTheme.colors.brand
+ val strokeColor = remember(focused) {
+ if (focused) {
+ activeStrokeColor
+ } else {
+ Color.Unspecified
+ }
+ }
+
+ Image(
+ painter = painterResource(id = coreDrawable.icon_24_plus_r),
+ contentDescription = "pokit profile image",
+ modifier = Modifier
+ .size(66.dp)
+ .clip(shape = RoundedCornerShape(12.dp))
+ .clickable {
+ onClick(pokitProfile)
+ }
+ .border(
+ color = strokeColor,
+ width = 1.dp,
+ shape = RoundedCornerShape(12.dp)
+ )
+ )
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/block/Toolbar.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/block/Toolbar.kt
new file mode 100644
index 00000000..8154ecce
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/block/Toolbar.kt
@@ -0,0 +1,54 @@
+package com.strayalpaca.addpokit.components.block
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import pokitmons.pokit.core.ui.theme.PokitTheme
+import pokitmons.pokit.core.ui.R.drawable as coreDrawable
+
+@Composable
+internal fun Toolbar(
+ onClickBack: () -> Unit,
+ title: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .height(56.dp)
+ .padding(horizontal = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ IconButton(
+ modifier = Modifier.size(48.dp),
+ onClick = onClickBack
+ ) {
+ Icon(
+ painter = painterResource(id = coreDrawable.icon_24_arrow_left),
+ contentDescription = "back button"
+ )
+ }
+
+ Text(
+ modifier = Modifier.weight(1f),
+ text = title,
+ style = PokitTheme.typography.title3.copy(color = PokitTheme.colors.textPrimary),
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.width(48.dp))
+ }
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/const/Consts.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/const/Consts.kt
new file mode 100644
index 00000000..9ec703a4
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/const/Consts.kt
@@ -0,0 +1,3 @@
+package com.strayalpaca.addpokit.const
+
+internal const val POKIT_NAME_MAX_LENGTH = 10
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt
new file mode 100644
index 00000000..511ee69c
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt
@@ -0,0 +1,29 @@
+package com.strayalpaca.addpokit.model
+
+import androidx.compose.runtime.Immutable
+import com.strayalpaca.addpokit.R
+
+@Immutable
+data class AddPokitScreenState(
+ val pokitInputErrorMessage: PokitInputErrorMessage? = null,
+ val pokitList: List = emptyList(),
+ val step: AddPokitScreenStep = AddPokitScreenStep.POKIT_LIST_LOADING,
+ val pokitProfile: PokitProfile? = null,
+)
+
+sealed class AddPokitScreenStep {
+ data object IDLE : AddPokitScreenStep()
+ data object POKIT_LIST_LOADING : AddPokitScreenStep()
+ data object POKIT_SAVE_LOADING : AddPokitScreenStep()
+ data object SELECT_PROFILE : AddPokitScreenStep()
+}
+
+sealed class AddPokitSideEffect {
+ data object AddPokitSuccess : AddPokitSideEffect()
+ data object OnNavigationBack : AddPokitSideEffect()
+}
+
+enum class PokitInputErrorMessage(val resourceId: Int) {
+ TEXT_LENGTH_LIMIT(R.string.text_length_limit_format),
+ ALREADY_USED_POKIT_NAME(R.string.already_used_pokit_name),
+}
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt
new file mode 100644
index 00000000..1b004274
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt
@@ -0,0 +1,15 @@
+package com.strayalpaca.addpokit.model
+
+data class Pokit(
+ val title: String,
+ val id: String,
+ val count: Int,
+)
+
+internal val samplePokitList = listOf(
+ Pokit(title = "안드로이드", id = "1", count = 2),
+ Pokit(title = "IOS", id = "2", count = 2),
+ Pokit(title = "디자인", id = "3", count = 2),
+ Pokit(title = "PM", id = "4", count = 1),
+ Pokit(title = "서버", id = "5", count = 2)
+)
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt
new file mode 100644
index 00000000..41274e56
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt
@@ -0,0 +1,13 @@
+package com.strayalpaca.addpokit.model
+
+data class PokitProfile(
+ val id: String,
+)
+
+internal val samplePokitProfileList =
+ listOf(
+ PokitProfile("1"), PokitProfile("2"), PokitProfile("3"), PokitProfile("4"),
+ PokitProfile("5"), PokitProfile("6"), PokitProfile("7"), PokitProfile("8"),
+ PokitProfile("9"), PokitProfile("10"), PokitProfile("11"), PokitProfile("12"),
+ PokitProfile("13"), PokitProfile("14"), PokitProfile("15")
+ )
diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/BackPressHandler.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/BackPressHandler.kt
new file mode 100644
index 00000000..2459aad0
--- /dev/null
+++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/BackPressHandler.kt
@@ -0,0 +1,35 @@
+package com.strayalpaca.addpokit.utils
+
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+
+@Composable
+internal fun BackPressHandler(
+ backPressedDispatcher: OnBackPressedDispatcher? =
+ LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
+ onBackPressed: () -> Unit,
+) {
+ val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
+
+ val backCallback = remember {
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ currentOnBackPressed()
+ }
+ }
+ }
+
+ DisposableEffect(backPressedDispatcher) {
+ backPressedDispatcher?.addCallback(backCallback)
+
+ onDispose {
+ backCallback.remove()
+ }
+ }
+}
diff --git a/feature/addpokit/src/main/res/values/string.xml b/feature/addpokit/src/main/res/values/string.xml
new file mode 100644
index 00000000..09e348fc
--- /dev/null
+++ b/feature/addpokit/src/main/res/values/string.xml
@@ -0,0 +1,11 @@
+
+
+ 포킷 추가
+ 포킷 이름
+ 카테고리 이름을 입력해주세요.
+ 저장하기
+ 내 포킷
+
+ 최대 10자까지 입력 가능합니다.
+ 사용 중인 포킷명입니다.
+
\ No newline at end of file
diff --git a/feature/addpokit/src/test/java/com/strayalpaca/addpokit/ExampleUnitTest.kt b/feature/addpokit/src/test/java/com/strayalpaca/addpokit/ExampleUnitTest.kt
new file mode 100644
index 00000000..03eade60
--- /dev/null
+++ b/feature/addpokit/src/test/java/com/strayalpaca/addpokit/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package com.strayalpaca.addpokit
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * 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)
+ }
+}