Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c0c9536
[add] 챌린지 정렬 기능 구현
cofls2027 Nov 19, 2025
d834c29
[feat] 챌린지 조회 기능 구현
cofls2027 Nov 26, 2025
9debabf
[feat] 챌린지 조회 기능 구현 2 - 파일 구조 수정
cofls2027 Nov 26, 2025
809e5fb
[fix] 챌린지 오류 수정을 위해 커밋
cofls2027 Nov 27, 2025
843c35e
[feat] 감사일기 post api 연결
cofls2027 Dec 23, 2025
3ac266e
[feat] 감사일기 삭제 기능 연결
cofls2027 Dec 31, 2025
b373acf
[feat] 감사일기 post api 연결결
cofls2027 Dec 31, 2025
33ae785
[feat] 챌린지 API 응답 구조 업데이트 및 페이징 로직 개선
cofls2027 Jan 5, 2026
1bc0a09
[feat] 챌린지 참여 API 연결결
cofls2027 Jan 12, 2026
02501d2
[feat] 챌린지 상세 조회 api 연결
cofls2027 Jan 12, 2026
ab9f426
[feat] 챌린지 검색 기능 연결
cofls2027 Jan 12, 2026
2d27eff
[feat] 챌린지 참여 취소 API 연결
cofls2027 Jan 12, 2026
00391f8
[fix] 감사일기 수정 모드 오류 수정
cofls2027 Jan 12, 2026
25a3742
[fix] 감사일기 GET API 수정 모드 수정
cofls2027 Jan 12, 2026
01adfff
[fix] 감사일기 PUT API 수정
cofls2027 Jan 12, 2026
9c0f08a
[fix] ChallengeFragment 파일 구조 수정
cofls2027 Jan 13, 2026
91a102f
[fix] ChallengeDetailFragment 수정
cofls2027 Jan 13, 2026
b0e769a
[fix] ChallengeAdapter 파일 수정정
cofls2027 Jan 13, 2026
c741284
[fix] DI 사용 코드로 수정정
cofls2027 Jan 13, 2026
a08769a
[fix] ChallengeViewModel 수정
cofls2027 Jan 13, 2026
52cb752
[fix] ChallengeFragment 파일 수정
cofls2027 Jan 13, 2026
9e9998f
[fix]build.gradle Data Binding 활성화
cofls2027 Jan 13, 2026
61fbe2c
Merge remote-tracking branch 'origin/develop' into chaerin
cofls2027 Jan 13, 2026
6bc3526
[fix] ChallengeItem 타입 불일치 오류 수정
cofls2027 Jan 20, 2026
d9360f6
[fix] ChallengeViewModel 코드 오류 수정
cofls2027 Jan 20, 2026
da29920
[fix] 불필요한 import문 삭제
cofls2027 Jan 20, 2026
c169903
[fix]loadChallenges() 오류 수정
cofls2027 Jan 20, 2026
34ffc54
[fix] ChallengeFragment.kt 파일 코드 오류 수정
cofls2027 Jan 20, 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
6 changes: 6 additions & 0 deletions .idea/studiobot.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ android {
buildFeatures {
buildConfig true
compose true
dataBinding true
}

defaultConfig {
Expand Down Expand Up @@ -77,9 +78,16 @@ dependencies {
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'

implementation 'androidx.paging:paging-runtime-ktx:3.2.1'// Paging 3
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'javax.inject:javax.inject:1'

// navigation
implementation("androidx.navigation:navigation-fragment-ktx:2.8.6")
implementation("androidx.navigation:navigation-ui-ktx:2.8.6")
implementation("androidx.navigation:navigation-safe-args-ktx:2.8.6")
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import com.example.lifemaster.data.remote.dto.ChallengeListResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface ChallengeApiService {

@GET("challenge")
suspend fun getChallengeList(
@Query("page") page: Int,
@Query("size") size: Int = 20 // 페이지 크기는 20으로 고정하거나 필요시 파라미터로 받기
): ChallengeListResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.example.lifemaster.data.remote.dto

import com.google.gson.annotations.SerializedName

/**
* 챌린지 목록 API(/challenge)의 전체 응답을 담는 데이터 클래스(DTO)
*/
data class ChallengeListResponse(
@SerializedName("content")
val content: List<ChallengeItemDto>,
@SerializedName("pageable")
val pageable: PageableDto? = null,
@SerializedName("last")
val last: Boolean,
@SerializedName("totalPages")
val totalPages: Int,
@SerializedName("totalElements")
val totalElements: Int,
@SerializedName("size")
val size: Int,
@SerializedName("number")
val number: Int,
@SerializedName("sort")
val sort: SortDto? = null,
@SerializedName("first")
val first: Boolean,
@SerializedName("numberOfElements")
val numberOfElements: Int,
@SerializedName("empty")
val empty: Boolean
)

/**
* ChallengeListResponse의 content 배열에 포함된 개별 챌린지 아이템 DTO
*/
data class ChallengeItemDto(
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("updatedAt")
val updatedAt: String? = null,
@SerializedName("challId")
val challId: Long,
@SerializedName("challName")
val challName: String,
@SerializedName("challDesc")
val challDesc: String,
@SerializedName("challImg")
val challImg: String,
@SerializedName("user")
val user: ChallengeUserDto? = null,
@SerializedName("challCnt")
val challCnt: Int,
@SerializedName("challMe")
val challMe: Boolean? = null // 검색 결과에만 포함되는 필드 (내가 참여한 챌린지 여부)
)

/**
* 챌린지 생성자 정보를 담는 DTO
*/
data class ChallengeUserDto(
@SerializedName("id")
val id: Long? = null,
@SerializedName("email")
val email: String? = null,
@SerializedName("nickname")
val nickname: String? = null,
@SerializedName("imageUrl")
val imageUrl: String? = null
)

/**
* 페이징 정보를 담는 DTO
*/
data class PageableDto(
@SerializedName("pageNumber")
val pageNumber: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("sort")
val sort: SortDto? = null,
@SerializedName("offset")
val offset: Int,
@SerializedName("paged")
val paged: Boolean,
@SerializedName("unpaged")
val unpaged: Boolean
)

/**
* 정렬 정보를 담는 DTO
*/
data class SortDto(
@SerializedName("sorted")
val sorted: Boolean,
@SerializedName("empty")
val empty: Boolean,
@SerializedName("unsorted")
val unsorted: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.lifemaster.data.repository.challenge

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.lifemaster.domain.model.ChallengeItem
import com.example.lifemaster.network.NetworkService
import retrofit2.HttpException
import java.io.IOException

/**
* 챌린지 목록 데이터를 서버에서 페이지 단위로 로드하는 PagingSource 구현체.
*/
class ChallengePagingSource(
private val apiService: NetworkService
) : PagingSource<Int, ChallengeItem>() {

private val STARTING_PAGE_INDEX = 0

override fun getRefreshKey(state: PagingState<Int, ChallengeItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ChallengeItem> {
val currentPage = params.key ?: STARTING_PAGE_INDEX

return try {
val response = apiService.getChallenges(
page = currentPage,
size = params.loadSize
)

// API 응답(DTO)을 앱에서 사용할 모델(Domain Model)로 변환합니다.
val challenges: List<ChallengeItem> = response.content.map { dto ->
ChallengeItem(
challId = dto.challId,
challName = dto.challName,
challTitle = dto.challDesc, // API의 challDesc를 UI의 challTitle로 사용
challImg = dto.challImg,
challJoinCnt = dto.challCnt // API의 challCnt를 UI의 challJoinCnt로 사용
)
}

// 다음 페이지 키 계산: API 응답의 last 필드를 사용하여 마지막 페이지 여부를 정확히 판단합니다.
val nextKey = if (response.last) null else currentPage + 1

LoadResult.Page(
data = challenges,
prevKey = if (currentPage == STARTING_PAGE_INDEX) null else currentPage - 1,
nextKey = nextKey
)
} catch (e: IOException) {
// 네트워크 연결 문제 (IO 예외) 처리
LoadResult.Error(e)
} catch (e: HttpException) {
// HTTP 오류 (4xx, 5xx 등) 처리
LoadResult.Error(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.lifemaster.data.repository.challenge

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.lifemaster.domain.model.ChallengeItem
import com.example.lifemaster.network.NetworkService
import kotlinx.coroutines.flow.Flow

class ChallengeRepository(private val apiService: NetworkService) {

fun getChallengePagingData(): Flow<PagingData<ChallengeItem>> {
return Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { ChallengePagingSource(apiService) }
).flow
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.lifemaster.domain.model

/**
* 앱의 비즈니스 로직(Domain) 또는 UI(Presentation)에서 사용할
* 챌린지 아이템을 나타내는 데이터 모델.
* PagingSource가 API 응답(DTO)을 이 클래스로 변환하여 전달합니다.
*/
data class ChallengeItem(
val challId: Long,
val challName: String,
val challTitle: String, // UI에 표시할 제목 (API의 challDesc를 여기에 매핑)
val challImg: String,
val challJoinCnt: Int, // UI에 표시할 참여자 수 (API의 challCnt를 여기에 매핑)
val createdAt: String? = null // 정렬을 위해 추가
)
69 changes: 63 additions & 6 deletions app/src/main/java/com/example/lifemaster/network/NetworkService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import com.example.lifemaster.presentation.home.sleep.model.SleepResponse
import com.example.lifemaster.presentation.home.sleep.model.SleepRequest
import com.example.lifemaster.presentation.login.model.LoginInfo
import com.example.lifemaster.presentation.home.todo.model.TodoItem
import com.example.lifemaster.presentation.total.challenge.model.ChallengeResponse
import com.example.lifemaster.data.remote.dto.ChallengeListResponse
import com.example.lifemaster.presentation.total.introspection.model.ThankRequest
import com.example.lifemaster.presentation.total.introspection.model.ThankResponse
import com.example.lifemaster.presentation.total.introspection.model.ThankCreateResponse
import com.example.lifemaster.presentation.total.introspection.model.ThankUpdateRequest
import com.example.lifemaster.presentation.total.introspection.model.DiaryRequest
import com.example.lifemaster.presentation.total.introspection.model.DiaryResponse
import com.example.lifemaster.presentation.login.model.NicknameCheckResponse
import com.example.lifemaster.presentation.login.model.RegisterInfo
import com.example.lifemaster.presentation.login.model.RegResponse
Expand Down Expand Up @@ -123,23 +127,53 @@ interface NetworkService {

// 챌린지 목록 조회
@GET("/challenge")
fun getChallenges(
@Query("page") page: Int
): Call<ChallengeResponse>
suspend fun getChallenges(
@Query("page") page: Int,
@Query("size") size: Int
): Response<ChallengeListResponse>

// 챌린지 참여
@POST("/challenge/{challId}/join")
suspend fun joinChallenge(
@Header("Authorization") token: String,
@Path("challId") challId: Long
): Response<String>

// 챌린지 참여 취소
@DELETE("/challenge/{challId}/leave")
suspend fun leaveChallenge(
@Header("Authorization") token: String,
@Path("challId") challId: Long
): Response<String>

// 챌린지 상세 조회
@GET("/challenge/{challId}")
suspend fun getChallengeDetail(
@Header("Authorization") token: String,
@Path("challId") challId: Long
): Response<com.example.lifemaster.data.remote.dto.ChallengeItemDto>

// 챌린지 검색
@GET("/challenge/search")
suspend fun searchChallenges(
@Header("Authorization") token: String,
@Query("name") name: String,
@Query("page") page: Int = 0
): com.example.lifemaster.data.remote.dto.ChallengeListResponse

// 감사일기 생성
@POST("/schedule/self-reflection/thank")
suspend fun createThank(
@Header("Authorization") token: String,
@Body request: ThankRequest
): Response<Unit>
): Response<ThankCreateResponse>

//감사일기 수정
@PUT("/schedule/self-reflection/thank/{thank-id}")
suspend fun updateThank(
@Header("Authorization") token: String,
@Path("thank-id") thankId: Long,
@Body request: ThankRequest
@Body request: ThankUpdateRequest
): Response<Unit>

// 감사일기 조회
Expand All @@ -156,6 +190,29 @@ interface NetworkService {
@Path("thank-id") thankId: Long
): Response<Unit>

// 다이어리 생성
@POST("/schedule/self-reflection/diary")
suspend fun createDiary(
@Header("Authorization") token: String,
@Body request: DiaryRequest
): Response<DiaryResponse>

// 다이어리 수정
@PUT("/schedule/self-reflection/diary/{diary-id}")
suspend fun updateDiary(
@Header("Authorization") token: String,
@Path("diary-id") diaryId: Long,
@Body request: DiaryRequest
): Response<DiaryResponse>

// 다이어리 삭제
@DELETE("/schedule/self-reflection/diary/{diary-id}")
suspend fun deleteDiary(
@Header("Authorization") token: String,
@Path("diary-id") diaryId: Long
): Response<Unit>


// 커뮤니티 게시글 전체 목록 조회
@GET("posts")
fun getPostsByType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ object RetrofitInstance {

private const val BASE_URL = "https://api.lifemaster.harvester.kr/"

val networkService: NetworkService by lazy {
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NetworkService::class.java)
}

val networkService: NetworkService by lazy {
retrofit.create(NetworkService::class.java)
}
}
Loading