diff --git a/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt b/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt index 359db1b..6a1f480 100644 --- a/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt +++ b/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt @@ -7,7 +7,7 @@ import com.zucchini.ai_members.pm.AiPmActivity import com.zucchini.auth.LoginActivity import com.zucchini.common.NavigationProvider import com.zucchini.projects.MainActivity -import com.zucchini.submit.SubmitDevActivity +import com.zucchini.submit.developer.SubmitDevActivity import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -30,9 +30,8 @@ class DefaultNavigationProvider override fun toLogin(): Intent = Intent(context, LoginActivity::class.java) - override fun toSubmitDev(): Intent = Intent(context, SubmitDevActivity::class.java) - override fun toMain(): Intent = Intent(context, MainActivity::class.java) + override fun toSubmitDev(): Intent = Intent(context, SubmitDevActivity::class.java) override fun toAiPmMember(): Intent = Intent(context, AiPmActivity::class.java) diff --git a/buildSrc/src/main/java/com/zucchini/buildsrc/Dependencies.kt b/buildSrc/src/main/java/com/zucchini/buildsrc/Dependencies.kt index 04f28df..bf2bb9e 100644 --- a/buildSrc/src/main/java/com/zucchini/buildsrc/Dependencies.kt +++ b/buildSrc/src/main/java/com/zucchini/buildsrc/Dependencies.kt @@ -14,6 +14,7 @@ object AndroidXDependencies { const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompatVersion}" const val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintLayoutVersion}" + const val recyclerView = "androidx.recyclerview:recyclerview:${Versions.recyclerViewVersion}" const val startup = "androidx.startup:startup-runtime:${Versions.appStartUpVersion}" const val activity = "androidx.activity:activity-ktx:${Versions.activityKtxVersion}" const val fragment = "androidx.fragment:fragment-ktx:${Versions.fragmentKtxVersion}" @@ -68,6 +69,7 @@ object ThirdPartyDependencies { const val okHttpBom = "com.squareup.okhttp3:okhttp-bom:${Versions.okHttpVersion}" const val okHttp = "com.squareup.okhttp3:okhttp" const val okHttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor" + const val gson = "com.google.code.gson:gson:${Versions.gsonVersion}" const val timber = "com.jakewharton.timber:timber:${Versions.timberVersion}" @@ -101,7 +103,8 @@ object ClassPathPlugins { object ComposeDependencies { const val composeUi = "androidx.compose.ui:ui:${Versions.composeCompilerVersion}" - const val composeActivity = "androidx.activity:activity-compose:${Versions.activityComposeVersion}" + const val composeActivity = + "androidx.activity:activity-compose:${Versions.activityComposeVersion}" const val composeMaterial3 = "androidx.compose.material3:material3:${Versions.material3Version}" const val composeTooling = "androidx.compose.ui:ui-tooling:${Versions.composeToolingVersion}" } diff --git a/buildSrc/src/main/java/com/zucchini/buildsrc/Versions.kt b/buildSrc/src/main/java/com/zucchini/buildsrc/Versions.kt index d5082f0..c311ed6 100644 --- a/buildSrc/src/main/java/com/zucchini/buildsrc/Versions.kt +++ b/buildSrc/src/main/java/com/zucchini/buildsrc/Versions.kt @@ -45,6 +45,8 @@ object Versions { const val circleImageViewVersion = "3.1.0" const val dotIndicatorVersion = "5.0" const val processPhoenixVersion = "2.1.2" + const val recyclerViewVersion = "1.3.2" + const val gsonVersion = "2.11.0" val javaVersion = JavaVersion.VERSION_17 const val jvmVersion = "17" diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 4367668..3a026e3 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -28,6 +28,7 @@ android { buildFeatures { buildConfig = true + viewBinding = true } } @@ -38,6 +39,8 @@ dependencies { implementation(coreKtx) implementation(appCompat) implementation(pagingRuntime) + implementation(constraintLayout) + implementation(recyclerView) } KotlinDependencies.run { diff --git a/core/common/src/main/java/com/zucchini/context/ContextExt.kt b/core/common/src/main/java/com/zucchini/context/ContextExt.kt new file mode 100644 index 0000000..3288cce --- /dev/null +++ b/core/common/src/main/java/com/zucchini/context/ContextExt.kt @@ -0,0 +1,61 @@ +package com.zucchini.context + +import android.app.Dialog +import android.content.Context +import android.graphics.Point +import android.os.Build +import android.view.WindowInsets +import android.view.WindowManager + +fun Context.dialogViewPercent( + dialog: Dialog?, + widthPercent: Double = 0.8, + heightPercent: Double = 0.8, +) { + val deviceSize = getDeviceSize() + dialog?.window?.run { + val params = attributes + params.width = (deviceSize[0] * widthPercent).toInt() + params.height = (deviceSize[1] * heightPercent).toInt() + attributes = params + } +} + +fun Context.dialogWidthPercent( + dialog: Dialog?, + widthPercent: Double = 0.8, +) { + val deviceSize = getDeviceSize() + dialog?.window?.run { + val params = attributes + params.width = (deviceSize[0] * widthPercent).toInt() + attributes = params + } +} + +fun Context.getDeviceSize(): IntArray { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowMetrics = windowManager.currentWindowMetrics + val windowInsets = windowMetrics.windowInsets + + val insets = + windowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars() or WindowInsets.Type.displayCutout(), + ) + val insetsWidth = insets.right + insets.left + val insetsHeight = insets.top + insets.bottom + + val bounds = windowMetrics.bounds + + return intArrayOf(bounds.width() - insetsWidth, bounds.height() - insetsHeight) + } else { + val display = windowManager.defaultDisplay + val size = Point() + + display?.getSize(size) + + return intArrayOf(size.x, size.y) + } +} diff --git a/core/common/src/main/java/com/zucchini/view/ViewExt.kt b/core/common/src/main/java/com/zucchini/view/ViewExt.kt index 1ed3846..d7ee876 100644 --- a/core/common/src/main/java/com/zucchini/view/ViewExt.kt +++ b/core/common/src/main/java/com/zucchini/view/ViewExt.kt @@ -50,6 +50,9 @@ fun Context.showShortToast(message: String) { ).show() } +fun Fragment.showShortToast(message: String) { + context?.showShortToast(message) +} fun Fragment.hideKeyboard() { view?.let { activity?.hideKeyboard(it) } } diff --git a/core/common/src/main/res/drawable/button_bright_olive_radius6.xml b/core/common/src/main/res/drawable/button_bright_olive_radius6.xml new file mode 100644 index 0000000..79fe106 --- /dev/null +++ b/core/common/src/main/res/drawable/button_bright_olive_radius6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/core/common/src/main/res/drawable/button_main_olive_radius6.xml b/core/common/src/main/res/drawable/button_main_olive_radius6.xml new file mode 100644 index 0000000..1624798 --- /dev/null +++ b/core/common/src/main/res/drawable/button_main_olive_radius6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/core/common/src/main/res/drawable/button_olive_brown_radius6.xml b/core/common/src/main/res/drawable/button_olive_brown_radius6.xml new file mode 100644 index 0000000..ca85e4c --- /dev/null +++ b/core/common/src/main/res/drawable/button_olive_brown_radius6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/core/common/src/main/res/drawable/button_olive_white_radius6.xml b/core/common/src/main/res/drawable/button_olive_white_radius6.xml new file mode 100644 index 0000000..6cb92ee --- /dev/null +++ b/core/common/src/main/res/drawable/button_olive_white_radius6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/core/common/src/main/res/layout/dialog_common_select_checkbox_button.xml b/core/common/src/main/res/layout/dialog_common_select_checkbox_button.xml new file mode 100644 index 0000000..254dbcb --- /dev/null +++ b/core/common/src/main/res/layout/dialog_common_select_checkbox_button.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/common/src/main/res/layout/dialog_common_two_button.xml b/core/common/src/main/res/layout/dialog_common_two_button.xml new file mode 100644 index 0000000..be89c0e --- /dev/null +++ b/core/common/src/main/res/layout/dialog_common_two_button.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + diff --git a/core/common/src/main/res/layout/item_dialog_checkbox.xml b/core/common/src/main/res/layout/item_dialog_checkbox.xml new file mode 100644 index 0000000..149fa2b --- /dev/null +++ b/core/common/src/main/res/layout/item_dialog_checkbox.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml new file mode 100644 index 0000000..efd65c6 --- /dev/null +++ b/core/common/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + 개발자 이름 + 이메일 + 개발한 분야를 선택해주세요 + 카테고리↗ + 팀 리더 + 완료 + \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/dialog_button_background_main_olive.xml b/core/designsystem/src/main/res/drawable/dialog_button_background_main_olive.xml new file mode 100644 index 0000000..bebf15d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/dialog_button_background_main_olive.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/network/src/main/java/com/sample/network/reponse/SearchDeveloperResultResponse.kt b/core/network/src/main/java/com/sample/network/reponse/SearchDeveloperResultResponse.kt new file mode 100644 index 0000000..b12e257 --- /dev/null +++ b/core/network/src/main/java/com/sample/network/reponse/SearchDeveloperResultResponse.kt @@ -0,0 +1,14 @@ +package com.sample.network.reponse + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SearchDeveloperResultResponse( + @SerialName("id") + val id: Int?, + @SerialName("name") + val name: String?, + @SerialName("email") + val email: String?, +) diff --git a/core/network/src/main/java/com/sample/network/request/SubmitProjectRequest.kt b/core/network/src/main/java/com/sample/network/request/SubmitProjectRequest.kt new file mode 100644 index 0000000..d164d5b --- /dev/null +++ b/core/network/src/main/java/com/sample/network/request/SubmitProjectRequest.kt @@ -0,0 +1,33 @@ +package com.sample.network.request + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SubmitProjectRequest( + @SerialName("appLink") + val appLink: String, + @SerialName("category") + val category: String, + @SerialName("devToolList") + val devToolList: List, + @SerialName("githubLink") + val githubLink: String, + @SerialName("infoPageLink") + val infoPageLink: String, + @SerialName("languageList") + val languageList: List, + @SerialName("longIntro") + val longIntro: String, + @SerialName("name") + val name: String, + @SerialName("shortIntro") + val shortIntro: String, + @SerialName("techStackList") + val techStackList: List, + @SerialName("webLink") + val webLink: String, + @SerialName("projectDevloperList") + val projectDevloperList: List +) \ No newline at end of file diff --git a/core/network/src/main/java/com/sample/network/service/DevelopersService.kt b/core/network/src/main/java/com/sample/network/service/DevelopersService.kt index e334874..fd398b8 100644 --- a/core/network/src/main/java/com/sample/network/service/DevelopersService.kt +++ b/core/network/src/main/java/com/sample/network/service/DevelopersService.kt @@ -3,6 +3,7 @@ package com.sample.network.service import com.sample.network.model.BaseResponse import com.sample.network.reponse.DevelopersDetailResponse import com.sample.network.reponse.DevelopersListResponse +import com.sample.network.reponse.SearchDeveloperResultResponse import com.sample.network.request.CreateDevelopersRequest import retrofit2.http.Body import retrofit2.http.GET @@ -31,4 +32,9 @@ interface DevelopersService { @Query("email") email: String? = null, @Body request: CreateDevelopersRequest, ): BaseResponse + + @GET("/api/developers/search") + suspend fun searchDevelopers( + @Query("developerName") developerName: String, + ): BaseResponse> } diff --git a/core/network/src/main/java/com/sample/network/service/ProjectsService.kt b/core/network/src/main/java/com/sample/network/service/ProjectsService.kt index ad4d8c8..1401c2a 100644 --- a/core/network/src/main/java/com/sample/network/service/ProjectsService.kt +++ b/core/network/src/main/java/com/sample/network/service/ProjectsService.kt @@ -3,7 +3,13 @@ package com.sample.network.service import com.sample.network.model.BaseResponse import com.sample.network.reponse.ProjectsDetailResponse import com.sample.network.reponse.ProjectsListResponse +import com.sample.network.request.SubmitProjectRequest +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query @@ -20,4 +26,11 @@ interface ProjectsService { suspend fun getProjectsDetailData( @Path("projectId") projectId: Int, ): BaseResponse + + @Multipart + @POST("api/projects") + suspend fun submitProject( + @Part requestDTO: MultipartBody.Part, + @Part image: MultipartBody.Part, + ): BaseResponse } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 9cf27e8..d89266e 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(okHttpLoggingInterceptor) implementation(retrofitJsonConverter) implementation(timber) + implementation(gson) } TestDependencies.run { diff --git a/data/src/main/java/com/zucchini/data/ContentUriRequestBody.kt b/data/src/main/java/com/zucchini/data/ContentUriRequestBody.kt new file mode 100644 index 0000000..4a33875 --- /dev/null +++ b/data/src/main/java/com/zucchini/data/ContentUriRequestBody.kt @@ -0,0 +1,52 @@ +package com.zucchini.data + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source + +class ContentUriRequestBody( + context: Context, + private val uri: Uri, +) : RequestBody() { + private val contentResolver = context.contentResolver + + private var fileName = "" + private var size = -1L + + init { + contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DISPLAY_NAME), + null, + null, + null, + )?.use { cursor -> + if (cursor.moveToFirst()) { + size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) + fileName = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)) + } + } + } + + fun getFileName() = fileName + + override fun contentLength(): Long = size + + override fun contentType(): MediaType? = + contentResolver.getType(uri)?.toMediaTypeOrNull() + + override fun writeTo(sink: BufferedSink) { + contentResolver.openInputStream(uri)?.source()?.use { source -> + sink.writeAll(source) + } + } + + fun toFormData() = MultipartBody.Part.createFormData("postImg", getFileName(), this) +} \ No newline at end of file diff --git a/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt b/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt index 41d5539..8cee540 100644 --- a/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt +++ b/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.zucchini.data import com.sample.network.service.DevelopersService +import com.zucchini.domain.model.FindDeveloperInfo import com.zucchini.domain.model.developers.DevelopersDetailModel import com.zucchini.domain.model.developers.DevelopersListModel import com.zucchini.domain.model.submit.SubmitDevInfo @@ -8,6 +9,7 @@ import com.zucchini.domain.repository.DevelopersRepository import com.zucchini.mapper.toCreateDevelopersRequest import com.zucchini.mapper.toDevelopersDetailModel import com.zucchini.mapper.toDevelopersListModel +import com.zucchini.mapper.toSearchDeveloperResult import javax.inject.Inject class DevelopersRepositoryImpl @Inject constructor( @@ -48,4 +50,10 @@ class DevelopersRepositoryImpl @Inject constructor( ).data } } + + override suspend fun searchDevelopers(developerName: String): Result> { + return runCatching { + developersService.searchDevelopers(developerName).data.toSearchDeveloperResult() + } + } } diff --git a/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt b/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt index 80c6a7e..b1516f4 100644 --- a/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt +++ b/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt @@ -1,32 +1,67 @@ package com.zucchini.data +import com.google.gson.Gson import com.sample.network.service.ProjectsService +import com.zucchini.domain.model.SubmitProjectInfo import com.zucchini.domain.model.projects.ProjectsDetailModel import com.zucchini.domain.model.projects.ProjectsListModel import com.zucchini.domain.repository.ProjectsRepository import com.zucchini.mapper.toProjectsDetailModel import com.zucchini.mapper.toProjectsListModel +import com.zucchini.mapper.toSubmitProjectRequest +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File import javax.inject.Inject -class ProjectsRepositoryImpl @Inject constructor( +class ProjectsRepositoryImpl +@Inject +constructor( private val projectsService: ProjectsService, ) : ProjectsRepository { - override suspend fun getProjectsListData(searchString: String?, category: String?, sortType: String, page: Int): Result { - return runCatching { - projectsService.getProjectsListData( - searchString = searchString, - category = category, - sortType = sortType, - page = page, - ).data.toProjectsListModel() + override suspend fun getProjectsListData( + searchString: String?, + category: String?, + sortType: String, + page: Int, + ): Result = + runCatching { + projectsService + .getProjectsListData( + searchString = searchString, + category = category, + sortType = sortType, + page = page, + ).data + .toProjectsListModel() } - } - override suspend fun getProjectsDetailData( - projectId: Int, - ): Result { - return runCatching { + override suspend fun getProjectsDetailData(projectId: Int): Result = + runCatching { projectsService.getProjectsDetailData(projectId).data.toProjectsDetailModel() } - } + + override suspend fun submitProject( + submitProjectInfo: SubmitProjectInfo, + imagePath: String, + ): Result = + runCatching { + // JSON 데이터를 문자열로 변환 + val gson = Gson() + val json = gson.toJson(submitProjectInfo.toSubmitProjectRequest()) + val jsonRequestBody = json.toRequestBody("application/json".toMediaTypeOrNull()) + + // JSON requestBody를 MultipartBody.Part로 변환 + val jsonPart = MultipartBody.Part.createFormData("requestDTO", null, jsonRequestBody) + + // 파일을 MultipartBody.Part로 변환 + val file = File(imagePath) + val fileRequestBody = file.asRequestBody("image/*".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("image", file.name, fileRequestBody) + + // 서비스 호출 + projectsService.submitProject(jsonPart, imagePart).data + } } diff --git a/data/src/main/java/com/zucchini/mapper/SearchDeveloper.kt b/data/src/main/java/com/zucchini/mapper/SearchDeveloper.kt new file mode 100644 index 0000000..3563f1a --- /dev/null +++ b/data/src/main/java/com/zucchini/mapper/SearchDeveloper.kt @@ -0,0 +1,13 @@ +package com.zucchini.mapper + +import com.sample.network.reponse.SearchDeveloperResultResponse +import com.zucchini.domain.model.FindDeveloperInfo + +internal fun List.toSearchDeveloperResult(): List = + this.map { developer -> + FindDeveloperInfo( + developerName = developer.name ?: "", + developerEmail = developer.email ?: "", + developerId = developer.id ?: -1, + ) + } \ No newline at end of file diff --git a/data/src/main/java/com/zucchini/mapper/SubmitProjectInfo.kt b/data/src/main/java/com/zucchini/mapper/SubmitProjectInfo.kt new file mode 100644 index 0000000..515cad1 --- /dev/null +++ b/data/src/main/java/com/zucchini/mapper/SubmitProjectInfo.kt @@ -0,0 +1,23 @@ +package com.zucchini.mapper + +import com.sample.network.request.SubmitProjectRequest +import com.zucchini.domain.model.SubmitProjectInfo + + +internal fun SubmitProjectInfo.toSubmitProjectRequest(): SubmitProjectRequest { + return SubmitProjectRequest( + name = projectName, + shortIntro = projectShortIntro, + longIntro = projectLongIntro, + githubLink = projectGithub, + webLink = projectWebLink, + infoPageLink = projectLink, + appLink = projectAppLink, + category = projectCategoryList.get(0), + languageList = projectLanguageList, + devToolList = projectCooperationList, + techStackList = projectTechStackList, + projectDevloperList = projectDeveloperList + ) +} + diff --git a/domain/src/main/java/com/zucchini/domain/model/FindDeveloperInfo.kt b/domain/src/main/java/com/zucchini/domain/model/FindDeveloperInfo.kt new file mode 100644 index 0000000..dcade7f --- /dev/null +++ b/domain/src/main/java/com/zucchini/domain/model/FindDeveloperInfo.kt @@ -0,0 +1,7 @@ +package com.zucchini.domain.model + +data class FindDeveloperInfo( + val developerId: Int, + val developerName: String, + val developerEmail: String, +) \ No newline at end of file diff --git a/domain/src/main/java/com/zucchini/domain/model/SubmitProjectInfo.kt b/domain/src/main/java/com/zucchini/domain/model/SubmitProjectInfo.kt new file mode 100644 index 0000000..d75eb4c --- /dev/null +++ b/domain/src/main/java/com/zucchini/domain/model/SubmitProjectInfo.kt @@ -0,0 +1,17 @@ +package com.zucchini.domain.model + +data class SubmitProjectInfo( + var projectName: String = "", + var projectGithub: String = "", + var imagePath: String = "", + var projectShortIntro: String = "", + var projectLongIntro: String = "", + var projectCategoryList: List = emptyList(), + var projectTechStackList: List = emptyList(), + var projectLanguageList: List = emptyList(), + var projectCooperationList: List = emptyList(), + var projectWebLink: String = "", + var projectAppLink: String = "", + var projectLink: String = "", + var projectDeveloperList: List = emptyList(), +) \ No newline at end of file diff --git a/domain/src/main/java/com/zucchini/domain/model/projects/KeywordList.kt b/domain/src/main/java/com/zucchini/domain/model/projects/KeywordList.kt index 9d82da0..ba81366 100644 --- a/domain/src/main/java/com/zucchini/domain/model/projects/KeywordList.kt +++ b/domain/src/main/java/com/zucchini/domain/model/projects/KeywordList.kt @@ -1,52 +1,82 @@ package com.zucchini.domain.model.projects object KeywordList { - val searchKeyword = listOf( + val categoryList = arrayListOf( Keyword(keywordEnglish = "SERVICE", keywordKorean = "서비스"), Keyword(keywordEnglish = "SECURITY", keywordKorean = "보안"), - Keyword(keywordEnglish = "FINANCE", keywordKorean = "금융"), - Keyword(keywordEnglish = "PLATFORM", keywordKorean = "플랫폼"), Keyword(keywordEnglish = "GAME", keywordKorean = "게임"), Keyword(keywordEnglish = "MOBILE_APP", keywordKorean = "모바일"), Keyword(keywordEnglish = "WEB_APPLICATION", keywordKorean = "웹"), Keyword(keywordEnglish = "DATA_ANALYTICS", keywordKorean = "데이터 분석"), Keyword(keywordEnglish = "AI", keywordKorean = "인공지능"), - Keyword(keywordEnglish = "ML", keywordKorean = "머신러닝"), Keyword(keywordEnglish = "IOT", keywordKorean = "IOT"), Keyword(keywordEnglish = "CLOUD_COMPUTING", keywordKorean = "클라우드"), Keyword(keywordEnglish = "BLOCKCHAIN", keywordKorean = "블록체인"), - Keyword(keywordEnglish = "E_COMMERCE", keywordKorean = "전자상거래"), - Keyword(keywordEnglish = "HEALTHCARE_IT", keywordKorean = "헬스케어"), - Keyword(keywordEnglish = "ED_TECH", keywordKorean = "교육"), - Keyword(keywordEnglish = "SOCIAL_MEDIA", keywordKorean = "소셜"), - Keyword(keywordEnglish = "CRM", keywordKorean = "CRM"), - Keyword(keywordEnglish = "ERP", keywordKorean = "ERP"), - Keyword(keywordEnglish = "BI", keywordKorean = "BI"), - Keyword(keywordEnglish = "CPS", keywordKorean = "CPS"), - Keyword(keywordEnglish = "AR", keywordKorean = "AR"), - Keyword("VR", keywordKorean = "VR"), - Keyword(keywordEnglish = "AUTOMATION", keywordKorean = "자동화"), + Keyword("VRAR", keywordKorean = "VR/AR"), Keyword(keywordEnglish = "ROBOTICS", keywordKorean = "로봇"), Keyword(keywordEnglish = "OTHER", keywordKorean = "기타"), ) - val partList = listOf( - Keyword(keywordEnglish = "FRONTEND", keywordKorean = "프론트엔드"), + val partList = arrayListOf( Keyword(keywordEnglish = "BACKEND", keywordKorean = "백엔드"), - Keyword(keywordEnglish = "FULLSTACK", keywordKorean = "풀스택"), Keyword(keywordEnglish = "MIDDLE_TIER", keywordKorean = "미들웨어"), Keyword(keywordEnglish = "WEB", keywordKorean = "웹"), Keyword(keywordEnglish = "DESKTOP", keywordKorean = "데스크탑"), + Keyword(keywordEnglish = "IOS", keywordKorean = "iOS"), + Keyword(keywordEnglish = "ANDROID", keywordKorean = "Android"), Keyword(keywordEnglish = "MOBILE", keywordKorean = "모바일"), Keyword(keywordEnglish = "GRAPHICS", keywordKorean = "그래픽"), Keyword(keywordEnglish = "GAME", keywordKorean = "게임"), Keyword(keywordEnglish = "DATA_SCIENTIST", keywordKorean = "데이터 과학자"), Keyword(keywordEnglish = "BIG_DATA", keywordKorean = "빅데이터"), Keyword(keywordEnglish = "DEVOPS", keywordKorean = "데브옵스"), - Keyword(keywordEnglish = "CRM", keywordKorean = "CRM"), Keyword(keywordEnglish = "SOFTWARE_TEST", keywordKorean = "테스트"), Keyword(keywordEnglish = "EMBEDDED_SOFTWARE", keywordKorean = "임베디드"), - Keyword(keywordEnglish = "WORDPRESS", keywordKorean = "워드프레스"), Keyword(keywordEnglish = "SECURITY", keywordKorean = "보안"), ) + + val cooperationList = arrayListOf( + Keyword(keywordEnglish = "NOTION", keywordKorean = "Notion"), + Keyword(keywordEnglish = "GITHUB", keywordKorean = "Github"), + Keyword(keywordEnglish = "SLACK", keywordKorean = "Slack"), + Keyword(keywordEnglish = "JIRA", keywordKorean = "Jira"), + Keyword(keywordEnglish = "TRELLO", keywordKorean = "Trello"), + Keyword(keywordEnglish = "FIGMA", keywordKorean = "Figma"), + ) + + val techStackList = arrayListOf( + Keyword(keywordEnglish = "DJANGO", keywordKorean = "Django"), + Keyword(keywordEnglish = "SPRING", keywordKorean = "Spring"), + Keyword(keywordEnglish = "REACT", keywordKorean = "React"), + Keyword(keywordEnglish = "ANGULAR", keywordKorean = "Angular"), + Keyword(keywordEnglish = "VUE", keywordKorean = "Vue"), + Keyword(keywordEnglish = "AWS", keywordKorean = "AWS"), + Keyword(keywordEnglish = "AZURE", keywordKorean = "Azure"), + Keyword(keywordEnglish = "DOCKER", keywordKorean = "Docker"), + Keyword(keywordEnglish = "LARAVEL", keywordKorean = "Laravel"), + Keyword(keywordEnglish = "EXPRESS", keywordKorean = "Express"), + Keyword(keywordEnglish = "NODE_JS", keywordKorean = "Node.js"), + Keyword(keywordEnglish = "DOT_NET", keywordKorean = ".net"), + Keyword(keywordEnglish = "JENKINS", keywordKorean = "Jenkins"), + Keyword(keywordEnglish = "KUBERNETES", keywordKorean = "Kubernetes"), + Keyword(keywordEnglish = "GOOGLE_CLOUD", keywordKorean = "Google Cloud"), + ) + + val languageList = arrayListOf( + Keyword(keywordEnglish = "GO", keywordKorean = "Go"), + Keyword(keywordEnglish = "JAVA", keywordKorean = "Java"), + Keyword(keywordEnglish = "KOTLIN", keywordKorean = "Kotlin"), + Keyword(keywordEnglish = "SWIFT", keywordKorean = "Swift"), + Keyword(keywordEnglish = "JAVASCRIPT", keywordKorean = "JavaScript"), + Keyword(keywordEnglish = "TS", keywordKorean = "TypeScript"), + Keyword(keywordEnglish = "PYTHON", keywordKorean = "Python"), + Keyword(keywordEnglish = "C", keywordKorean = "C"), + Keyword(keywordEnglish = "CPP", keywordKorean = "C++"), + Keyword(keywordEnglish = "CS", keywordKorean = "C#"), + Keyword(keywordEnglish = "PHP", keywordKorean = "PHP"), + Keyword(keywordEnglish = "R", keywordKorean = "R"), + Keyword(keywordEnglish = "RUBY", keywordKorean = "RUBY"), + Keyword(keywordEnglish = "RUST", keywordKorean = "RUST"), + Keyword(keywordEnglish = "SQL", keywordKorean = "SQL"), + ) } diff --git a/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt b/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt index 2edd3c3..616448b 100644 --- a/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt +++ b/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt @@ -1,5 +1,6 @@ package com.zucchini.domain.repository +import com.zucchini.domain.model.FindDeveloperInfo import com.zucchini.domain.model.developers.DevelopersDetailModel import com.zucchini.domain.model.developers.DevelopersListModel import com.zucchini.domain.model.submit.SubmitDevInfo @@ -8,5 +9,11 @@ interface DevelopersRepository { suspend fun getDevelopersListData(page: Int, part: String?): Result suspend fun getDevelopersDetailData(developerId: Int): Result - suspend fun createDeveloperInfo(accessToken: String, email: String? = null, submitDevInfo: SubmitDevInfo): Result + suspend fun createDeveloperInfo( + accessToken: String, + email: String? = null, + submitDevInfo: SubmitDevInfo + ): Result + + suspend fun searchDevelopers(developerName: String): Result> } diff --git a/domain/src/main/java/com/zucchini/domain/repository/ProjectsRepository.kt b/domain/src/main/java/com/zucchini/domain/repository/ProjectsRepository.kt index 104d0fd..b487301 100644 --- a/domain/src/main/java/com/zucchini/domain/repository/ProjectsRepository.kt +++ b/domain/src/main/java/com/zucchini/domain/repository/ProjectsRepository.kt @@ -1,9 +1,21 @@ package com.zucchini.domain.repository +import com.zucchini.domain.model.SubmitProjectInfo import com.zucchini.domain.model.projects.ProjectsDetailModel import com.zucchini.domain.model.projects.ProjectsListModel interface ProjectsRepository { - suspend fun getProjectsListData(searchString: String?, category: String?, sortType: String, page: Int): Result + suspend fun getProjectsListData( + searchString: String?, + category: String?, + sortType: String, + page: Int, + ): Result + suspend fun getProjectsDetailData(projectId: Int): Result + + suspend fun submitProject( + submitProjectInfo: SubmitProjectInfo, + imagePath: String, + ): Result } diff --git a/feature/projects/build.gradle.kts b/feature/projects/build.gradle.kts index 81e450c..a78c4b0 100644 --- a/feature/projects/build.gradle.kts +++ b/feature/projects/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(project(":core:designsystem")) implementation(project(":core:common")) implementation(project(":core:network")) + implementation(project(":data")) KotlinDependencies.run { implementation(kotlin) diff --git a/feature/projects/src/main/AndroidManifest.xml b/feature/projects/src/main/AndroidManifest.xml index c7c42c9..1bb17da 100644 --- a/feature/projects/src/main/AndroidManifest.xml +++ b/feature/projects/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ - - + + + + diff --git a/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt b/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt index 90b90e9..aa96a61 100644 --- a/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt @@ -10,7 +10,7 @@ import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.ActivityLoginBinding import com.zucchini.projects.MainActivity import com.zucchini.projects.adapter.IntroducePagerAdapter -import com.zucchini.submit.SubmitDevActivity +import com.zucchini.submit.developer.SubmitDevActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/feature/projects/src/main/java/com/zucchini/dialog/CheckBoxListAdapter.kt b/feature/projects/src/main/java/com/zucchini/dialog/CheckBoxListAdapter.kt new file mode 100644 index 0000000..dca37de --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/dialog/CheckBoxListAdapter.kt @@ -0,0 +1,52 @@ +package com.zucchini.dialog + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.zucchini.core.common.databinding.ItemDialogCheckboxBinding + +class CheckBoxListAdapter( + private val items: List, + private val selectedItems: MutableList, + private val maxSelectionCount: Int +) : RecyclerView.Adapter() { + + class ViewHolder( + val binding: ItemDialogCheckboxBinding + ) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val binding = + ItemDialogCheckboxBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder( + holder: ViewHolder, + position: Int + ) { + val item = items[position] + holder.binding.cbDialogContent.text = item + + // Initialize checkbox state + holder.binding.cbDialogContent.isChecked = selectedItems.contains(item) + + holder.binding.cbDialogContent.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + // Check if the maximum selection count is not exceeded + if (selectedItems.size < maxSelectionCount) { + selectedItems.add(item) + } else { + holder.binding.cbDialogContent.isChecked = false + } + } else { + selectedItems.remove(item) + } + } + } + + override fun getItemCount() = items.size +} diff --git a/feature/projects/src/main/java/com/zucchini/dialog/SelectCheckBoxCommonDialog.kt b/feature/projects/src/main/java/com/zucchini/dialog/SelectCheckBoxCommonDialog.kt new file mode 100644 index 0000000..3d5e11b --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/dialog/SelectCheckBoxCommonDialog.kt @@ -0,0 +1,115 @@ +package com.zucchini.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.zucchini.context.dialogViewPercent +import com.zucchini.core.common.databinding.DialogCommonSelectCheckboxButtonBinding +import com.zucchini.view.setOnSingleClickListener + +class SelectCheckBoxCommonDialog : DialogFragment() { + private lateinit var binding: DialogCommonSelectCheckboxButtonBinding + + private var confirmButtonClickListener: ((List) -> Unit)? = null + private var items: List = listOf() + private val selectedItems = mutableListOf() + private var maxSelectionCount: Int = Int.MAX_VALUE + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DialogCommonSelectCheckboxButtonBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + initViews() + initButtonListener() + } + + override fun onResume() { + super.onResume() + context?.dialogViewPercent(dialog) + dialog?.window?.run { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } + + fun setConfirmButtonClickListener(confirmButtonClickListener: (List) -> Unit) { + this.confirmButtonClickListener = confirmButtonClickListener + } + + private fun initViews() { + val title = arguments?.getString(TITLE, "") + val description = arguments?.getString(DESCRIPTION) + val confirmButtonText = arguments?.getString(CONFIRM_BUTTON_TEXT, "") + items = arguments?.getStringArrayList(ITEMS) ?: listOf() + maxSelectionCount = arguments?.getInt(MAX_SELECTION_COUNT, Int.MAX_VALUE) ?: Int.MAX_VALUE + + with(binding) { + tvDialogTitle.text = title + tvDialogDescription.text = description + tvConfirmButton.text = confirmButtonText + + rvDialogContents.layoutManager = LinearLayoutManager(context) + rvDialogContents.adapter = CheckBoxListAdapter(items, selectedItems, maxSelectionCount) + } + } + + private fun initButtonListener() { + binding.tvConfirmButton.setOnSingleClickListener { + confirmButtonClickListener?.invoke(selectedItems) + dismiss() + } + } + + fun showAllowingStateLoss( + fm: FragmentManager, + tag: String = "", + ) { + fm + .beginTransaction() + .add(this, tag) + .commitAllowingStateLoss() + } + + companion object { + const val TAG = "SelectCheckBoxCommonDialog" + + const val TITLE = "title" + const val DESCRIPTION = "description" + const val CONFIRM_BUTTON_TEXT = "confirmButtonText" + const val ITEMS = "items" + const val MAX_SELECTION_COUNT = "maxSelectionCount" + + fun newInstance( + title: String, + description: String? = null, + confirmButtonText: String, + items: ArrayList, + maxSelectionCount: Int = Int.MAX_VALUE + ): SelectCheckBoxCommonDialog = + SelectCheckBoxCommonDialog().apply { + arguments = + Bundle().apply { + putString(TITLE, title) + putString(DESCRIPTION, description) + putString(CONFIRM_BUTTON_TEXT, confirmButtonText) + putStringArrayList(ITEMS, items) + putInt(MAX_SELECTION_COUNT, maxSelectionCount) + } + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/dialog/TwoButtonCommonDialog.kt b/feature/projects/src/main/java/com/zucchini/dialog/TwoButtonCommonDialog.kt new file mode 100644 index 0000000..9d454c8 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/dialog/TwoButtonCommonDialog.kt @@ -0,0 +1,114 @@ +package com.zucchini.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.zucchini.context.dialogWidthPercent +import com.zucchini.core.common.databinding.DialogCommonTwoButtonBinding +import com.zucchini.view.setOnSingleClickListener + +class TwoButtonCommonDialog : DialogFragment() { + private lateinit var binding: DialogCommonTwoButtonBinding + + private var confirmButtonClickListener: (() -> Unit)? = null + private var dismissButtonClickListener: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DialogCommonTwoButtonBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + initViews() + initButtonListener() + } + + override fun onResume() { + super.onResume() + context?.dialogWidthPercent(dialog) + dialog?.window?.run { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } + + fun setConfirmButtonClickListener(confirmButtonClickListener: () -> Unit) { + this.confirmButtonClickListener = confirmButtonClickListener + } + + fun setDismissButtonClickListener(dismissButtonClickListener: () -> Unit) { + this.dismissButtonClickListener = dismissButtonClickListener + } + + private fun initViews() { + val title = arguments?.getString(TITLE, "") + val description = arguments?.getString(DESCRIPTION) + val confirmButtonText = arguments?.getString(CONFIRM_BUTTON_TEXT, "") + val dismissButtonText = arguments?.getString(DISMISS_BUTTON_TEXT, "") + + with(binding) { + tvDialogTitle.text = title + tvDialogDescription.text = description + tvConfirmButton.text = confirmButtonText + tvDismissButton.text = dismissButtonText + } + } + + private fun initButtonListener() { + binding.tvConfirmButton.setOnSingleClickListener { + confirmButtonClickListener?.invoke() + dismiss() + } + binding.tvDismissButton.setOnSingleClickListener { + dismissButtonClickListener?.invoke() + dismiss() + } + } + + fun showAllowingStateLoss( + fm: FragmentManager, + tag: String = "", + ) { + fm + .beginTransaction() + .add(this, tag) + .commitAllowingStateLoss() + } + + companion object { + const val TAG = "TwoButtonCommonDialog" + + const val TITLE = "title" + const val DESCRIPTION = "description" + const val CONFIRM_BUTTON_TEXT = "confirmButtonText" + const val DISMISS_BUTTON_TEXT = "dismissButtonText" + + fun newInstance( + title: String, + description: String? = null, + confirmButtonText: String, + dismissButtonText: String, + ): TwoButtonCommonDialog = + TwoButtonCommonDialog().apply { + arguments = + Bundle().apply { + putString(TITLE, title) + putString(DESCRIPTION, description) + putString(CONFIRM_BUTTON_TEXT, confirmButtonText) + putString(DISMISS_BUTTON_TEXT, dismissButtonText) + } + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/mypage/MypageFragment.kt b/feature/projects/src/main/java/com/zucchini/mypage/MypageFragment.kt index e9ee4ad..06439e0 100644 --- a/feature/projects/src/main/java/com/zucchini/mypage/MypageFragment.kt +++ b/feature/projects/src/main/java/com/zucchini/mypage/MypageFragment.kt @@ -14,6 +14,7 @@ import coil.load import coil.transform.RoundedCornersTransformation import com.sample.network.datastore.NetworkPreference import com.zucchini.common.NavigationProvider +import com.zucchini.dialog.TwoButtonCommonDialog import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.FragmentMypageBinding import com.zucchini.projects.developer.DevDetailActivity @@ -117,13 +118,34 @@ class MypageFragment : Fragment() { private fun clickLogout() { binding.tvLogout.setOnClickListener { - viewModel.logout() + + } + + binding.tvLogout.setOnClickListener { + TwoButtonCommonDialog.newInstance( + title = getString(R.string.logout_dialog_title), + confirmButtonText = getString(R.string.all_check), + dismissButtonText = getString(R.string.all_cancel), + ).apply { + setConfirmButtonClickListener { + viewModel.logout() + } + }.showAllowingStateLoss(childFragmentManager) } } private fun clickWithdrawal() { binding.tvWithdrawal.setOnClickListener { - viewModel.withdrawal() + TwoButtonCommonDialog.newInstance( + title = getString(R.string.withdrawal_dialog_title), + description = getString(R.string.withdrawal_dialog_description), + confirmButtonText = getString(R.string.all_withdrawal), + dismissButtonText = getString(R.string.all_cancel), + ).apply { + setConfirmButtonClickListener { + viewModel.withdrawal() + } + }.showAllowingStateLoss(childFragmentManager) } } } diff --git a/feature/projects/src/main/java/com/zucchini/projects/projects/ProjectsFragment.kt b/feature/projects/src/main/java/com/zucchini/projects/projects/ProjectsFragment.kt index ed58080..a704af3 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/projects/ProjectsFragment.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/projects/ProjectsFragment.kt @@ -1,7 +1,6 @@ package com.zucchini.projects.projects import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -20,6 +19,7 @@ import com.zucchini.projects.adapter.PageIndicatorAdapter import com.zucchini.projects.projects.adapter.ProjectsAdapter import com.zucchini.projects.projects.adapter.SearchKeywordAdapter import com.zucchini.projects.projects.viewmodel.ProjectsListViewModel +import com.zucchini.submit.project.SubmitProjectActivity import com.zucchini.view.hideKeyboard import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn @@ -152,14 +152,13 @@ class ProjectsFragment : Fragment() { binding.rvSearchKeyword.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) binding.rvSearchKeyword.adapter = searchKeywordAdapter - searchKeywordAdapter.submitList(KeywordList.searchKeyword) + searchKeywordAdapter.submitList(KeywordList.categoryList) } private fun initSubmitProjectButton() { binding.floatingActionButton.setOnClickListener { - val projectFormUri = getString(com.zucchini.feature.projects.R.string.project_form) - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(projectFormUri)) + val intent = Intent(context, SubmitProjectActivity::class.java) startActivity(intent) } } diff --git a/feature/projects/src/main/java/com/zucchini/submit/SubmitDevActivity.kt b/feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevActivity.kt similarity index 99% rename from feature/projects/src/main/java/com/zucchini/submit/SubmitDevActivity.kt rename to feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevActivity.kt index 50e627e..000df5c 100644 --- a/feature/projects/src/main/java/com/zucchini/submit/SubmitDevActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevActivity.kt @@ -1,4 +1,4 @@ -package com.zucchini.submit +package com.zucchini.submit.developer import android.content.Intent import android.os.Bundle diff --git a/feature/projects/src/main/java/com/zucchini/submit/SubmitDevViewModel.kt b/feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevViewModel.kt similarity index 99% rename from feature/projects/src/main/java/com/zucchini/submit/SubmitDevViewModel.kt rename to feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevViewModel.kt index 23a2a99..f78c294 100644 --- a/feature/projects/src/main/java/com/zucchini/submit/SubmitDevViewModel.kt +++ b/feature/projects/src/main/java/com/zucchini/submit/developer/SubmitDevViewModel.kt @@ -1,4 +1,4 @@ -package com.zucchini.submit +package com.zucchini.submit.developer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectActivity.kt b/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectActivity.kt new file mode 100644 index 0000000..807c0f2 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectActivity.kt @@ -0,0 +1,46 @@ +package com.zucchini.submit.project + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.zucchini.feature.projects.databinding.ActivitySubmitProjectBinding +import com.zucchini.submit.project.adapter.SubmitProjectPagerAdapter +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SubmitProjectActivity : AppCompatActivity() { + private lateinit var binding: ActivitySubmitProjectBinding + private val viewModel: SubmitProjectViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySubmitProjectBinding.inflate(layoutInflater) + setContentView(binding.root) + + initViews() + initListeners() + } + + private fun initViews() { + binding.vpSubmitProject.adapter = SubmitProjectPagerAdapter(this) + binding.vpSubmitProject.isUserInputEnabled = false + } + + private fun initListeners() { + binding.ivBackButton.setOnClickListener { + handleBackNavigation() + } + } + + private fun handleBackNavigation() { + if (binding.vpSubmitProject.currentItem > 0) { + binding.vpSubmitProject.currentItem -= 1 + } else { + finish() + } + } + + fun setCurrentItem(item: Int) { + binding.vpSubmitProject.setCurrentItem(item, true) + } +} diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectViewModel.kt b/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectViewModel.kt new file mode 100644 index 0000000..2d6c174 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/SubmitProjectViewModel.kt @@ -0,0 +1,137 @@ +package com.zucchini.submit.project + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zucchini.domain.model.FindDeveloperInfo +import com.zucchini.domain.model.SubmitProjectInfo +import com.zucchini.domain.repository.DevelopersRepository +import com.zucchini.domain.repository.ProjectsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class SubmitProjectViewModel + @Inject + constructor( + private val developersRepository: DevelopersRepository, + private val projectsRepository: ProjectsRepository, + ) : ViewModel() { + private val _projectName = MutableStateFlow("") + + private val _imagePath = MutableStateFlow("") + + private val _projectGithub = MutableStateFlow("") + + private val _projectShortIntro = MutableStateFlow("") + + private val _projectLongIntro = MutableStateFlow("") + + private val _projectCategoryList = MutableStateFlow(emptyList()) + + private val _projectTechStackList = MutableStateFlow(emptyList()) + + private val _projectLanguageList = MutableStateFlow(emptyList()) + + private val _projectCooperationList = MutableStateFlow(emptyList()) + + private val _projectWebLink = MutableStateFlow("") + + private val _projectAppLink = MutableStateFlow("") + + private val _projectLink = MutableStateFlow("") + + private val _addProjectDeveloperList = MutableStateFlow(emptyList()) + + private val _searchDeveloperResultList = MutableStateFlow(emptyList()) + val searchDeveloperResultList = _searchDeveloperResultList.asStateFlow() + + private val _isSuccessSubmitProject = MutableStateFlow(false) + val isSuccessSubmitProject = _isSuccessSubmitProject.asStateFlow() + + fun updateProjectInfo( + projectName: String, + projectGithub: String, + projectShortIntro: String, + projectLongIntro: String, + projectWebLink: String, + projectAppLink: String, + projectLink: String, + ) { + _projectName.value = projectName + _projectGithub.value = projectGithub + _projectShortIntro.value = projectShortIntro + _projectLongIntro.value = projectLongIntro + _projectWebLink.value = projectWebLink + _projectAppLink.value = projectAppLink + _projectLink.value = projectLink + } + + fun updateImagePath(imagePath: String) { + _imagePath.value = imagePath + } + + fun updateProjectCategory(projectCategoryList: List = emptyList()) { + _projectCategoryList.value = projectCategoryList + } + + fun updateProjectTechStack(projectTechStackList: List = emptyList()) { + _projectTechStackList.value = projectTechStackList + } + + fun updateProjectLanguage(projectLanguageList: List = emptyList()) { + _projectLanguageList.value = projectLanguageList + } + + fun updateProjectCooperation(projectCooperationList: List = emptyList()) { + _projectCooperationList.value = projectCooperationList + } + + fun addDeveloperToProject(developerId: Int) { + _addProjectDeveloperList.value += developerId + } + + fun removeDeveloperFromProject(developerId: Int) { + _addProjectDeveloperList.value -= developerId + } + + fun searchDeveloperList(searchKeyword: String) { + viewModelScope.launch { + developersRepository.searchDevelopers(searchKeyword).onSuccess { + _searchDeveloperResultList.value = it + } + } + } + + fun submitProject() { + viewModelScope.launch { + val submit = SubmitProjectInfo() + submit.projectName = _projectName.value + submit.projectGithub = _projectGithub.value + submit.projectShortIntro = _projectShortIntro.value + submit.projectLongIntro = _projectLongIntro.value + submit.projectCategoryList = _projectCategoryList.value + submit.projectTechStackList = _projectTechStackList.value + submit.projectLanguageList = _projectLanguageList.value + submit.projectCooperationList = _projectCooperationList.value + submit.projectWebLink = _projectWebLink.value + submit.projectAppLink = _projectAppLink.value + submit.projectLink = _projectLink.value + submit.projectDeveloperList = _addProjectDeveloperList.value + + projectsRepository + .submitProject(submit, _imagePath.value) + .onSuccess { + _isSuccessSubmitProject.value = true + }.onFailure { + _isSuccessSubmitProject.value = false + Timber.d("failed to submit project $it") + Log.e("SubmitProjectViewModel", "failed to submit project", it) + } + } + } + } diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/adapter/FindDevAdapter.kt b/feature/projects/src/main/java/com/zucchini/submit/project/adapter/FindDevAdapter.kt new file mode 100644 index 0000000..9cae3dd --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/adapter/FindDevAdapter.kt @@ -0,0 +1,57 @@ +package com.zucchini.submit.project.adapter + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.zucchini.domain.model.FindDeveloperInfo +import com.zucchini.domain.model.SubmitProjectInfo +import com.zucchini.feature.projects.databinding.ItemFindDevBinding +import com.zucchini.submit.project.SubmitProjectViewModel +import com.zucchini.view.ItemDiffCallback + +class FindDevAdapter( + private val viewModel: SubmitProjectViewModel +) : ListAdapter( + ItemDiffCallback( + onItemsTheSame = { old, new -> old == new }, + onContentsTheSame = { old, new -> old == new }, + ), +) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): FindDevViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ItemFindDevBinding.inflate(inflater, parent, false) + return FindDevViewHolder(binding) + } + + override fun onBindViewHolder( + holder: FindDevViewHolder, + position: Int, + ) { + holder.bind(getItem(position)) + } + + inner class FindDevViewHolder( + private val binding: ItemFindDevBinding, + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(findDevInfo: FindDeveloperInfo) { + binding.run { + tvDeveloperName.text = findDevInfo.developerName + tvDeveloperEmail.text = findDevInfo.developerEmail + + root.setOnClickListener { + cbFindDev.isChecked = !cbFindDev.isChecked + if (cbFindDev.isChecked) { + viewModel.addDeveloperToProject(findDevInfo.developerId) + } else { + viewModel.removeDeveloperFromProject(findDevInfo.developerId) + } + } + } + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/adapter/SubmitProjectPagerAdapter.kt b/feature/projects/src/main/java/com/zucchini/submit/project/adapter/SubmitProjectPagerAdapter.kt new file mode 100644 index 0000000..23bf387 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/adapter/SubmitProjectPagerAdapter.kt @@ -0,0 +1,23 @@ +package com.zucchini.submit.project.adapter + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.zucchini.submit.project.fragment.SubmitProjectFragment +import com.zucchini.submit.project.fragment.SubmitProjectWithDevFragment + +class SubmitProjectPagerAdapter( + activity: AppCompatActivity, +) : FragmentStateAdapter(activity) { + override fun getItemCount(): Int { + return 2 + } + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> SubmitProjectFragment() + 1 -> SubmitProjectWithDevFragment() + else -> throw IllegalStateException("Invalid position: $position") + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectFragment.kt b/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectFragment.kt new file mode 100644 index 0000000..3e2e324 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectFragment.kt @@ -0,0 +1,231 @@ +package com.zucchini.submit.project.fragment + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.zucchini.dialog.SelectCheckBoxCommonDialog +import com.zucchini.domain.model.projects.KeywordList +import com.zucchini.feature.projects.R +import com.zucchini.feature.projects.databinding.FragmentSubmitProjectBinding +import com.zucchini.submit.project.SubmitProjectActivity +import com.zucchini.submit.project.SubmitProjectViewModel +import com.zucchini.view.hideKeyboard + +class SubmitProjectFragment : Fragment() { + private var _binding: FragmentSubmitProjectBinding? = null + private val binding: FragmentSubmitProjectBinding get() = _binding!! + + private val viewModel: SubmitProjectViewModel by activityViewModels() + + // 권한 요청 + private val requestPermissionLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + navigateToGallery() + } + } + + // 가져온 사진 보여주기 + private val pickImageLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + handleImageResult(result) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentSubmitProjectBinding.inflate(inflater, container, false) + + initImageSubmitView() + initDialogClickListener() + clickSubmitButton() + hideKeyboardClickListener() + + return binding.root + } + + private fun hideKeyboardClickListener() { + binding.root.setOnClickListener { + hideKeyboard() + } + } + + private fun initImageSubmitView() { + binding.tvProjectSubmitImage.setOnClickListener { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.READ_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + ) { + navigateToGallery() + } else { + requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + } + + private fun handleImageResult(result: ActivityResult) { + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + data?.data?.let { uri -> + // 이미지 URI를 사용하여 이미지를 표시하거나 추가 작업을 수행합니다. + binding.ivProjectSubmit.setImageURI(uri) + val uriToAbsolutePath = absolutelyPath(uri, requireContext()) + viewModel.updateImagePath(uriToAbsolutePath) + Log.d("Selected Image", "Selected Image URI: $uri") + } + } else { + Log.d("Image Failure", "Image selection failed or was canceled.") + } + } + + private fun absolutelyPath( + path: Uri?, + context: Context, + ): String { + val proj: Array = arrayOf(MediaStore.Images.Media.DATA) + val c: Cursor? = context.contentResolver.query(path!!, proj, null, null, null) + val index = c?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + c?.moveToFirst() + + val result = c?.getString(index!!) + + return result!! + } + + private fun navigateToGallery() { + val gallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI) + pickImageLauncher.launch(gallery) + } + + private fun initDialogClickListener() { + selectProjectCategory() + selectStacks() + } + + private fun selectProjectCategory() { + // 카테고리 키워드 리스트를 한글과 영어 키워드를 매핑하는 Map으로 준비 + val keywordMap = KeywordList.categoryList.associateBy { it.keywordKorean } + + binding.tvSubmitProjectCategory.setOnClickListener { + SelectCheckBoxCommonDialog + .newInstance( + title = "프로젝트 카테고리", + description = "프로젝트 카테고리를 선택해주세요. (최대 2개)", + confirmButtonText = getString(R.string.all_check), + items = keywordMap.keys.toList() as ArrayList, // 한글 리스트 전달 + maxSelectionCount = 2, + ).apply { + setConfirmButtonClickListener { selectedItems -> + // 선택한 한글 키워드를 영어 키워드로 변환 + val selectedEnglishItems = + selectedItems.mapNotNull { keywordKorean -> + keywordMap[keywordKorean]?.keywordEnglish + } + + viewModel.updateProjectCategory(projectCategoryList = selectedEnglishItems) + } + }.showAllowingStateLoss(parentFragmentManager, "SelectCheckBoxCommonDialog") + } + } + + private fun selectStacks() { + val languageMap = KeywordList.languageList.associateBy { it.keywordKorean } + val techStackMap = KeywordList.techStackList.associateBy { it.keywordKorean } + val cooperationToolMap = KeywordList.cooperationList.associateBy { it.keywordKorean } + + binding.tvDevStackLanguage.setOnClickListener { + SelectCheckBoxCommonDialog + .newInstance( + title = getString(R.string.dialog_language_title), + description = getString(R.string.dialog_language_description), + confirmButtonText = getString(R.string.all_check), + items = languageMap.keys.toList() as ArrayList, + maxSelectionCount = 3, + ).apply { + setConfirmButtonClickListener { selectedItems -> + // 선택한 한글 키워드를 영어 키워드로 변환 + val selectedEnglishItems = + selectedItems.mapNotNull { keywordKorean -> + languageMap[keywordKorean]?.keywordEnglish + } + + viewModel.updateProjectLanguage(projectLanguageList = selectedEnglishItems) + } + }.showAllowingStateLoss(parentFragmentManager, "SelectCheckBoxCommonDialog") + } + binding.tvDevStackDevStack.setOnClickListener { + SelectCheckBoxCommonDialog + .newInstance( + title = getString(R.string.dialog_techstack_title), + description = getString(R.string.dialog_techstack_description), + confirmButtonText = getString(R.string.all_check), + items = techStackMap.keys.toList() as ArrayList, + maxSelectionCount = 3, + ).apply { + setConfirmButtonClickListener { selectedItems -> + // 선택한 한글 키워드를 영어 키워드로 변환 + val selectedEnglishItems = + selectedItems.mapNotNull { keywordKorean -> + techStackMap[keywordKorean]?.keywordEnglish + } + + viewModel.updateProjectTechStack(projectTechStackList = selectedEnglishItems) + } + }.showAllowingStateLoss(parentFragmentManager, "SelectCheckBoxCommonDialog") + } + binding.tvDevStackCooperation.setOnClickListener { + SelectCheckBoxCommonDialog + .newInstance( + title = getString(R.string.dialog_cooperation_title), + description = getString(R.string.dialog_cooperation_description), + confirmButtonText = getString(R.string.all_check), + items = cooperationToolMap.keys.toList() as ArrayList, + maxSelectionCount = 3, + ).apply { + setConfirmButtonClickListener { selectedItems -> + // 선택한 한글 키워드를 영어 키워드로 변환 + val selectedEnglishItems = + selectedItems.mapNotNull { keywordKorean -> + cooperationToolMap[keywordKorean]?.keywordEnglish + } + + viewModel.updateProjectCooperation(projectCooperationList = selectedEnglishItems) + } + }.showAllowingStateLoss(parentFragmentManager, "SelectCheckBoxCommonDialog") + } + } + + private fun clickSubmitButton() { + binding.btnNext.setOnClickListener { + viewModel.updateProjectInfo( + projectName = binding.etProjectName.text.toString(), + projectGithub = binding.etGithub.text.toString(), + projectShortIntro = binding.etProjectIntroContentShort.text.toString(), + projectLongIntro = binding.etProjectIntroContentLong.text.toString(), + projectWebLink = binding.etProjectWebLink.text.toString(), + projectAppLink = binding.etProjectAppLink.text.toString(), + projectLink = binding.etProjectInfoLink.text.toString(), + ) + (activity as? SubmitProjectActivity)?.setCurrentItem(1) + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectWithDevFragment.kt b/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectWithDevFragment.kt new file mode 100644 index 0000000..4ba4f76 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/submit/project/fragment/SubmitProjectWithDevFragment.kt @@ -0,0 +1,88 @@ +package com.zucchini.submit.project.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import com.zucchini.common.NavigationProvider +import com.zucchini.feature.projects.R +import com.zucchini.feature.projects.databinding.FragmentSubmitProjectWithDevBinding +import com.zucchini.submit.project.SubmitProjectViewModel +import com.zucchini.submit.project.adapter.FindDevAdapter +import com.zucchini.view.hideKeyboard +import com.zucchini.view.showShortToast +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@AndroidEntryPoint +class SubmitProjectWithDevFragment : Fragment() { + private var _binding: FragmentSubmitProjectWithDevBinding? = null + private val binding: FragmentSubmitProjectWithDevBinding get() = _binding!! + + private val viewModel: SubmitProjectViewModel by activityViewModels() + private lateinit var devAdapter: FindDevAdapter + + @Inject + lateinit var navigationProvider: NavigationProvider + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentSubmitProjectWithDevBinding.inflate(inflater, container, false) + + initDevInfoAdapter() + clickSearchKeyword() + clickSubmitButton() + hideKeyboardClickListener() + + return binding.root + } + + private fun hideKeyboardClickListener() { + binding.root.setOnClickListener { + hideKeyboard() + } + } + + private fun initDevInfoAdapter() { + devAdapter = FindDevAdapter(viewModel) + binding.rvFindDev.adapter = devAdapter + binding.rvFindDev.layoutManager = GridLayoutManager(requireContext(), 2) + } + + private fun clickSearchKeyword() { + binding.ivSearch.setOnClickListener { + viewModel.searchDeveloperList(binding.etSearchbar.text.toString()) + } + + viewModel.searchDeveloperResultList.flowWithLifecycle(lifecycle).onEach { developerList -> + devAdapter.submitList(developerList) + }.launchIn(lifecycleScope) + } + + private fun clickSubmitButton() { + binding.btnNext.setOnClickListener { + viewModel.submitProject() + viewModel.isSuccessSubmitProject.flowWithLifecycle(lifecycle).onEach { isSuccess -> + if (isSuccess) { + showShortToast(getString(R.string.success_sumbit_project)) + navigationProvider.toMain().also { + startActivity(it) + requireActivity().finish() + } + } else { + showShortToast(getString(R.string.fail_submit_project)) + } + }.launchIn(lifecycleScope) + } + } +} diff --git a/feature/projects/src/main/res/layout/activity_submit_dev.xml b/feature/projects/src/main/res/layout/activity_submit_dev.xml index 7a950f5..8bcbcfe 100644 --- a/feature/projects/src/main/res/layout/activity_submit_dev.xml +++ b/feature/projects/src/main/res/layout/activity_submit_dev.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.zucchini.submit.SubmitDevActivity"> + tools:context="com.zucchini.submit.developer.SubmitDevActivity"> + + + + + + + + + + + + + diff --git a/feature/projects/src/main/res/layout/fragment_submit_project.xml b/feature/projects/src/main/res/layout/fragment_submit_project.xml new file mode 100644 index 0000000..a4108b1 --- /dev/null +++ b/feature/projects/src/main/res/layout/fragment_submit_project.xml @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/projects/src/main/res/layout/fragment_submit_project_with_dev.xml b/feature/projects/src/main/res/layout/fragment_submit_project_with_dev.xml new file mode 100644 index 0000000..bfdcf2a --- /dev/null +++ b/feature/projects/src/main/res/layout/fragment_submit_project_with_dev.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/projects/src/main/res/layout/item_find_dev.xml b/feature/projects/src/main/res/layout/item_find_dev.xml new file mode 100644 index 0000000..3257164 --- /dev/null +++ b/feature/projects/src/main/res/layout/item_find_dev.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/projects/src/main/res/layout/item_projects.xml b/feature/projects/src/main/res/layout/item_projects.xml index db474fa..e3f709d 100644 --- a/feature/projects/src/main/res/layout/item_projects.xml +++ b/feature/projects/src/main/res/layout/item_projects.xml @@ -11,7 +11,6 @@ android:id="@+id/iv_project_profile" android:layout_width="60dp" android:layout_height="wrap_content" - android:layout_marginVertical="5dp" android:layout_marginStart="20dp" android:src="@drawable/project_profile_default" app:layout_constraintBottom_toBottomOf="parent" @@ -23,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginTop="20dp" + android:layout_marginTop="13dp" android:fontFamily="@font/pretendardextrabold" android:textColor="@color/olive_black" android:textSize="14sp" @@ -68,7 +67,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="20dp" - android:layout_marginBottom="15dp" + android:layout_marginBottom="13dp" android:fontFamily="@font/pretendardmedium" android:gravity="end" android:textColor="@color/gray2" diff --git a/feature/projects/src/main/res/layout/item_search_keyword.xml b/feature/projects/src/main/res/layout/item_search_keyword.xml index 86b80b3..7c8e769 100644 --- a/feature/projects/src/main/res/layout/item_search_keyword.xml +++ b/feature/projects/src/main/res/layout/item_search_keyword.xml @@ -16,7 +16,7 @@ android:minWidth="0dp" android:minHeight="0dp" android:paddingHorizontal="15dp" - android:paddingVertical="10dp" + android:paddingVertical="9dp" android:textColor="@color/olive_black" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/feature/projects/src/main/res/values/strings.xml b/feature/projects/src/main/res/values/strings.xml index 54678a4..1bc63aa 100644 --- a/feature/projects/src/main/res/values/strings.xml +++ b/feature/projects/src/main/res/values/strings.xml @@ -10,14 +10,24 @@ 개발자 찾기 개발자 상세 페이지 프로젝트 상세 페이지 + 프로젝트 등록하기 Github + 프로젝트 Github 주소 카카오톡 아이디 이메일 프로젝트 보러가기↗ 개발자 정보 프로젝트 소개 + 프로젝트 소개를 입력해주세요 프로젝트 소개 관련 링크 + 사용가능한 웹 링크를 입력해주세요 + 앱을 다운받을 수 있는 곳의 링크를 입력해주세요 + 프로젝트 소개 페이지 링크를 입력해주세요 + 웹 링크 + 앱 다운로드 링크 + 소개 페이지 링크 + 함께한 개발자를 정보를 입력해주세요! 개발자 정보 화면 터치 시 슈플렉터를 시작할 수 있어요! 내 정보 @@ -100,5 +110,38 @@ Loading... 브랜딩 생성에 실패했어요. 다시 시도해주세요. 복사하기 + 프로젝트의 깃허브 링크를 입력해주세요 + 등록할 프로젝트 이름을 입력해주세요. + 기술 스택을 선택해주세요 + 프로젝트의 카테고리를 선택해주세요 + 언어 + 프로젝트에 대한 상세 설명을 작성해주세요. (500자 제한) + 프로젝트 한줄 소개를 작성해주세요. + 개발자 정보 입력하기↗ + 사용 언어↗ + 기술 스택↗ + 협업툴↗ + 카테고리↗ + 이미지 업로드 + 이미지를 선택해주세요. + 탈퇴 + 취소 + 확인 + 슈플렉터를 로그아웃 하시겠어요? + 정말 슈플렉터를 탈퇴 하시겠어요? + 회원탈퇴 후 유저 정보는 30일 동안 임시 보관되며, 이후 영구 삭제됩니다. + 프로젝트를 함께한 개발자의 이름을 검색해서 찾아보세요! + 슈플렉터에 등록되어 있는 개발자라면, 프로젝트를 함께 진행한 개발자로 등록할 수 있어요. + 개발자 이름을 검색해주세요 + 사용 언어 + 프로젝트에서 사용한 언어를 선택해주세요.\n(최대 3개) + 사용 기술 스택 + 프로젝트에서 사용한 기술 스택을 선택해주세요.\n(최대 3개) + 사용 협업툴 + 프로젝트에서 사용한 협업툴을 선택해주세요.\n(최대 3개) + 함께한 개발자 추가하러 가기 + 다음 + 프로젝트 등록에 실패했습니다. 입력 값을 다시 확인해주세요. + 프로젝트가 등록습니다. \ No newline at end of file