Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
112b7e8
#18[feat] : 라이브러리 플러그인 gson 추가
fredleeJH Jan 3, 2026
d72fa2a
#18[feat] : 의존성 추가
fredleeJH Jan 3, 2026
295ce05
#18[feat] : 스터디 만들기 응답 Dto 추가
fredleeJH Jan 3, 2026
d2ed962
#18[feat] : 이미지 압축 유틸 함수 추가
fredleeJH Jan 3, 2026
4b6d9b3
#18[feat] : 스터디 스타일 enum class 추가
fredleeJH Jan 3, 2026
5ff4837
#18[feat] : 이미지용 gson 확장함수 추가
fredleeJH Jan 3, 2026
80bd5c9
#18[feat] : 체크 아이콘 추가
fredleeJH Jan 3, 2026
4223500
#18[feat] : Detail, Register 그래프 추가
fredleeJH Jan 3, 2026
8cbe110
#18[feat] : Detail nav 추가
fredleeJH Jan 3, 2026
f6c5c88
#18[feat] : Detail nav 추가
fredleeJH Jan 3, 2026
40db5ad
#18[feat] : 스터디 등록하기 화면 api 연동
fredleeJH Jan 3, 2026
beb444e
#18[feat] : 스터디 화면 공통 다이얼로그 컴포넌트 추가
fredleeJH Jan 3, 2026
3adc947
#18[feat] : 스팟 공용 버튼 기본 폰트 설정 추가
fredleeJH Jan 3, 2026
8d8160c
#18[feat] : 스터디 등록하기 화면 api 연동
fredleeJH Jan 3, 2026
e28c4fe
#18[feat] : 스터디 디테일 화면 추가
fredleeJH Jan 3, 2026
0966055
#18[feat] : 버전 수정
fredleeJH Jan 3, 2026
82d077d
#18[feat] : 라이브러리 수정
fredleeJH Jan 4, 2026
c4e864f
#18[feat] : Mapper 수정
fredleeJH Jan 4, 2026
00563f7
#18[feat] : RepositoryImpl 수정
fredleeJH Jan 4, 2026
fb32281
#18[feat] : 네비게이션 수정
fredleeJH Jan 4, 2026
5b6ece0
#18[feat] : 스낵바 호스트 추가
fredleeJH Jan 4, 2026
61049d2
#18[feat] : 네비게이션 수정
fredleeJH Jan 4, 2026
b950331
#18[feat] : 불필요한 변수 제거
fredleeJH Jan 4, 2026
b4ed2c5
Merge branch 'develop' into feat/#18-study-register
fredleeJH Jan 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class AndroidLibraryPlugin : Plugin<Project> {

dependencies {
implementation(libs.getLibrary("timber"))
implementation(libs.getLibrary("gson"))
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions core/common/src/main/java/com/umcspot/spot/common/util/FileUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.umcspot.spot.common.util

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream

object FileUtil {
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
val inputStream = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()

if (bitmap == null) return null

val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)

FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}

bitmap.recycle()

file
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
Comment on lines +12 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

리소스 누수 위험: InputStream이 예외 발생 시 닫히지 않을 수 있습니다.

Line 14에서 열린 inputStream이 Line 15의 decodeStream에서 예외가 발생하면 Line 16의 close()가 실행되지 않아 리소스 누수가 발생합니다. use() 블록을 사용하여 예외 발생 여부와 관계없이 스트림이 닫히도록 보장해야 합니다.

🔎 리소스 관리 개선 제안
 fun createTempFileFromUri(context: Context, uri: Uri): File? {
     return runCatching {
-        val inputStream = context.contentResolver.openInputStream(uri)
-        val bitmap = BitmapFactory.decodeStream(inputStream)
-        inputStream?.close()
-
-        if (bitmap == null) return null
+        context.contentResolver.openInputStream(uri)?.use { inputStream ->
+            val bitmap = BitmapFactory.decodeStream(inputStream)
+                ?: return null
 
-        val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
+            val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
 
-        FileOutputStream(file).use { outputStream ->
-            bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
-        }
+            FileOutputStream(file).use { outputStream ->
+                bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+            }
 
-        bitmap.recycle()
+            bitmap.recycle()
 
-        file
+            file
+        }
     }.onFailure { e ->
         Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
     }.getOrNull()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
val inputStream = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
if (bitmap == null) return null
val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}
bitmap.recycle()
file
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
?: return null
val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}
bitmap.recycle()
file
}
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
🤖 Prompt for AI Agents
In core/common/src/main/java/com/umcspot/spot/common/util/FileUtil.kt around
lines 12 to 32, the InputStream opened from contentResolver may not be closed if
BitmapFactory.decodeStream throws, causing a resource leak; wrap the InputStream
in a Kotlin use{} block (or apply try-with-resources equivalent) so it is closed
regardless of exceptions, move decodeStream and any downstream work that depends
on the stream inside that use block, handle a null bitmap after the use block
(or return early inside it) and keep the existing runCatching/onFailure behavior
for logging.

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.umcspot.spot.designsystem.theme.B500
import com.umcspot.spot.designsystem.theme.G300
Expand All @@ -26,7 +27,8 @@ fun SpotActivationButton(
buttonText: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = false
isEnabled: Boolean = false,
style: TextStyle = SpotTheme.typography.h5
) {
val borderColor = if (isEnabled) SpotTheme.colors.B500 else SpotTheme.colors.G300
val textColor = if (isEnabled) SpotTheme.colors.B500 else SpotTheme.colors.G400
Expand All @@ -50,7 +52,7 @@ fun SpotActivationButton(
) {
Text(
text = buttonText,
style = SpotTheme.typography.h3,
style = style,
color = textColor
)
}
Expand Down
9 changes: 9 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_check.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="18dp"
android:viewportWidth="25"
android:viewportHeight="18">
<path
android:pathData="M7.546,14.539L2.33,9.323C2.071,9.073 1.724,8.934 1.363,8.938C1.003,8.941 0.658,9.085 0.403,9.34C0.148,9.595 0.003,9.94 0,10.301C-0.003,10.661 0.136,11.008 0.386,11.268L6.573,17.455C6.831,17.713 7.181,17.858 7.546,17.858C7.91,17.858 8.26,17.713 8.518,17.455L23.643,2.33C23.893,2.071 24.032,1.724 24.029,1.363C24.025,1.003 23.881,0.658 23.626,0.403C23.371,0.148 23.026,0.003 22.666,0C22.305,-0.003 21.958,0.136 21.698,0.386L7.546,14.539Z"
android:fillColor="#005BFF"/>
</vector>
29 changes: 21 additions & 8 deletions core/model/src/main/java/com/umcspot/spot/model/Global.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ enum class WeatherType { HEAVYRAIN, RAIN, SNOW, WIND, COLD, HOT, SUNNY }

enum class SortType { RECENT, RECOMMEND, COMMENT_COUNT }

enum class PostType { PASS_EXPERIENCE, INFORMATION_SHARING, COUNSELING, JOB_TALK, FREE_TALK}
enum class PostType { PASS_EXPERIENCE, INFORMATION_SHARING, COUNSELING, JOB_TALK, FREE_TALK }

val PostType.korean: String
get() = when (this) {
PostType.PASS_EXPERIENCE -> "합격후기"
PostType.INFORMATION_SHARING -> "정보공유"
PostType.COUNSELING -> "고민상담"
PostType.JOB_TALK -> "취준토크"
PostType.FREE_TALK -> "자유토크"
PostType.PASS_EXPERIENCE -> "합격후기"
PostType.INFORMATION_SHARING -> "정보공유"
PostType.COUNSELING -> "고민상담"
PostType.JOB_TALK -> "취준토크"
PostType.FREE_TALK -> "자유토크"
}

enum class RecruitingStudySort(val label: String) {
Expand All @@ -27,15 +27,15 @@ enum class RecruitingStudySort(val label: String) {
enum class AlertKind { POPULAR_POST, STUDY_NOTICE, STUDY_SCHEDULE, TODO_DONE }

enum class ActivityType(
val label : String
val label: String
) {
ONLINE("온라인"),
OFFLINE("오프라인")
}

enum class FeeRange(
val label: String
){
) {
NONE("없음"),
UNDER_10K("1만원 미만"),
ABOUT10K("1만원대"),
Expand All @@ -60,6 +60,19 @@ enum class StudyTheme(
OTHER("기타")
}

enum class StudyStyle {
NETWORKING,
GOAL_OR_RULE_ORIENTED,
SHORT_TERM,
LONG_TERM,
INDIVIDUAL_PLUS_DISCUSSION,
GROUP_PLUS_SIMULTANEOUS,
LEARNING_BASED,
DISCUSSION_BASED,
LIGHT_AND_FLEXIBLE,
STRUCTURED_AND_PLANNED
}

enum class SocialLoginType(
val title: String
) {
Expand Down
17 changes: 17 additions & 0 deletions core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.umcspot.spot.ui.extension

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import com.google.gson.Gson

fun Any.toRequestBody(): RequestBody {
val jsonString = Gson().toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
Comment on lines +9 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gson 인스턴스를 재사용하여 성능을 개선하세요.

매번 새로운 Gson() 인스턴스를 생성하면 불필요한 오버헤드가 발생합니다. Gson 인스턴스는 스레드 안전하므로 싱글톤으로 공유해야 합니다.

🔎 제안하는 수정사항
+private val gson = Gson()
+
 fun Any.toRequestBody(): RequestBody {
-    val jsonString = Gson().toJson(this)
+    val jsonString = gson.toJson(this)
     return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun Any.toRequestBody(): RequestBody {
val jsonString = Gson().toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
private val gson = Gson()
fun Any.toRequestBody(): RequestBody {
val jsonString = gson.toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
🤖 Prompt for AI Agents
In core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt around lines 9
to 12, the extension creates a new Gson() on every call which adds unnecessary
overhead; replace per-call instantiation with a shared singleton Gson instance
(declare a private top-level immutable Gson and use it in toRequestBody) so the
extension reuses the thread-safe Gson across calls.


fun Any.toMultipartBodyPart(name: String): MultipartBody.Part {
val requestBody = this.toRequestBody()
return MultipartBody.Part.createFormData(name, null, requestBody)
}
1 change: 1 addition & 0 deletions data/study/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ android {
}
dependencies {
implementation(projects.domain.study)
implementation(projects.core.ui)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ package com.umcspot.spot.study.datasource
import com.umcspot.spot.model.ActivityType
import com.umcspot.spot.model.FeeRange
import com.umcspot.spot.model.RecruitingStudySort
import com.umcspot.spot.model.SortType
import com.umcspot.spot.model.StudyTheme
import com.umcspot.spot.network.model.BaseResponse
import com.umcspot.spot.study.dto.request.StudyRequestDto
import com.umcspot.spot.study.dto.response.CreateStudyResponseDto
import com.umcspot.spot.study.dto.response.StudyResponseDto
import java.io.File

interface StudyDataSource {
suspend fun getPopularStudies(): BaseResponse<StudyResponseDto>
suspend fun getRecommendStudies(): BaseResponse<StudyResponseDto>
suspend fun getRecruitingStudies(sortType : RecruitingStudySort, activityType: ActivityType, theme: StudyTheme, feeRange: FeeRange): BaseResponse<StudyResponseDto>
suspend fun getRecruitingStudies(
sortType: RecruitingStudySort,
activityType: ActivityType,
theme: StudyTheme,
feeRange: FeeRange
): BaseResponse<StudyResponseDto>
suspend fun createStudy(request: StudyRequestDto, imageFile: File?): BaseResponse<CreateStudyResponseDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ package com.umcspot.spot.study.datasourceimpl
import com.umcspot.spot.model.ActivityType
import com.umcspot.spot.model.FeeRange
import com.umcspot.spot.model.RecruitingStudySort
import com.umcspot.spot.model.SortType
import com.umcspot.spot.model.StudyTheme
import com.umcspot.spot.network.model.BaseResponse
import com.umcspot.spot.study.datasource.StudyDataSource
import com.umcspot.spot.study.dto.request.StudyRequestDto
import com.umcspot.spot.study.dto.response.CreateStudyResponseDto
import com.umcspot.spot.study.dto.response.StudyResponseDto
import com.umcspot.spot.study.service.StudyService
import com.umcspot.spot.ui.extension.toMultipartBodyPart
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import javax.inject.Inject

class StudyDataSourceImpl @Inject constructor(
Expand All @@ -22,7 +28,24 @@ class StudyDataSourceImpl @Inject constructor(
): BaseResponse<StudyResponseDto> =
studyService.getRecommendStudies()

override suspend fun getRecruitingStudies(sortType : RecruitingStudySort, activityType: ActivityType, theme: StudyTheme, feeRange: FeeRange) : BaseResponse<StudyResponseDto> =
override suspend fun getRecruitingStudies(
sortType: RecruitingStudySort,
activityType: ActivityType,
theme: StudyTheme,
feeRange: FeeRange
): BaseResponse<StudyResponseDto> =
studyService.getRecruitingStudies(sortType, activityType, theme, feeRange)

override suspend fun createStudy(
request: StudyRequestDto,
imageFile: File?
): BaseResponse<CreateStudyResponseDto> {
val requestPart = request.toMultipartBodyPart("request")
val imagePart = imageFile?.let { file ->
val requestBody = file.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("imageFile", file.name, requestBody)
}
return studyService.createStudy(requestPart, imagePart)
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
package com.umcspot.spot.study.dto.request

import android.annotation.SuppressLint
import kotlinx.serialization.SerialName
import com.umcspot.spot.model.StudyStyle
import com.umcspot.spot.model.StudyTheme
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class StudyRequestDto(
@SerialName("studyId")
val studyId : Int
)
@SerialName("name")
val name: String,

@SerialName("maxMembers")
val maxMembers: Int,

@SerialName("hasFee")
val hasFee: Boolean,

@SerialName("amount")
val amount: Int,

@SerialName("description")
val description: String,

@SerialName("categories")
val categories: List<StudyTheme>,

@SerialName("styles")
val styles: List<StudyStyle>,

@SerialName("regionCodes")
val regionCodes: List<String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.umcspot.spot.study.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CreateStudyResponseDto(
@SerialName("studyId")
val studyId: Long
)
Comment on lines +6 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 프로젝트에서 kotlinx.serialization과 Gson 사용 패턴 확인

echo "=== kotlinx.serialization 사용 현황 ==="
rg -n --type=kotlin "@Serializable|@SerialName" | head -20

echo -e "\n=== Gson 사용 현황 ==="
rg -n --type=kotlin "@SerializedName|import com.google.gson" | head -20

echo -e "\n=== Gradle 의존성 확인 ==="
fd -e gradle -e kts -x grep -l "serialization\|gson" {}

echo -e "\n=== 전체 DTO 파일 현황 ==="
fd "Dto.kt$" | head -15

Repository: SPOTeam/Android-V2

Length of output: 4749


🏁 Script executed:

#!/bin/bash
# Gradle 의존성 파일 내용 확인
fd -e gradle -e kts | xargs grep -l "serialization\|gson" | head -5 | xargs cat

Repository: SPOTeam/Android-V2

Length of output: 1827


🏁 Script executed:

cat data/study/build.gradle.kts

Repository: SPOTeam/Android-V2

Length of output: 257


🏁 Script executed:

# 다른 DTO들의 직렬화 어노테이션 사용 패턴 확인
rg -n "@Serializable|@SerializedName" data/ -A 2 -B 2 | head -50

Repository: SPOTeam/Android-V2

Length of output: 4482


CreateStudyResponseDto는 올바른 직렬화 라이브러리를 사용합니다. 대신 같은 모듈의 StudyRequestDto와 일관성을 맞추세요.

이 파일은 프로젝트 전역 표준인 kotlinx.serialization을 올바르게 사용하고 있습니다. 그러나 같은 data/study 모듈에 속한 StudyRequestDto.kt는 Gson의 @SerializedName을 사용하고 있어 모듈 내 불일치가 발생합니다. 프로젝트는 네비게이션, 코어/네트워크, 다른 데이터 모듈 전반에서 kotlinx.serialization을 일관되게 사용하므로, StudyRequestDto.kt도 같은 라이브러리로 통일해야 합니다.

🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/dto/response/CreateStudyResponseDto.kt
around lines 6-10: the module mixes kotlinx.serialization (here) with Gson
annotations used in StudyRequestDto.kt, causing inconsistency; update
StudyRequestDto.kt to use kotlinx.serialization to match the module/project
standard by annotating the class with @Serializable, replacing any Gson
@SerializedName usages with @SerialName, importing kotlinx.serialization
annotations, and ensuring the module has the kotlinx.serialization
plugin/dependency configured; adjust any serialization-specific types or default
values to be compatible with kotlinx.serialization and run a build to verify no
remaining Gson references.

Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@ package com.umcspot.spot.study.mapper
import com.umcspot.spot.study.dto.request.StudyRequestDto
import com.umcspot.spot.study.dto.response.Study as DTOStudy
import com.umcspot.spot.study.dto.response.StudyResponseDto
import com.umcspot.spot.study.model.Study as DomainStudy
import com.umcspot.spot.study.model.StudyCreateModel
import com.umcspot.spot.study.model.StudyResult
import com.umcspot.spot.study.model.StudyResultList

// Domain -> Request DTO
fun DomainStudy.toData(): StudyRequestDto =
StudyRequestDto(
studyId = this.studyId
)
fun StudyCreateModel.toData(): StudyRequestDto = StudyRequestDto(
name = this.name,
maxMembers = this.maxMembers,
hasFee = this.hasFee,
amount = this.amount,
description = this.description,
categories = this.categories,
styles = this.styles,
regionCodes = this.regionCodes
)

fun DTOStudy.toDomain() : StudyResult =
StudyResult (
studyId = this.studyId,
title = this.title,
goal = this.goal,
maxMember = this.maxMember,
member = this.member,
likes = this.likes,
views = this.views,
studyImage = this.studyImage
)
fun DTOStudy.toDomain(): StudyResult = StudyResult(
studyId = this.studyId,
title = this.title,
goal = this.goal,
maxMember = this.maxMember,
member = this.member,
likes = this.likes,
views = this.views,
studyImage = this.studyImage
)

fun StudyResponseDto.toDomainList(): StudyResultList =
StudyResultList(
studyList = this.studyList.map(DTOStudy::toDomain)
)
fun StudyResponseDto.toDomain(): StudyResultList = StudyResultList(
studyList = this.studyList.map { it.toDomain() }
)
Loading