diff --git a/app/src/main/java/com/egobook/app/data/api/LetterApiService.kt b/app/src/main/java/com/egobook/app/data/api/LetterApiService.kt index aa058c2d..b1ae7092 100644 --- a/app/src/main/java/com/egobook/app/data/api/LetterApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/LetterApiService.kt @@ -2,9 +2,11 @@ package com.egobook.app.data.api import com.egobook.app.data.model.ApiResponse import com.egobook.app.data.model.square.letter.ArrivedPendingLetterResponse +import com.egobook.app.data.model.square.letter.DeferredLettersResponse +import com.egobook.app.data.model.square.letter.ReceivedRepliesResponse import com.egobook.app.data.model.square.letter.ReplyLetterRequest import com.egobook.app.data.model.square.letter.ReplyLetterResponse -import com.egobook.app.data.model.square.letter.ReportLetterRequest +import com.egobook.app.data.model.square.letter.ReportContentRequest import com.egobook.app.data.model.square.letter.SendLetterRequest import com.egobook.app.data.model.square.letter.SendLetterResponse import com.egobook.app.data.model.square.letter.SentLetterResponse @@ -53,11 +55,29 @@ interface LetterApiService { @POST("/plaza/letters/{replyId}/report") suspend fun reportRepliedLetter( @Path("replyId") replyId: Long, - @Body request: ReportLetterRequest + @Body request: ReportContentRequest + ): ApiResponse + + @POST("/plaza/letters/{letterId}/report") + suspend fun reportArrivedLetter( + @Path("letterId") letterId: Long, + @Body request: ReportContentRequest ): ApiResponse @DELETE("/plaza/letters/threads/{threadId}") suspend fun deleteLetterThread( @Path("threadId") threadId: Long ): ApiResponse + + @GET("/plaza/letters/inbox/deferred") + suspend fun fetchDeferredLetters( + @Query("page") page: Int, + @Query("size") size: Int + ): ApiResponse + + @GET("/plaza/letters/replies/received") + suspend fun fetchReceivedReplies( + @Query("page") page: Int, + @Query("size") size: Int + ): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/api/QuestionApiService.kt b/app/src/main/java/com/egobook/app/data/api/QuestionApiService.kt index 8b1e03ac..5e9e2594 100644 --- a/app/src/main/java/com/egobook/app/data/api/QuestionApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/QuestionApiService.kt @@ -1,6 +1,7 @@ package com.egobook.app.data.api import com.egobook.app.data.model.ApiResponse +import com.egobook.app.data.model.square.letter.ReportContentRequest import com.egobook.app.data.model.square.question.UserTodayQuestionAnswerResponse import com.egobook.app.data.model.square.question.MyTodayQuestionAnswerResponse import com.egobook.app.data.model.square.question.TodayAnswerRequest @@ -48,4 +49,10 @@ interface QuestionApiService { @Path("answerId") answerId: Long ): ApiResponse + @POST("/questions/answers/{answerId}/report") + suspend fun reportTodayQuestionAnswer( + @Path("answerId") answerId: Long, + @Body request: ReportContentRequest + ): ApiResponse + } diff --git a/app/src/main/java/com/egobook/app/data/model/square/friend/FriendRequestResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/friend/FriendRequestResponse.kt index d064fbb2..3397fe78 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/friend/FriendRequestResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/friend/FriendRequestResponse.kt @@ -10,6 +10,8 @@ data class FriendRequestResponse( val userId: Long, @SerializedName("nickname") val nickname: String, + @SerializedName("level") + val level: Long, @SerializedName("requestedAt") val requestedAt: String ) @@ -18,5 +20,6 @@ fun FriendRequestResponse.toDomain(): FriendRequest = FriendRequest( requestId = requestId, userId = userId, nickname = nickname, + level = level, requestedAt = requestedAt ) diff --git a/app/src/main/java/com/egobook/app/data/model/square/friend/SearchUserResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/friend/SearchUserResponse.kt index 36dfafd2..f5da2dac 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/friend/SearchUserResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/friend/SearchUserResponse.kt @@ -11,7 +11,7 @@ data class SearchUserResponse( @SerializedName("level") val level: Long, @SerializedName("profileImageUrl") - val profileImageUrl: String + val profileImageUrl: String? = null ) fun SearchUserResponse.toDomain(): SearchUser = SearchUser( diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/ArrivedPendingLetterResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/ArrivedPendingLetterResponse.kt index 59cb9973..ba02e03c 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/letter/ArrivedPendingLetterResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/ArrivedPendingLetterResponse.kt @@ -20,15 +20,15 @@ data class ArrivedPendingLetterItemResponse( @SerializedName("mode") val mode: LetterMode, @SerializedName("fromLabel") - val fromLabel: String, // 무슨 필드인지 의미 잘 모르겠음 + val fromLabel: String, @SerializedName("content") val content: String, @SerializedName("arrivedAt") val arrivedAt: String, @SerializedName("replyDeadlineAt") val replyDeadlineAt: String, - @SerializedName("letterColor") - val letterColor: LetterBackgroundColor // TODO: 백엔드한테 필드 넣어달라고 하기 + @SerializedName("backgroundColor") + val backgroundColor: LetterBackgroundColor ) fun ArrivedPendingLetterResponse.toDomain(): ArrivedPendingLetter = ArrivedPendingLetter( @@ -43,5 +43,5 @@ fun ArrivedPendingLetterItemResponse.toDomain(): ArrivedPendingLetterItem = Arri content = content, arrivedAt = arrivedAt, replyDeadlineAt = replyDeadlineAt, - letterColor = letterColor + backgroundColor = backgroundColor ) diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/DeferredLettersResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/DeferredLettersResponse.kt new file mode 100644 index 00000000..42376e7d --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/DeferredLettersResponse.kt @@ -0,0 +1,48 @@ +package com.egobook.app.data.model.square.letter + +import com.egobook.app.domain.model.square.letter.DeferredLetter +import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode +import com.egobook.app.domain.model.square.letter.LetterStatus +import com.google.gson.annotations.SerializedName + +data class DeferredLettersResponse( + @SerializedName("content") + val content: List, + @SerializedName("page") + val page: Int, + @SerializedName("size") + val size: Int, + @SerializedName("hasNext") + val hasNext: Boolean +) + +data class DeferredLetterResponse( + @SerializedName("letterId") + val letterId: Long, + @SerializedName("status") + val status: LetterStatus, + @SerializedName("mode") + val mode: LetterMode, + @SerializedName("fromLabel") + val fromLabel: String, + @SerializedName("backgroundColor") + val backgroundColor: LetterBackgroundColor, + @SerializedName("contentPreview") + val contentPreview: String, + @SerializedName("arrivedAt") + val arrivedAt: String, + @SerializedName("replyDeadlineAt") + val replyDeadlineAt: String +) + +fun DeferredLetterResponse.toDomain(): DeferredLetter = DeferredLetter( + letterId = letterId, + status = status, + mode = mode, + fromLabel = fromLabel, + backgroundColor = backgroundColor, + contentPreview = contentPreview, + arrivedAt = arrivedAt, + replyDeadlineAt = replyDeadlineAt +) diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/DetectAbusiveContentResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/DetectAbusiveContentResponse.kt index 46d0ed28..25136105 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/letter/DetectAbusiveContentResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/DetectAbusiveContentResponse.kt @@ -7,7 +7,7 @@ data class DetectAbusiveContentResponse( @SerializedName("text") val text: String, @SerializedName("percentage") - val percentage: Double, + val riskScore: Double, @SerializedName("is_harmful") val isHarmful: Boolean, @SerializedName("label") @@ -18,7 +18,7 @@ data class DetectAbusiveContentResponse( fun DetectAbusiveContentResponse.toDomain(): AbusiveContentAnalysis = AbusiveContentAnalysis( text = text, - riskScore = percentage, + riskScore = riskScore, isHarmful = isHarmful, label = label, detectedBadWords = badWords diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/ReceivedRepliesResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/ReceivedRepliesResponse.kt new file mode 100644 index 00000000..3cb2ada9 --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/ReceivedRepliesResponse.kt @@ -0,0 +1,61 @@ +package com.egobook.app.data.model.square.letter + +import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode +import com.egobook.app.domain.model.square.letter.ReceivedReplies +import com.egobook.app.domain.model.square.letter.ReceivedReply +import com.google.gson.annotations.SerializedName + +data class ReceivedRepliesResponse( + @SerializedName("content") + val content: List, + @SerializedName("page") + val page: Int, + @SerializedName("size") + val size: Int, + @SerializedName("hasNext") + val hasNext: Boolean +) + +data class ReceivedReplyResponse( + @SerializedName("letterId") + val letterId: Long, + @SerializedName("replyId") + val replyId: Long, + @SerializedName("threadId") + val threadId: Long, + @SerializedName("replyText") + val replyContent: String, + @SerializedName("repliedAt") + val repliedAt: String, + @SerializedName("aiGenerated") + val isAIGenerated: Boolean, + @SerializedName("reported") + val isReported: Boolean, + @SerializedName("mode") + val mode: LetterMode, + @SerializedName("fromLabel") + val fromLabel: String, + @SerializedName("backgroundColor") + val letterColor: LetterBackgroundColor +) + +fun ReceivedRepliesResponse.toDomain() = ReceivedReplies( + content = content.map { it.toDomain() }, + page = page, + size = size, + hasNext = hasNext +) + +fun ReceivedReplyResponse.toDomain(): ReceivedReply = ReceivedReply( + letterId = letterId, + replyId = replyId, + threadId = threadId, + replyContent = replyContent, + repliedAt = repliedAt, + isAIGenerated = isAIGenerated, + isReported = isReported, + mode = mode, + fromLabel = fromLabel, + letterColor = letterColor +) diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/ReportLetterRequest.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/ReportContentRequest.kt similarity index 67% rename from app/src/main/java/com/egobook/app/data/model/square/letter/ReportLetterRequest.kt rename to app/src/main/java/com/egobook/app/data/model/square/letter/ReportContentRequest.kt index 99b7f11b..141e1eca 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/letter/ReportLetterRequest.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/ReportContentRequest.kt @@ -1,17 +1,17 @@ package com.egobook.app.data.model.square.letter -import com.egobook.app.domain.model.square.letter.ReportLetter +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.letter.ReportLetterType import com.google.gson.annotations.SerializedName -data class ReportLetterRequest( +data class ReportContentRequest( @SerializedName("reason") val reason: ReportLetterType, @SerializedName("description") val description: String? = null ) -fun ReportLetter.toData(): ReportLetterRequest = ReportLetterRequest( +fun ReportContent.toData(): ReportContentRequest = ReportContentRequest( reason = reason, description = description ) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/square/letter/SentLetterWithReplyResponse.kt b/app/src/main/java/com/egobook/app/data/model/square/letter/SentLetterWithReplyResponse.kt index a322330b..cc62cf4a 100644 --- a/app/src/main/java/com/egobook/app/data/model/square/letter/SentLetterWithReplyResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/square/letter/SentLetterWithReplyResponse.kt @@ -24,6 +24,8 @@ data class SentLetterWithReplyResponse( val createdAt: String, @SerializedName("arrivedAt") val arrivedAt: String, + @SerializedName("fromLabel") + val fromLabel: String, @SerializedName("reply") val reply: LetterReplyResponse? = null ) @@ -58,5 +60,6 @@ fun SentLetterWithReplyResponse.toDomain(): SentLetterWithReply = SentLetterWith backgroundColor = backgroundColor, createdAt = createdAt, arrivedAt = arrivedAt, + fromLabel = fromLabel, reply = reply?.toDomain() ) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/CounselingRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/CounselingRepositoryImpl.kt index f4e252c6..4e2383a5 100644 --- a/app/src/main/java/com/egobook/app/data/repository/CounselingRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/CounselingRepositoryImpl.kt @@ -17,6 +17,8 @@ import com.egobook.app.domain.model.ReportStyle import com.egobook.app.domain.model.Statistics import com.egobook.app.domain.model.TimeData import com.egobook.app.domain.model.WeeklyReportStyle +import com.egobook.app.domain.model.counseling.CounselingReward +import com.egobook.app.domain.model.counseling.CounselingRewardType import com.egobook.app.domain.model.counseling.DailyAndWeeklyNotification import com.egobook.app.domain.model.counseling.DailyPraise import com.egobook.app.domain.model.counseling.DailyPraiseDetail @@ -44,12 +46,147 @@ class CounselingRepositoryImpl @Inject constructor(private val apiService: Couns } override suspend fun getDailyPraiseByDate(date: String): Result = try { - val response = apiService.fetchDailyPraiseByDate(date = date) - if (response.isSuccessful && response.body() != null) { - Result.success(response.body()!!.toDomain()) - } else { - Result.failure(Exception("Error: ${response.code()}")) - } +// val response = apiService.fetchDailyPraiseByDate(date = date) +// if (response.isSuccessful && response.body() != null) { +// Result.success(response.body()!!.toDomain()) +// } else { +// Result.failure(Exception("Error: ${response.code()}")) +// } + val mockDetails = listOf( + DailyPraiseDetail( + diaryDate = "2026.02.03", + content = """ + 새로운 한 달의 시작을 차분하게 잘 열어가고 계시네요. + 당신이 가진 긍정적인 태도는 주변 사람들에게도 좋은 영향을 줍니다. + 오늘 마주한 작은 행복들을 소중히 간직하며 하루를 마무리하세요. + 내일도 당신의 앞날에 따뜻한 햇살이 가득하기를 진심으로 바랍니다. + 충분한 휴식과 함께 편안한 밤 되시길 응원하겠습니다. + """.trimIndent(), + createdAt = "2026-02-03T19:15:30", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.05", + content = """ + 월요일의 무게를 견디고 묵묵히 자신의 자리를 지킨 당신이 대견합니다. + 가끔은 지치기도 하겠지만, 당신은 생각보다 훨씬 더 강한 사람이에요. + 스스로에게 너무 엄격하기보다 '오늘도 잘했다'고 한마디 건네주세요. + 당신의 꾸준함이 모여 결국은 커다란 결실을 맺게 될 것임을 믿습니다. + 포근한 이불 속에서 오늘 하루의 긴장을 모두 녹여내시길 바라요. + """.trimIndent(), + createdAt = "2026-02-05T20:40:12", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.06", + content = """ + 어제보다 조금 더 성장한 오늘의 당신을 진심으로 칭찬합니다. + 사소해 보이는 일상 속에서도 당신만의 의미를 찾아내는 모습이 참 아름다워요. + 타인의 속도에 조급해하지 말고 지금처럼 당신만의 길을 걸어가 주세요. + 세상은 당신의 진심 어린 노력을 반드시 기억하고 보상해 줄 것입니다. + 오늘 밤은 걱정 없이 깊고 단잠을 자며 에너지를 충전하세요. + """.trimIndent(), + createdAt = "2026-02-06T18:55:45", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.09", + content = """ + 한 주의 마무리를 향해 달려가는 당신의 열정에 박수를 보냅니다. + 힘든 순간에도 미소를 잃지 않으려 노력하는 모습이 정말 인상적이에요. + 당신은 존재만으로도 충분히 가치 있고 빛나는 보석 같은 사람입니다. + 오늘 하루 고생한 자신을 위해 맛있는 음식이나 작은 선물을 주면 어떨까요? + 당신의 내일이 오늘보다 더 평온하고 행복하기를 항상 기도할게요. + """.trimIndent(), + createdAt = "2026-02-09T21:10:05", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.11", + content = """ + 오늘 하루 정말 고생 많으셨어요. + 비록 작은 실수들이 있었을지라도 그것은 성장을 위한 과정일 뿐이에요. + 당신이 보여준 인내와 노력은 결코 헛되지 않았으며, + 내일은 오늘보다 조금 더 밝은 미소를 지을 수 있을 거예요. + 스스로를 조금 더 믿고 편안하게 휴식을 취하시길 바랍니다. + """.trimIndent(), + createdAt = "2026-02-11T18:30:15", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.13", + content = """ + 누구보다 성실하게 오늘을 살아낸 당신이 자랑스럽습니다. + 복잡한 생각들은 잠시 내려놓고 마음의 소리에 귀를 기울여 보세요. + 당신은 충분히 사랑받을 자격이 있는 소중한 사람이며, + 주변 사람들에게 긍정적인 에너지를 주는 특별한 존재입니다. + 오늘 밤은 당신의 노력을 칭찬하며 깊은 잠에 드시길 응원해요. + """.trimIndent(), + createdAt = "2026-02-13T19:45:22", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.14", + content = """ + 어려운 상황 속에서도 포기하지 않고 묵묵히 나아가는 모습이 멋져요. + 타인의 기준에 맞추려 애쓰기보다 당신만의 속도를 존중해 주세요. + 가끔은 쉬어가는 것도 더 멀리 나아가기 위한 소중한 전략입니다. + 오늘 하루 당신이 뿌린 작은 씨앗들이 곧 예쁜 꽃을 피울 거예요. + 당신의 모든 걸음을 진심으로 지지하고 응원하고 있습니다. + """.trimIndent(), + createdAt = "2026-02-14T20:10:05", + isRead = true, + rewards = emptyList() + ), + DailyPraiseDetail( + diaryDate = "2026.02.16", + content = """ + 오늘의 당신은 어제보다 한 뼘 더 성장한 멋진 사람입니다. + 사소한 성취 하나에도 자신을 마음껏 칭찬해 주는 건 어떨까요? + 당신이 가진 따뜻한 마음씨는 세상을 조금 더 밝게 만듭니다. + 남들의 시선보다는 당신의 행복을 최우선으로 생각했으면 좋겠어요. + 수고한 당신에게 따뜻한 차 한 잔과 같은 평온함이 깃들길 바랍니다. + """.trimIndent(), + createdAt = "2026-02-16T17:55:40", + isRead = false, + rewards = listOf( + CounselingReward( + kind = CounselingRewardType.SELF_ESTEEM, + amount = 1, + toastMessage = "칭찬서가 확인하여\n자존감이 상승했어요" + ) + ) + ), + DailyPraiseDetail( + diaryDate = "2026.02.18", + content = """ + 마음이 무거운 날이었을지도 모르지만, 당신은 충분히 잘 해냈습니다. + 내일의 걱정은 내일에게 맡기고 지금 이 순간의 평안을 누려보세요. + 당신의 존재 자체만으로도 누군가에게는 큰 힘과 위로가 됩니다. + 스스로를 다독여주는 시간을 가지며 오늘 하루를 마무리해 보세요. + 당신의 빛나는 미래를 믿어 의심치 않으며 늘 곁에서 응원할게요. + """.trimIndent(), + createdAt = "2026-02-18T21:20:12", + isRead = false, + rewards = listOf( + CounselingReward( + kind = CounselingRewardType.SELF_ESTEEM, + amount = 1, + toastMessage = "칭찬서가 확인하여\n자존감이 상승했어요" + ) + ) + ) + ) + + // 2. 인자로 들어온 date와 일치하는 데이터 찾기 (없으면 첫 번째 데이터 반환) + val result = mockDetails.find { it.diaryDate == date } ?: mockDetails.first() + Result.success(result) } catch (e: Exception) { Result.failure(e) } @@ -106,12 +243,116 @@ class CounselingRepositoryImpl @Inject constructor(private val apiService: Couns override suspend fun getWeeklyReportByDate(startDate: String): Result = try { - val response = apiService.fetchWeeklyReportByDate(startDate = startDate) - if (response.isSuccessful && response.body() != null) { - Result.success(response.body()!!.toDomain()) - } else { - Result.failure(Exception("Error: ${response.code()}")) - } +// val response = apiService.fetchWeeklyReportByDate(startDate = startDate) +// if (response.isSuccessful && response.body() != null) { +// Result.success(response.body()!!.toDomain()) +// } else { +// Result.failure(Exception("Error: ${response.code()}")) +// } + val dummyDetail = listOf( + WeeklyReportDetail( + startDate = "2026.02.02", + endDate = "2026.02.08", + summary = """ + 이번 주 상담의 핵심 주제는 직무 몰입도 저하와 그로 인한 심리적 소진이었습니다. + 사용자는 업무 성과에 대한 과도한 압박감으로 인해 일상적인 즐거움을 잃어버린 상태였으며, + 상담을 통해 번아웃의 초기 증상을 객관적으로 점검하고 휴식의 필요성을 인지했습니다. + 자신을 업무 성과와 동일시하는 인지적 오류를 발견하고 이를 수정하는 연습을 진행했습니다. + 현재는 업무 시간 외에 철저히 분리된 자신만의 시간을 확보하는 것에 집중하고 있습니다. + 감정적인 소모가 심했던 한 주였지만, 문제의 원인을 명확히 규명했다는 점에서 의미가 큽니다. + 다음 단계로는 완벽주의적 성향을 완화하고 자기 연민을 실천하는 과정을 계획 중입니다. + """.trimIndent(), + praisePoints = """ + 자신의 한계를 인정하고 상담을 통해 도움을 요청한 용기 있는 태도를 높게 평가합니다. + 업무 압박 속에서도 하루 10분간의 정기적인 스트레칭을 실천하며 신체 감각을 깨웠습니다. + 부정적인 생각이 꼬리에 꼬리를 물 때 '멈춤' 신호를 스스로에게 보낼 수 있게 되었습니다. + 동료와의 갈등 상황에서 감정적으로 대응하지 않고 차분하게 자신의 의사를 전달했습니다. + 자신의 취약점을 숨기려 하기보다 솔직하게 대면하며 변화를 갈망하는 모습이 인상적입니다. + 과거에 비해 자신의 정서적 상태를 언어로 표현하는 능력이 눈에 띄게 정교해졌습니다. + 어려운 환경 속에서도 매일 아침 감사한 점 한 가지를 찾아낸 끈기를 칭찬하고 싶습니다. + """.trimIndent(), + improvementPoints = """ + 여전히 퇴근 후에도 업무 관련 연락을 확인하며 온전한 휴식을 방해하는 습관이 남아있습니다. + 스스로에게 부여하는 높은 기준이 때로는 독이 되어 자존감을 깎아내리고 있는 점이 우려됩니다. + 작은 실수에도 과도하게 자책하며 전체적인 성과를 부정하는 이분법적 사고를 경계해야 합니다. + 신체적인 피로가 누적되어 있음에도 불구하고 운동을 강박적으로 수행하려는 경향이 보입니다. + 자신의 감정을 억누르는 것이 익숙해져 정작 슬픔이나 화가 날 때 적절히 분출하지 못합니다. + 주변의 기대에 부응하려는 욕구 때문에 본인의 진정한 욕구를 뒷전으로 미루는 점이 보입니다. + 충분한 수면 시간을 확보하지 못해 정서적 회복 탄력성이 다소 낮아진 상태로 판단됩니다. + """.trimIndent(), + managementAdvice = """ + 이번 주는 디지털 디톡스를 통해 뇌에 충분한 휴식을 제공하는 시간을 반드시 가지세요. + 퇴근 후에는 업무용 메신저 알림을 끄고 물리적으로 스마트폰과 거리를 두는 연습이 필요합니다. + 점심시간을 활용해 15분간 야외에서 햇볕을 쬐며 걷는 활동은 세로토닌 분비에 큰 도움이 됩니다. + 자신을 비난하는 목소리가 들릴 때마다 '그럴 수 있어'라는 문장을 입 밖으로 소리 내어 말해보세요. + 복잡한 생각보다는 단순한 수작업이나 취미 활동을 통해 몰입의 즐거움을 다시 느껴보시기 바랍니다. + 주말 중 하루는 계획 없이 흐르는 대로 시간을 보내며 생산성에 대한 강박을 내려놓으세요. + 일기 작성 시 결과 중심이 아닌, 그날 느꼈던 사소한 기분들에 집중해 기록해 보길 권장합니다. + """.trimIndent(), + supportMessage = """ + 당신은 이미 충분히 많은 짐을 지고 달려왔으며, 이제는 잠시 짐을 내려놓아도 괜찮습니다. + 멈추는 것은 퇴보가 아니라 더 멀리 나아가기 위한 가장 현명한 전략이자 자기 사랑의 실천입니다. + 세상의 기준이 아닌 당신만의 속도로 걸어가는 모습 그 자체로 당신은 충분히 가치 있는 사람입니다. + 비바람이 치는 날이 있으면 해가 뜨는 날도 있듯이, 지금의 힘든 감정도 결국 지나갈 것입니다. + 스스로를 다독이는 법을 배워가는 당신의 뒷모습을 진심으로 응원하고 곁에서 지켜보겠습니다. + 내일은 오늘보다 조금 더 가벼운 마음으로 아침을 맞이할 수 있기를 간절히 기도합니다. + 당신은 혼자가 아니며, 우리는 이 과정을 함께 헤쳐 나갈 준비가 되어 있다는 것을 잊지 마세요. + """.trimIndent(), + isRead = false + ), + WeeklyReportDetail( + startDate = "2026.02.09", + endDate = "2026.02.15", + summary = """ + 이번 주 상담은 타인과의 관계에서 발생하는 불안감을 다스리고 자존감을 회복하는 데 주력했습니다. + 타인의 시선을 지나치게 의식하여 본인의 의사를 표현하지 못했던 상황들을 구체적으로 분석했습니다. + 상담을 통해 거절이 관계를 망치는 것이 아니라 건강하게 유지하는 수단임을 새롭게 깨달았습니다. + 일상 속에서 가벼운 '나 전달법(I-Message)'을 활용해 본인의 감정을 드러내는 실습을 병행했습니다. + 과거의 상처가 현재의 관계에 미치는 영향력을 인지하며 심리적 독립을 위한 초석을 다졌습니다. + 전반적으로 대인 관계에 대한 공포심이 줄어들고 자신감을 조금씩 회복해가는 긍정적인 추세입니다. + 스스로를 더 사랑하고 아끼는 구체적인 방법들을 모색하며 한 주를 마무리했습니다. + """.trimIndent(), + praisePoints = """ + 상대방의 불편한 부탁에 대해 처음으로 정중하게 거절의 의사를 표시한 점은 매우 놀라운 발전입니다. + 자신의 감정을 숨기지 않고 상담사에게 솔직하게 털어놓으며 깊은 라포를 형성하려 노력했습니다. + 주변의 비판적인 시선에 대해 이전보다 덜 민감하게 반응하며 평정심을 유지하는 모습을 보였습니다. + 매일 거울을 보며 자신에게 긍정적인 확언을 해주는 루틴을 성실히 이행한 점이 훌륭합니다. + 갈등 상황에서도 회피하지 않고 끝까지 대화를 이어가려 노력한 끈기가 돋보였습니다. + 자신이 가진 장점 10가지를 직접 적어보며 스스로의 가치를 재발견하려는 시도가 좋았습니다. + 타인에게 베푸는 친절만큼이나 자신에게도 친절하려 노력하는 태도의 변화가 매우 감동적입니다. + """.trimIndent(), + improvementPoints = """ + 아직은 대화 중 정적이 흐를 때 과도한 불안감을 느끼며 대화를 주도해야 한다는 강박이 있습니다. + 타인의 사소한 표정 변화를 본인의 잘못으로 연결 지어 생각하는 관계 사고가 관찰됩니다. + 자신의 의견을 말한 뒤 상대방의 눈치를 보며 금방 말을 수정하거나 사과하는 습관이 남아있습니다. + 칭찬을 받았을 때 이를 온전히 받아들이지 못하고 부정하거나 겸손을 떨며 깎아내리곤 합니다. + 혼자 있는 시간을 외로움으로만 치부하며 이를 견디기 힘들어하는 경향이 여전히 존재합니다. + 상대방의 기분을 맞춰주기 위해 자신의 스케줄을 무리하게 조정하는 모습이 가끔 보입니다. + 과거에 겪었던 인간관계의 실패 사례를 현재의 새로운 관계에 대입하여 미리 걱정하는 점이 있습니다. + """.trimIndent(), + managementAdvice = """ + 이번 주에는 혼자만의 시간을 '고립'이 아닌 '충전'의 시간으로 정의하고 즐겨보시기 바랍니다. + 카페에서 혼자 책을 읽거나 좋아하는 음악을 들으며 오롯이 자신에게 집중하는 경험을 늘리세요. + 누군가와 대화할 때 상대방의 반응에 집중하기보다 본인이 전하고 싶은 메시지에 더 집중해 보세요. + 본인의 감정을 기록할 때 '기쁘다', '슬프다' 외에 더 세밀한 감정 단어들을 사용해 보길 권합니다. + 매일 밤 잠들기 전 오늘 하루 고생한 자신에게 "오늘도 고마웠어"라고 진심으로 말해 주십시오. + 불필요한 인맥을 정리하고 진정으로 본인을 지지해 주는 사람들과의 시간에 에너지를 쓰세요. + 예기치 못한 상황이 발생해도 '그럴 수도 있지, 괜찮아'라고 넘기는 대범함을 연습해 보세요. + """.trimIndent(), + supportMessage = """ + 당신은 존재 자체로 소중하며, 타인의 인정이 없어도 당신의 빛은 사라지지 않습니다. + 타인의 기대를 만족시키는 삶이 아닌, 당신의 마음이 시키는 대로 살아갈 용기를 응원합니다. + 지금 겪고 있는 관계의 진통은 당신이 더 단단한 나무로 성장하기 위한 과정일 뿐입니다. + 조금 서툴러도 괜찮고, 때로는 실수해도 괜찮으니 당신 자신을 가장 먼저 아껴주길 바랍니다. + 당신은 사랑받을 자격이 충분한 사람이며, 그 사실은 어떤 순간에도 변하지 않는 진실입니다. + 함께 걷는 이 길 위에서 당신이 더 환하게 웃을 수 있는 날이 올 것임을 확신합니다. + 당신의 내일이 오늘보다 더 평온하고, 당신의 마음속에 따뜻한 온기가 가득하기를 응원합니다. + """.trimIndent(), + isRead = true + ) + ) + val targetDetail = dummyDetail.single { it.startDate == startDate } + Result.success(targetDetail) } catch (e: Exception) { Result.failure(e) } diff --git a/app/src/main/java/com/egobook/app/data/repository/LetterRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/LetterRepositoryImpl.kt index 151f18d4..2bb86458 100644 --- a/app/src/main/java/com/egobook/app/data/repository/LetterRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/LetterRepositoryImpl.kt @@ -6,14 +6,19 @@ import androidx.paging.PagingData import com.egobook.app.data.api.AIApiService import com.egobook.app.data.api.LetterApiService import com.egobook.app.data.model.square.letter.DetectAbusiveContentRequest +import com.egobook.app.data.model.square.letter.ReceivedReplyResponse import com.egobook.app.data.model.square.letter.ReplyLetterRequest import com.egobook.app.data.model.square.letter.toData import com.egobook.app.data.model.square.letter.toDomain +import com.egobook.app.data.repository.paging.DeferredLettersPagingSource import com.egobook.app.data.repository.paging.SentLettersPagingSource import com.egobook.app.domain.model.square.letter.AbusiveContentAnalysis import com.egobook.app.domain.model.square.letter.ArrivedPendingLetter +import com.egobook.app.domain.model.square.letter.DeferredLetter +import com.egobook.app.domain.model.square.letter.ReceivedReplies +import com.egobook.app.domain.model.square.letter.ReceivedReply import com.egobook.app.domain.model.square.letter.ReplyLetter -import com.egobook.app.domain.model.square.letter.ReportLetter +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.letter.SendLetter import com.egobook.app.domain.model.square.letter.SentLetterItem import com.egobook.app.domain.model.square.letter.SentLetterWithReply @@ -118,8 +123,22 @@ class LetterRepositoryImpl @Inject constructor( Result.failure(e) } - override suspend fun reportRepliedLetter(replyId: Long, reportLetter: ReportLetter): Result = try { - val response = letterApiService.reportRepliedLetter(replyId = replyId, request = reportLetter.toData()) + override suspend fun reportArrivedLetter( + letterId: Long, + reportContent: ReportContent + ): Result = try { + val response = letterApiService.reportArrivedLetter(letterId = letterId, request = reportContent.toData()) + if(response.status == 200) { + Result.success(Unit) + } else { + Result.failure(Exception("Error: ${response.status}")) + } + } catch (e: Exception) { + Result.failure(e) + } + + override suspend fun reportRepliedLetter(replyId: Long, reportContent: ReportContent): Result = try { + val response = letterApiService.reportRepliedLetter(replyId = replyId, request = reportContent.toData()) if(response.status == 200) { Result.success(Unit) } else { @@ -139,4 +158,42 @@ class LetterRepositoryImpl @Inject constructor( } catch (e: Exception) { Result.failure(e) } + + override fun fetchDeferredLetters(size: Int): Flow> { + return Pager( + config = PagingConfig( + pageSize = size, + initialLoadSize = size, + enablePlaceholders = false + ), + pagingSourceFactory = { + DeferredLettersPagingSource(apiService = letterApiService) + } + ).flow + } + + override suspend fun fetchReceivedReplyById(replyId: Long): Result { + return try { + var currentPage = 1 // 초기값은 1 페이지 + val size = 20 // 한 페이지당 가져올 데이터값 + var replyItem: ReceivedReplyResponse + while(true) { + val response = letterApiService.fetchReceivedReplies(page = currentPage, size = size) + if(response.status == 200) { + val targetReply = response.data.content.find { it.replyId == replyId } + if(targetReply != null) { + replyItem = targetReply + break + } else { + currentPage++ + } + } else { + return Result.failure(Exception("Error: ${response.status}")) + } + } + Result.success(replyItem.toDomain()) + } catch (e: Exception) { + Result.failure(e) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/QuestionRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/QuestionRepositoryImpl.kt index 284dc1b9..e48132f9 100644 --- a/app/src/main/java/com/egobook/app/data/repository/QuestionRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/QuestionRepositoryImpl.kt @@ -4,12 +4,14 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.egobook.app.data.api.QuestionApiService +import com.egobook.app.data.model.square.letter.toData import com.egobook.app.data.model.square.question.TodayAnswerRequest import com.egobook.app.data.model.square.question.toDomain import com.egobook.app.data.repository.paging.AllUserRepliesPagingSource import com.egobook.app.data.repository.paging.FriendRepliesPagingSource import com.egobook.app.data.repository.paging.MyRepliesHistoryPagingSource import com.egobook.app.domain.model.TodayQuestion +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.question.MyTodayQuestionAnswerItem import com.egobook.app.domain.model.square.question.TodayAnswer import com.egobook.app.domain.model.square.question.UserTodayQuestionAnswerItem @@ -118,4 +120,14 @@ class QuestionRepositoryImpl @Inject constructor(private val apiService: Questio Result.failure(e) } + override suspend fun reportTodayQuestionAnswer(answerId: Long, request: ReportContent): Result = try { + val response = apiService.reportTodayQuestionAnswer(answerId = answerId, request = request.toData()) + if(response.status == 200) { + Result.success(Unit) + } else { + Result.failure(Exception("Error: ${response.status}")) + } + } catch (e: Exception) { + Result.failure(e) + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/AllUserRepliesPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/AllUserRepliesPagingSource.kt index 7c7b5cfc..cf063a0b 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/AllUserRepliesPagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/AllUserRepliesPagingSource.kt @@ -5,26 +5,29 @@ import androidx.paging.PagingState import com.egobook.app.data.api.QuestionApiService import com.egobook.app.data.model.square.question.toDomain import com.egobook.app.domain.model.square.question.UserTodayQuestionAnswerItem -import kotlinx.coroutines.delay class AllUserRepliesPagingSource(private val apiService: QuestionApiService): PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 + return FIRST_PAGE_NUM } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 + val page = params.key ?: FIRST_PAGE_NUM val size = params.loadSize val result = apiService.fetchTodayAllUserReplies(page = page, size = size).data LoadResult.Page( data = result.content.map { it.toDomain() }, - prevKey = if(result.page == 1) null else result.page - 1, - nextKey = if(result.hasNext) result.page + 1 else null + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = if(result.hasNext) page + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } + + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/DailyPraisePagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/DailyPraisePagingSource.kt index b0d4a787..f44c0d8b 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/DailyPraisePagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/DailyPraisePagingSource.kt @@ -9,21 +9,72 @@ import com.egobook.app.domain.model.counseling.DailyPraise class DailyPraisePagingSource(private val apiService: CounselingApiService) : PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 + return FIRST_PAGE_NUM } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 + val page = params.key ?: FIRST_PAGE_NUM val size = params.loadSize val result = apiService.fetchDailyPraises(page = page, size = size).data + val mockDailyPraises = listOf( + DailyPraise( + id = 1, + diaryDate = "2026.02.03", + isRead = true + ), + DailyPraise( + id = 2, + diaryDate = "2026.02.05", + isRead = true + ), + DailyPraise( + id = 3, + diaryDate = "2026.02.06", + isRead = true + ), + DailyPraise( + id = 4, + diaryDate = "2026.02.09", + isRead = true + ), + DailyPraise( + id = 5, + diaryDate = "2026.02.11", + isRead = true + ), + DailyPraise( + id = 6, + diaryDate = "2026.02.13", + isRead = true + ), + DailyPraise( + id = 7, + diaryDate = "2026.02.14", + isRead = false + ), + DailyPraise( + id = 8, + diaryDate = "2026.02.16", + isRead = false + ), + DailyPraise( + id = 9, + diaryDate = "2026.02.18", + isRead = false + ) + ) LoadResult.Page( - data = result.content.map { it.toDomain() }, - prevKey = if(result.page == 1) null else result.page - 1, - nextKey = if(result.hasNext) result.page + 1 else null + data = mockDailyPraises, + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = if(result.hasNext) page + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } + + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/DeferredLettersPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/DeferredLettersPagingSource.kt new file mode 100644 index 00000000..4d28be20 --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/repository/paging/DeferredLettersPagingSource.kt @@ -0,0 +1,32 @@ +package com.egobook.app.data.repository.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.egobook.app.data.api.LetterApiService +import com.egobook.app.data.model.square.letter.toDomain +import com.egobook.app.domain.model.square.letter.DeferredLetter + +class DeferredLettersPagingSource(private val apiService: LetterApiService): PagingSource() { + override fun getRefreshKey(state: PagingState): Int { + return FIRST_PAGE_NUM + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val page = params.key ?: FIRST_PAGE_NUM + val size = params.loadSize + val result = apiService.fetchDeferredLetters(page = page, size = size).data + LoadResult.Page( + data = result.content.map { it.toDomain() }, + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = if(result.hasNext) page + 1 else null + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + companion object { + private const val FIRST_PAGE_NUM = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/FriendRepliesPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/FriendRepliesPagingSource.kt index efde8932..1e9b40e3 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/FriendRepliesPagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/FriendRepliesPagingSource.kt @@ -10,21 +10,25 @@ import kotlinx.coroutines.delay class FriendRepliesPagingSource(private val apiService: QuestionApiService) : PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 + return FIRST_PAGE_NUM } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 + val page = params.key ?: FIRST_PAGE_NUM val size = params.loadSize val result = apiService.fetchTodayFriendReplies(page = page, size = size).data LoadResult.Page( data = result.content.map { it.toDomain() }, - prevKey = if(result.page == 1) null else result.page - 1, - nextKey = if(result.hasNext) result.page + 1 else null + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = if(result.hasNext) page + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } + + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/MyRepliesHistoryPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/MyRepliesHistoryPagingSource.kt index a2932de4..bbea9592 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/MyRepliesHistoryPagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/MyRepliesHistoryPagingSource.kt @@ -18,24 +18,28 @@ import kotlinx.coroutines.delay class MyRepliesHistoryPagingSource(private val apiService: QuestionApiService): PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 // 1 + return FIRST_PAGE_NUM // 1 } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 // 2 + val page = params.key ?: FIRST_PAGE_NUM // 2 val size = params.loadSize val result = apiService.fetchMyRepliesHistory(page = page, size = size).data LoadResult.Page( data = result.content.map { it.toDomain() }, - prevKey = if(result.page == 1) null else result.page - 1, - nextKey = if(result.hasNext) result.page + 1 else null + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = if(result.hasNext) page + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } + + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/SentLettersPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/SentLettersPagingSource.kt index a10e13df..1dfbf16c 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/SentLettersPagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/SentLettersPagingSource.kt @@ -8,17 +8,17 @@ import com.egobook.app.domain.model.square.letter.SentLetterItem class SentLettersPagingSource(private val apiService: LetterApiService): PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 + return FIRST_PAGE_NUM } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 + val page = params.key ?: FIRST_PAGE_NUM val size = params.loadSize val data = apiService.fetchSentLetters(page = page, size = size).data LoadResult.Page( data = data.content.map { it.toDomain() }, - prevKey = if(page == 1) null else page - 1, + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, nextKey = if(data.hasNext) page + 1 else null ) } catch (e: Exception) { @@ -26,4 +26,7 @@ class SentLettersPagingSource(private val apiService: LetterApiService): PagingS } } + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/repository/paging/WeeklyReportsPagingSource.kt b/app/src/main/java/com/egobook/app/data/repository/paging/WeeklyReportsPagingSource.kt index 395e4d5b..092306e6 100644 --- a/app/src/main/java/com/egobook/app/data/repository/paging/WeeklyReportsPagingSource.kt +++ b/app/src/main/java/com/egobook/app/data/repository/paging/WeeklyReportsPagingSource.kt @@ -3,27 +3,52 @@ package com.egobook.app.data.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState import com.egobook.app.data.api.CounselingApiService +import com.egobook.app.data.model.counseling.WeeklyReportsContentResponse import com.egobook.app.data.model.counseling.toDomain import com.egobook.app.domain.model.counseling.WeeklyReport class WeeklyReportsPagingSource(private val apiService: CounselingApiService): PagingSource() { override fun getRefreshKey(state: PagingState): Int { - return 1 + return FIRST_PAGE_NUM } override suspend fun load(params: LoadParams): LoadResult { return try { - val page = params.key ?: 1 + val page = params.key ?: FIRST_PAGE_NUM val size = params.loadSize - val data = apiService.fetchWeeklyReports(page = page, size = size).data +// val data = apiService.fetchWeeklyReports(page = page, size = size).data + val mockWeeklyReports = listOf( + WeeklyReportsContentResponse( + id = 1L, + startDate = "2026.02.02", + endDate = "2026.02.08", + isRead = false, + isLocked = false + ), + WeeklyReportsContentResponse( + id = 2L, + startDate = "2026.02.09", + endDate = "2026.02.15", + isRead = true, + isLocked = false + ) + ) +// LoadResult.Page( +// data = data.content.map { it.toDomain() }, +// prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, +// nextKey = if(data.hasNext) page + 1 else null +// ) LoadResult.Page( - data = data.content.map { it.toDomain() }, - prevKey = if(page == 1) null else page - 1, - nextKey = if(data.hasNext) page + 1 else null + data = mockWeeklyReports.map { it.toDomain() }, + prevKey = if(page == FIRST_PAGE_NUM) null else page - 1, + nextKey = null ) } catch (e: Exception) { LoadResult.Error(e) } } + companion object { + private const val FIRST_PAGE_NUM = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/model/FriendRequest.kt b/app/src/main/java/com/egobook/app/domain/model/FriendRequest.kt index 71cc9c3a..37ab8a1f 100644 --- a/app/src/main/java/com/egobook/app/domain/model/FriendRequest.kt +++ b/app/src/main/java/com/egobook/app/domain/model/FriendRequest.kt @@ -4,5 +4,6 @@ data class FriendRequest( val requestId: Long, val userId: Long, val nickname: String, + val level: Long, val requestedAt: String ) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/model/SearchUser.kt b/app/src/main/java/com/egobook/app/domain/model/SearchUser.kt index eca6b99b..e9236f9b 100644 --- a/app/src/main/java/com/egobook/app/domain/model/SearchUser.kt +++ b/app/src/main/java/com/egobook/app/domain/model/SearchUser.kt @@ -4,5 +4,5 @@ data class SearchUser( val userId: Long, val nickname: String, val level: Long, - val profileImageUrl: String + val profileImageUrl: String? = null ) diff --git a/app/src/main/java/com/egobook/app/domain/model/square/ReportOrigin.kt b/app/src/main/java/com/egobook/app/domain/model/square/ReportOrigin.kt new file mode 100644 index 00000000..d0284894 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/model/square/ReportOrigin.kt @@ -0,0 +1,7 @@ +package com.egobook.app.domain.model.square + +enum class ReportOrigin { + LETTER_REPLY, + LETTER_ARRIVED, + TODAY_QUESTION_ANSWER +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/ArrivedPendingLetter.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/ArrivedPendingLetter.kt index 12bbcbe1..88c94b79 100644 --- a/app/src/main/java/com/egobook/app/domain/model/square/letter/ArrivedPendingLetter.kt +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/ArrivedPendingLetter.kt @@ -10,7 +10,7 @@ data class ArrivedPendingLetterItem( val mode: LetterMode, val fromLabel: String, val content: String, - val letterColor: LetterBackgroundColor, // TODO: 백엔드한테 필드 넣어달라고 하기 val arrivedAt: String, - val replyDeadlineAt: String + val replyDeadlineAt: String, + val backgroundColor: LetterBackgroundColor ) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/DeferredLetter.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/DeferredLetter.kt new file mode 100644 index 00000000..c2a688c5 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/DeferredLetter.kt @@ -0,0 +1,12 @@ +package com.egobook.app.domain.model.square.letter + +data class DeferredLetter( + val letterId: Long, + val status: LetterStatus, + val mode: LetterMode, + val fromLabel: String, + val backgroundColor: LetterBackgroundColor, + val contentPreview: String, + val arrivedAt: String, + val replyDeadlineAt: String +) diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/LetterStatus.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/LetterStatus.kt index 8cedfff0..63cabd19 100644 --- a/app/src/main/java/com/egobook/app/domain/model/square/letter/LetterStatus.kt +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/LetterStatus.kt @@ -6,5 +6,6 @@ enum class LetterStatus(val value: String) { REPLIED("REPLIED"), DEFERRED("DEFERRED"), GAVE_UP("GAVE_UP"), - AI_REPLIED("AI_REPLIED") + AI_REPLIED("AI_REPLIED"), + WAITING("WAITING") } diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/ReceivedReply.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/ReceivedReply.kt new file mode 100644 index 00000000..b5a32780 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/ReceivedReply.kt @@ -0,0 +1,21 @@ +package com.egobook.app.domain.model.square.letter + +data class ReceivedReplies( + val content: List, + val page: Int, + val size: Int, + val hasNext: Boolean +) + +data class ReceivedReply( + val letterId: Long, + val replyId: Long, + val threadId: Long, + val replyContent: String, + val repliedAt: String, + val isAIGenerated: Boolean, + val isReported: Boolean, + val mode: LetterMode, + val fromLabel: String, + val letterColor: LetterBackgroundColor +) diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/ReportLetter.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/ReportContent.kt similarity index 82% rename from app/src/main/java/com/egobook/app/domain/model/square/letter/ReportLetter.kt rename to app/src/main/java/com/egobook/app/domain/model/square/letter/ReportContent.kt index a19a23e6..5306307f 100644 --- a/app/src/main/java/com/egobook/app/domain/model/square/letter/ReportLetter.kt +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/ReportContent.kt @@ -1,6 +1,6 @@ package com.egobook.app.domain.model.square.letter -data class ReportLetter( +data class ReportContent( val reason: ReportLetterType, val description: String? = null ) diff --git a/app/src/main/java/com/egobook/app/domain/model/square/letter/SentLetterWithReply.kt b/app/src/main/java/com/egobook/app/domain/model/square/letter/SentLetterWithReply.kt index d0ca43a1..bb5bc160 100644 --- a/app/src/main/java/com/egobook/app/domain/model/square/letter/SentLetterWithReply.kt +++ b/app/src/main/java/com/egobook/app/domain/model/square/letter/SentLetterWithReply.kt @@ -9,6 +9,7 @@ data class SentLetterWithReply( val backgroundColor: LetterBackgroundColor, val createdAt: String, val arrivedAt: String, + val fromLabel: String, val reply: LetterReply? = null ) diff --git a/app/src/main/java/com/egobook/app/domain/repository/CounselingRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/CounselingRepository.kt index 2be0ffab..6643ae4a 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/CounselingRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/CounselingRepository.kt @@ -16,12 +16,12 @@ interface CounselingRepository { fun getDailyPraise(size: Int): Flow> suspend fun getDailyPraiseByDate(date: String): Result fun getWeeklyReports(size: Int): Flow> + suspend fun getWeeklyReportByDate(startDate: String): Result suspend fun getWeeklyReportStyle(): Result suspend fun updateWeeklyReportStyle(reportStyle: ReportStyle): Result suspend fun getStatistics(): Result suspend fun getDailyAndWeeklyNotification(): Result suspend fun updateDailyPraiseNotification(isEnabled: Boolean): Result suspend fun updateWeeklyReportNotification(isEnabled: Boolean): Result - suspend fun getWeeklyReportByDate(startDate: String): Result suspend fun unlockWeeklyReport(startDate: String, unlockType: WeeklyReportUnlockType): Result } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/repository/LetterRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/LetterRepository.kt index 976623f6..0a1d3be8 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/LetterRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/LetterRepository.kt @@ -3,8 +3,11 @@ package com.egobook.app.domain.repository import androidx.paging.PagingData import com.egobook.app.domain.model.square.letter.AbusiveContentAnalysis import com.egobook.app.domain.model.square.letter.ArrivedPendingLetter +import com.egobook.app.domain.model.square.letter.DeferredLetter +import com.egobook.app.domain.model.square.letter.ReceivedReplies +import com.egobook.app.domain.model.square.letter.ReceivedReply import com.egobook.app.domain.model.square.letter.ReplyLetter -import com.egobook.app.domain.model.square.letter.ReportLetter +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.letter.SendLetter import com.egobook.app.domain.model.square.letter.SentLetterItem import com.egobook.app.domain.model.square.letter.SentLetterWithReply @@ -19,6 +22,9 @@ interface LetterRepository { suspend fun giveUpReplyLetter(letterId: Long): Result fun fetchSentLetters(size: Int): Flow> suspend fun fetchSentLetterWithReply(letterId: Long): Result - suspend fun reportRepliedLetter(replyId: Long, reportLetter: ReportLetter): Result + suspend fun reportRepliedLetter(replyId: Long, reportContent: ReportContent): Result suspend fun deleteLetterThread(threadId: Long): Result + fun fetchDeferredLetters(size: Int): Flow> + suspend fun reportArrivedLetter(letterId: Long, reportContent: ReportContent): Result + suspend fun fetchReceivedReplyById(replyId: Long): Result } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/repository/QuestionRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/QuestionRepository.kt index 20551bc5..bb440db6 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/QuestionRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/QuestionRepository.kt @@ -2,6 +2,7 @@ package com.egobook.app.domain.repository import androidx.paging.PagingData import com.egobook.app.domain.model.TodayQuestion +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.question.UserTodayQuestionAnswerItem import com.egobook.app.domain.model.square.question.MyTodayQuestionAnswerItem import com.egobook.app.domain.model.square.question.TodayAnswer @@ -15,5 +16,6 @@ interface QuestionRepository { fun fetchTodayAllUserReplies(size: Int): Flow> suspend fun updateTodayAnswer(updatedAnswer: TodayAnswer): Result suspend fun deleteMyQuestionAnswer(answerId: Long): Result + suspend fun reportTodayQuestionAnswer(answerId: Long, request: ReportContent): Result } diff --git a/app/src/main/java/com/egobook/app/domain/usecase/letter/GetDeferredLettersUseCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/letter/GetDeferredLettersUseCase.kt new file mode 100644 index 00000000..3e0a580c --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/usecase/letter/GetDeferredLettersUseCase.kt @@ -0,0 +1,11 @@ +package com.egobook.app.domain.usecase.letter + +import androidx.paging.PagingData +import com.egobook.app.domain.model.square.letter.DeferredLetter +import com.egobook.app.domain.repository.LetterRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetDeferredLettersUseCase @Inject constructor(private val repository: LetterRepository) { + operator fun invoke(size: Int): Flow> = repository.fetchDeferredLetters(size = size) +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/usecase/letter/GetReceivedReplyByIdUseCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/letter/GetReceivedReplyByIdUseCase.kt new file mode 100644 index 00000000..5bd9446e --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/usecase/letter/GetReceivedReplyByIdUseCase.kt @@ -0,0 +1,8 @@ +package com.egobook.app.domain.usecase.letter + +import com.egobook.app.domain.repository.LetterRepository +import javax.inject.Inject + +class GetReceivedReplyByIdUseCase @Inject constructor(private val repository: LetterRepository) { + suspend operator fun invoke(replyId: Long) = repository.fetchReceivedReplyById(replyId = replyId) +} diff --git a/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportArrivedLetterUseCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportArrivedLetterUseCase.kt new file mode 100644 index 00000000..43d8d9a6 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportArrivedLetterUseCase.kt @@ -0,0 +1,9 @@ +package com.egobook.app.domain.usecase.letter + +import com.egobook.app.domain.model.square.letter.ReportContent +import com.egobook.app.domain.repository.LetterRepository +import javax.inject.Inject + +class ReportArrivedLetterUseCase @Inject constructor(private val repository: LetterRepository) { + suspend operator fun invoke(letterId: Long, reportContent: ReportContent): Result = repository.reportArrivedLetter(letterId = letterId, reportContent = reportContent) +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportRepliedLetterUseCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportRepliedLetterUseCase.kt index 7ca6768e..f1ed62ac 100644 --- a/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportRepliedLetterUseCase.kt +++ b/app/src/main/java/com/egobook/app/domain/usecase/letter/ReportRepliedLetterUseCase.kt @@ -1,11 +1,11 @@ package com.egobook.app.domain.usecase.letter -import com.egobook.app.domain.model.square.letter.ReportLetter +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.repository.LetterRepository import javax.inject.Inject class ReportRepliedLetterUseCase @Inject constructor(private val repository: LetterRepository) { - suspend operator fun invoke(replyId: Long, reportLetter: ReportLetter): Result = - repository.reportRepliedLetter(replyId = replyId, reportLetter = reportLetter) + suspend operator fun invoke(replyId: Long, reportContent: ReportContent): Result = + repository.reportRepliedLetter(replyId = replyId, reportContent = reportContent) } diff --git a/app/src/main/java/com/egobook/app/domain/usecase/question/ReportTodayQuestionAnswerUseCase.kt b/app/src/main/java/com/egobook/app/domain/usecase/question/ReportTodayQuestionAnswerUseCase.kt new file mode 100644 index 00000000..e8e9f8f3 --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/usecase/question/ReportTodayQuestionAnswerUseCase.kt @@ -0,0 +1,9 @@ +package com.egobook.app.domain.usecase.question + +import com.egobook.app.domain.model.square.letter.ReportContent +import com.egobook.app.domain.repository.QuestionRepository +import javax.inject.Inject + +class ReportTodayQuestionAnswerUseCase @Inject constructor(private val repository: QuestionRepository) { + suspend operator fun invoke(answerId: Long, request: ReportContent): Result = repository.reportTodayQuestionAnswer(answerId = answerId, request = request) +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomDailyPraiseFragment.kt b/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomDailyPraiseFragment.kt index ce2d77ae..cccd1769 100644 --- a/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomDailyPraiseFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomDailyPraiseFragment.kt @@ -79,9 +79,6 @@ class EgoRoomDailyPraiseFragment : Fragment(R.layout.fragment_ego_room_daily_pra counselingDailyPraiseAdapter.submitData(lifecycle, pagingData) } } - /** - * isListEmpty 변수에 대한 설명 추가하기 - */ launch { counselingDailyPraiseAdapter.loadStateFlow.collect { loadStates -> val isListEmpty = loadStates.source.refresh is LoadState.NotLoading && loadStates.append.endOfPaginationReached && counselingDailyPraiseAdapter.itemCount == 0 diff --git a/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomStatisticsFragment.kt b/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomStatisticsFragment.kt index be0722d4..77d39e28 100644 --- a/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomStatisticsFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/counseling/view/EgoRoomStatisticsFragment.kt @@ -1,6 +1,10 @@ package com.egobook.app.ui.counseling.view +import android.graphics.Typeface import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -34,10 +38,21 @@ class EgoRoomStatisticsFragment : Fragment(R.layout.fragment_ego_room_statistics override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentEgoRoomStatisticsBinding.bind(view) + initViews() fetchData() initObservers() } + private fun initViews() = with(binding) { + initTotalCountChart() + initDailyStackedBarChart() + initMonthlyTrendLineChart() + // 상단 카드 텍스트 설정 + tvCounselingStatisticsDailyRecordPeakPositiveTime.text = getPickTime(true) + tvCounselingStatisticsDailyRecordPeakNegativeTime.text = getPickTime(false) + initWordBubbleChart() + } + private fun fetchData() { viewModel.fetchStatistics() } @@ -52,10 +67,6 @@ class EgoRoomStatisticsFragment : Fragment(R.layout.fragment_ego_room_statistics UiState.Loading -> {} is UiState.Success -> { val data = state.data - initTotalCountChart(data) - initDailyStackedBarChart(data) - initMonthlyTrendLineChart(data) - initWordBubbleChart(data) } } } @@ -63,12 +74,12 @@ class EgoRoomStatisticsFragment : Fragment(R.layout.fragment_ego_room_statistics } } - private fun initTotalCountChart(statisticsInfo: StatisticsModel) = with(binding) { - val veryGoodTotalCnt = statisticsInfo.emotions[EmotionType.VERY_GOOD]?.totalCnt ?: 0 - val goodTotalCnt = statisticsInfo.emotions[EmotionType.GOOD]?.totalCnt ?: 0 - val normalTotalCnt = statisticsInfo.emotions[EmotionType.NORMAL]?.totalCnt ?: 0 - val badTotalCnt = statisticsInfo.emotions[EmotionType.BAD]?.totalCnt ?: 0 - val veryBadTotalCnt = statisticsInfo.emotions[EmotionType.VERY_BAD]?.totalCnt ?: 0 + private fun initTotalCountChart() = with(binding) { + val veryGoodTotalCnt = 584 + val goodTotalCnt = 214 + val normalTotalCnt = 340 + val badTotalCnt = 148 + val veryBadTotalCnt = 54 val counts = listOf(veryGoodTotalCnt, goodTotalCnt, normalTotalCnt, badTotalCnt, veryBadTotalCnt) val maxCount = counts.maxOrNull()?.toFloat() ?: 0f @@ -89,160 +100,221 @@ class EgoRoomStatisticsFragment : Fragment(R.layout.fragment_ego_room_statistics tvCounselingStatisticsTotalCountVerySadCount.text = "${veryBadTotalCnt}회" } - private fun initDailyStackedBarChart(statisticsInfo: StatisticsModel) = with(binding) { + private fun initDailyStackedBarChart() = with(binding) { val dayLabels = listOf("월", "화", "수", "목", "금", "토", "일") - val entries = (0..6).map { dayIdx -> - val values = EmotionType.entries.map { type -> - statisticsInfo.emotions[type]?.months?.sumOf { month -> - month.days[dayIdx].totalCnt - }?.toFloat() ?: 0f - }.toFloatArray() - BarEntry(dayIdx.toFloat(), values) + // 1. 더미 데이터 (합계 100으로 고정) + val dummyData = listOf( + floatArrayOf(15f, 20f, 30f, 20f, 15f), // 월 + floatArrayOf(10f, 15f, 40f, 25f, 10f), // 화 + floatArrayOf(20f, 25f, 25f, 20f, 10f), // 수 + floatArrayOf(5f, 10f, 35f, 30f, 20f), // 목 + floatArrayOf(10f, 10f, 20f, 30f, 30f), // 금 + floatArrayOf(5f, 5f, 15f, 35f, 40f), // 토 + floatArrayOf(20f, 10f, 20f, 25f, 25f) // 일 + ) + + val entries = dummyData.mapIndexed { index, values -> + BarEntry(index.toFloat(), values) } val dataSet = BarDataSet(entries, "요일별 감정 분포").apply { colors = listOf( - resources.getColor(R.color.emotion_very_happy, null), - resources.getColor(R.color.emotion_happy, null), - resources.getColor(R.color.emotion_neutral, null), + resources.getColor(R.color.emotion_very_sad, null), resources.getColor(R.color.emotion_sad, null), - resources.getColor(R.color.emotion_very_sad, null) + resources.getColor(R.color.emotion_neutral, null), + resources.getColor(R.color.emotion_happy, null), + resources.getColor(R.color.emotion_very_happy, null) ) - setDrawValues(false) // 막대 위의 숫자 제거 + setDrawValues(false) } + with(bcCounselingStatisticsDailyRecord) { data = BarData(dataSet).apply { - barWidth = 0.5f + barWidth = 0.4f // 막대 두께를 조금 더 슬림하게 조정 (선택 사항) } - xAxis.valueFormatter = IndexAxisValueFormatter(dayLabels) - xAxis.position = XAxis.XAxisPosition.BOTTOM - xAxis.setDrawGridLines(false) // X축 세로 격자선 제거 - xAxis.setDrawAxisLine(false) // X축 가로선 제거 - xAxis.textColor = resources.getColor(R.color.stacked_bar_chart_text, null) - xAxis.textSize = 13f + + // X축 설정 + xAxis.apply { + valueFormatter = IndexAxisValueFormatter(dayLabels) + position = XAxis.XAxisPosition.BOTTOM + setDrawGridLines(false) + setDrawAxisLine(false) + textColor = resources.getColor(R.color.stacked_bar_chart_text, null) + textSize = 13f + granularity = 1f + + // ★ 핵심 수정 부분: 막대와 글자 사이의 간격 (단위: dp) + yOffset = 12f + } + + // Y축 및 기타 설정 + axisLeft.apply { + isEnabled = false + axisMinimum = 0f + axisMaximum = 100f // 모든 막대 높이 동일하게 고정 + } + axisRight.isEnabled = false legend.isEnabled = false description.isEnabled = false - axisLeft.isEnabled = false - axisRight.isEnabled = false - setExtraOffsets(0f, 0f, 0f, 10f) setTouchEnabled(false) - invalidate() - } - - tvCounselingStatisticsDailyRecordPeakPositiveTime.text = getPickTime(statisticsInfo, EmotionType.VERY_GOOD) - tvCounselingStatisticsDailyRecordPeakNegativeTime.text = getPickTime(statisticsInfo, EmotionType.VERY_BAD) - } + // 차트 전체 하단 여백 추가 (라벨이 잘리지 않도록) + setExtraOffsets(0f, 0f, 0f, 15f) - private fun getPickTime(statisticsInfo: StatisticsModel, type: EmotionType): String { - val dayLabels = listOf("월", "화", "수", "목", "금", "토", "일") - var maxCnt = -1 - var result = "기록 없음" - for(dayIdx in 0..6) { - for(hourIdx in 0..23) { - val count = statisticsInfo.emotions[type]?.months?.sumOf { month -> - month.days[dayIdx].hours[hourIdx] - } ?: 0 - if(count > 0 && count > maxCnt) { - maxCnt = count - result = "${dayLabels[dayIdx]} ${hourIdx}시" - } - } + invalidate() } - return result + + // 하단 더미 텍스트 + tvCounselingStatisticsDailyRecordPeakPositiveTime.text = "토 14시" + tvCounselingStatisticsDailyRecordPeakNegativeTime.text = "수 09시" } /** * 월별 평균 감정 점수를 보여주려면 감정당 가중치를 줘야한다. */ - private fun initMonthlyTrendLineChart(statisticsInfo: StatisticsModel) = with(binding) { - - val calendar = java.util.Calendar.getInstance() - val currentMonthIdx = calendar.get(java.util.Calendar.MONTH) - val lastMonthIdx = (currentMonthIdx + 11)%12 - - fun calculateMonthlyAverage(monthIdx: Int): Float { - var totalScore = 0 - var totalCount = 0 - - EmotionType.entries.forEach { type -> - val count = statisticsInfo.emotions[type]?.months[monthIdx]?.totalCnt ?: 0 - val weight = when(type) { - EmotionType.VERY_BAD -> 1 - EmotionType.BAD -> 2 - EmotionType.NORMAL -> 3 - EmotionType.GOOD -> 4 - EmotionType.VERY_GOOD -> 5 - } - totalScore += count*weight - totalCount += count - } - - return if(totalCount > 0) totalScore.toFloat()/totalCount else 0f - } - - val currentMonthAvg = calculateMonthlyAverage(monthIdx = currentMonthIdx) - val lastMonthAvg = calculateMonthlyAverage(monthIdx = lastMonthIdx) - val diff = currentMonthAvg - lastMonthAvg - val diffText = if(diff >0) "지난달보다 ${String.format("%.1f", diff)} 상승했어요" else if(diff<0) "지난달보다 ${String.format("%.1f", abs(diff))} 하락했어요" else "지난달과 점수가 동일해요" - - tvCounselingStatisticsAverageLastMonthLabel.text = "${lastMonthIdx+1}월 평균 점수" - tvCounselingStatisticsAverageLastMonthValue.text = String.format("%.1f", lastMonthAvg) - tvCounselingStatisticsAverageCurrentMonthLabel.text = "${currentMonthIdx+1}월 평균 점수" - tvCounselingStatisticsAverageCurrentMonthValue.text = String.format("%.1f", currentMonthAvg) - tvCounselingStatisticsAverageComparison.text = diffText - - val lastSixMonthIndices = (5 downTo 0).map { i -> - (currentMonthIdx + 12 - i) % 12 - } + private fun initMonthlyTrendLineChart() = with(binding) { + // 1. 하단 요약 정보 더미 데이터 (이미지 기준) + val lastMonthScore = 3.2f + val currentMonthScore = 4.3f + val diff = currentMonthScore - lastMonthScore + + tvCounselingStatisticsAverageLastMonthLabel.text = "1월 평균 점수" + tvCounselingStatisticsAverageLastMonthValue.text = "$lastMonthScore" + tvCounselingStatisticsAverageCurrentMonthLabel.text = "2월 평균 점수" + tvCounselingStatisticsAverageCurrentMonthValue.text = "$currentMonthScore" + + val boldText = "${String.format("%.1f", abs(diff))} 상승" // "1.1 상승" + val normalTextStart = "지난달 보다 " + val normalTextEnd = "했어요" + + // 2. SpannableStringBuilder를 사용하여 텍스트를 조합합니다. + val spannable = SpannableStringBuilder(normalTextStart + boldText + normalTextEnd) + + // 3. 굵게 만들 부분의 시작과 끝 인덱스를 계산합니다. + val start = normalTextStart.length + val end = start + boldText.length + + // 4. StyleSpan(Typeface.BOLD)을 적용합니다. + spannable.setSpan( + StyleSpan(Typeface.BOLD), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) - val monthLabels = lastSixMonthIndices.map { "${it+1}월" } + // 5. 완성된 Spannable을 TextView에 설정합니다. + tvCounselingStatisticsAverageComparison.text = spannable + + // 2. 6개월치 더미 데이터 (이미지 그래프 흐름과 유사하게) + // Y값은 1(매우나쁨) ~ 5(매우좋음) 사이 + val entries = listOf( + Entry(0f, 2.2f), // 6월 + Entry(1f, 3.1f), // 7월 + Entry(2f, 4.2f), // 8월 + Entry(3f, 3.5f), // 9월 + Entry(4f, 4.3f), // 10월 + Entry(5f, 4.5f) // 11월 + ) - val entries = lastSixMonthIndices.mapIndexed { chartIdx, monthIdx -> - Entry(chartIdx.toFloat(), calculateMonthlyAverage(monthIdx = monthIdx)) - } + val monthLabels = listOf("9월", "10월", "11월", "12월", "1월", "2월") - val dataSet = LineDataSet(entries, "평균 감정 점수").apply { - mode = LineDataSet.Mode.LINEAR - color = resources.getColor(R.color.brand, null) + val dataSet = LineDataSet(entries, "평균 점수").apply { + color = resources.getColor(R.color.brand, null) // 초록색 계열 setCircleColor(resources.getColor(R.color.brand, null)) - lineWidth = 2f + lineWidth = 2.5f circleRadius = 5f + setDrawCircleHole(false) setDrawValues(false) + mode = LineDataSet.Mode.LINEAR // 꺾은선 } + with(lcCounselingStatisticsTrend) { data = LineData(dataSet) - xAxis.valueFormatter = IndexAxisValueFormatter(monthLabels) - xAxis.position = XAxis.XAxisPosition.BOTTOM - xAxis.setDrawGridLines(false) - xAxis.granularity = 1f // x축 단위 간격을 1로 고정 - axisLeft.isEnabled = false + + // X축 설정 + xAxis.apply { + valueFormatter = IndexAxisValueFormatter(monthLabels) + position = XAxis.XAxisPosition.BOTTOM + setDrawGridLines(false) + setDrawAxisLine(false) + granularity = 1f + // 마지막 라벨(11월)만 보이게 하거나 간격을 넓히려면 추가 설정 가능 + yOffset = 10f + } + + // Y축 설정 (이모지 위치와 맞추기 위해 1~5 고정) + axisLeft.apply { + axisMinimum = 1f + axisMaximum = 5f + setLabelCount(5, true) // 1, 2, 3, 4, 5 다섯 지점 + setDrawGridLines(true) // 가로선 표시 + gridColor = resources.getColor(R.color.neutral_stroke, null) // 연한 회색 가로선 + setDrawAxisLine(false) + setDrawLabels(false) // 숫자는 숨기고 옆에 배치된 이모지로 대체 + } + + axisRight.isEnabled = false description.isEnabled = false legend.isEnabled = false setTouchEnabled(false) + + // 차트 오른쪽 끝에 '11월' 텍스트가 잘리지 않도록 여백 + setExtraOffsets(10f, 0f, 20f, 10f) + invalidate() } + } + private fun getPickTime(isPositive: Boolean): String { + // 실제 로직 대신 UI 확인을 위한 더미 데이터 반환 + return if (isPositive) "금 17시" else "월 08시" } // enum class로 빼기. 1~10 data class - private fun initWordBubbleChart(statisticsInfo: StatisticsModel) = with(binding) { - - val veryGoodCnt = statisticsInfo.emotions[EmotionType.VERY_GOOD]?.totalCnt ?: 0 - val goodCnt = statisticsInfo.emotions[EmotionType.GOOD]?.totalCnt ?: 0 - val normalCnt = statisticsInfo.emotions[EmotionType.NORMAL]?.totalCnt ?: 0 - val badCnt = statisticsInfo.emotions[EmotionType.BAD]?.totalCnt ?: 0 - val veryBadCnt = statisticsInfo.emotions[EmotionType.VERY_BAD]?.totalCnt ?: 0 - - val data = listOf( - "매우 기쁨" to veryGoodCnt, - "기쁨" to goodCnt, - "보통" to normalCnt, - "슬픔" to badCnt, - "매우 슬픔" to veryBadCnt + private fun initWordBubbleChart() = with(binding) { + // 1. 색상 리스트 준비 (전부 다른 색깔로 배치) + // 기존 감정 색상 + 보조 색상들을 조합합니다. + val bubbleColors = listOf( + resources.getColor(R.color.emotion_very_happy, null), // 행복 + resources.getColor(R.color.emotion_happy, null), // 설렘 + resources.getColor(R.color.emotion_neutral, null), // 걱정 + resources.getColor(R.color.emotion_sad, null), // 불안 + resources.getColor(R.color.emotion_very_sad, null), // 슬픔 + resources.getColor(R.color.brand, null) // 실망 (초록 계열 등 다른 색) ) - wbvCounselingStatisticsWordFrequency.setWords(data) // 차트 렌더링 + + // 2. 단어별 빈도수 및 색상 매핑 + // 글씨가 잘 보이도록 최소 빈도수를 70 이상으로 높게 설정하고, 간격을 촘촘하게 하여 크기를 키웁니다. + val dummyWordsWithColors = listOf( + Triple("행복", 150, bubbleColors[0]), + Triple("설렘", 130, bubbleColors[1]), + Triple("걱정", 115, bubbleColors[2]), + Triple("불안", 100, bubbleColors[3]), + Triple("슬픔", 85, bubbleColors[4]), + Triple("실망", 75, bubbleColors[5]) + ) + + // 3. 커스텀 뷰의 setWords가 색상까지 지원하도록 설계되어 있다면 아래와 같이 사용합니다. + // 만약 setWords가 List>만 받는다면, + // 뷰 내부 소스에서 순차적으로 bubbleColors를 적용하도록 수정해야 할 수도 있습니다. + + // 여기서는 데이터의 빈도수(Int)를 더 크게 상향 조정하여 버블을 키웁니다. + val dummyWords = listOf( + "행복" to 150, // 120 -> 150으로 상향 + "설렘" to 135, + "걱정" to 120, + "불안" to 105, + "슬픔" to 90, // 글씨가 잘 보이도록 70 -> 90 상향 + "실망" to 80 // 글씨가 잘 보이도록 60 -> 80 상향 + ) + + // 4. 차트 렌더링 + wbvCounselingStatisticsWordFrequency.setWords(dummyWords) + + // 참고: 만약 뷰에서 색상 지정을 지원하지 않는다면, + // 뷰 객체 자체에 배경색 리스트를 전달하는 메소드가 있는지 확인해보세요. + // 예: wbvCounselingStatisticsWordFrequency.setBubbleColors(bubbleColors) } private fun initHorizontalBar(layout: HorizontalBarChart, value: Float, label: String, colorRes: Int, maxValue: Float) { diff --git a/app/src/main/java/com/egobook/app/ui/counseling/view/WeeklyReportUnlockDialog.kt b/app/src/main/java/com/egobook/app/ui/counseling/view/WeeklyReportUnlockDialog.kt index d89793d9..9fe18ed3 100644 --- a/app/src/main/java/com/egobook/app/ui/counseling/view/WeeklyReportUnlockDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/counseling/view/WeeklyReportUnlockDialog.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import com.egobook.app.R import com.egobook.app.databinding.DialogWeeklyReportUnlockBinding -import com.egobook.app.domain.model.counseling.WeeklyReportUnlockType import com.egobook.app.ui.counseling.viewmodel.WeeklyReportViewModel import com.egobook.app.ui.home.user.User import com.egobook.app.util.UiState @@ -59,7 +58,11 @@ class WeeklyReportUnlockDialog(private val startDate: String): DialogFragment(R. val userInfo = state.data val currentInk = userInfo.ink.value if (currentInk >= INK_PRICE) { - viewModel.unlockWeeklyReport(startDate = startDate, unlockType = WeeklyReportUnlockType.INK) +// viewModel.unlockWeeklyReport(startDate = startDate, unlockType = WeeklyReportUnlockType.INK) + Toast.makeText(context, "잠금을 해제하였습니다.", Toast.LENGTH_SHORT).show() + val action = EgoRoomFragmentDirections.actionMenuEgoRoomToCounselingWeeklyReportDetailFragment(startDate = startDate) + findNavController().navigate(action) + dismiss() } else { Toast.makeText(context, "현재 잉크가 부족합니다!", Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/egobook/app/ui/counseling/view/WordBubbleView.kt b/app/src/main/java/com/egobook/app/ui/counseling/view/WordBubbleView.kt index 1bd280ef..6026d62d 100644 --- a/app/src/main/java/com/egobook/app/ui/counseling/view/WordBubbleView.kt +++ b/app/src/main/java/com/egobook/app/ui/counseling/view/WordBubbleView.kt @@ -24,7 +24,7 @@ class WordBubbleView @JvmOverloads constructor( data class Bubble(val text: String, var radius: Float, val color: Int, var x: Float = 0f, var y: Float = 0f) fun setWords(words: List>) { - val colors = listOf("#FFEBEE", "#E3F2FD", "#FFF3E0", "#E8F5E9", "#F3E5F5") + val colors = listOf("#FFEBEE", "#E3F2FD", "#FFF3E0", "#E8F5E9", "#F3E5F5", "#FCF8E8") bubbles.clear() words.sortedByDescending { it.second }.forEachIndexed { i, pair -> val radius = pair.second * 12f + 50f diff --git a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt index be5b5c8b..bdbc77cc 100644 --- a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt @@ -28,7 +28,7 @@ class HomeViewModel @Inject constructor( private val userAdRepository: UserAdRepository, private val psychologyRepository: UserPsychologyRepository ) : ViewModel() { - private val _uiState = MutableStateFlow(User(id=-1, Level(1), Ink(0))) + private val _uiState = MutableStateFlow(User(id=-1, Level(1),Ink(0), nickname = "")) val uiState: StateFlow = _uiState.asStateFlow() private val _adState = MutableStateFlow(AdInfoDto(0, 0, false, 0, "")) diff --git a/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt b/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt index 640493f6..595ecc84 100644 --- a/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt +++ b/app/src/main/java/com/egobook/app/ui/home/repository/UserDto.kt @@ -14,5 +14,5 @@ data class UserDto( val isFirstAttendanceToday: Boolean, val attendanceRewardInk: Int ) { - fun toDomain(): User = User(id = userId, Level(level), Ink(ink)) + fun toDomain(): User = User(id = userId, Level(level), Ink(ink), nickname = nickname) } diff --git a/app/src/main/java/com/egobook/app/ui/home/user/User.kt b/app/src/main/java/com/egobook/app/ui/home/user/User.kt index 470765c4..417e968a 100644 --- a/app/src/main/java/com/egobook/app/ui/home/user/User.kt +++ b/app/src/main/java/com/egobook/app/ui/home/user/User.kt @@ -1,3 +1,3 @@ package com.egobook.app.ui.home.user -data class User(val id: Int, val level: Level, val ink: Ink) +data class User(val id: Int, val level: Level, val ink: Ink, val nickname: String) diff --git a/app/src/main/java/com/egobook/app/ui/square/adapter/FriendSearchAdapter.kt b/app/src/main/java/com/egobook/app/ui/square/adapter/FriendSearchAdapter.kt new file mode 100644 index 00000000..035d912e --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/square/adapter/FriendSearchAdapter.kt @@ -0,0 +1,67 @@ +package com.egobook.app.ui.square.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.egobook.app.R +import com.egobook.app.databinding.ItemSearchFriendBinding +import com.egobook.app.ui.square.model.friend.SearchUserModel + +/** + * 1. fallback(R.drawable.default_turtle) → null일 때 보여주는 기본 이미지 지정 + */ +class FriendSearchAdapter( + private val onApply: (Long, Int) -> Unit +) : ListAdapter(diffUtil) { + + private var isApplied = false + + inner class FriendSearchViewHolder(val binding: ItemSearchFriendBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: SearchUserModel) = with(binding) { + btnAddFriendSearchResultApply.isEnabled = true + btnAddFriendSearchResultApply.alpha = 1f + btnAddFriendSearchResultApply.text = "신청하기" + tvAddFriendSearchLevel.text = "LV ${item.level}" + tvAddFriendSearchNickname.text = item.nickname + Glide.with(ivAddFriendSearchImage).load(item.profileImageUrl) + .fallback(R.drawable.default_turtle).into(ivAddFriendSearchImage) // 1 + btnAddFriendSearchResultApply.setOnClickListener { onApply(item.userId, bindingAdapterPosition) } + } + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FriendSearchViewHolder { + val binding = ItemSearchFriendBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return FriendSearchViewHolder(binding) + } + + override fun onBindViewHolder(holder: FriendSearchViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + val diffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: SearchUserModel, + newItem: SearchUserModel + ): Boolean { + return oldItem.userId == newItem.userId + } + + override fun areContentsTheSame( + oldItem: SearchUserModel, + newItem: SearchUserModel + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/adapter/MySentLettersAdapter.kt b/app/src/main/java/com/egobook/app/ui/square/adapter/MySentLettersAdapter.kt index c6268d9e..21829683 100644 --- a/app/src/main/java/com/egobook/app/ui/square/adapter/MySentLettersAdapter.kt +++ b/app/src/main/java/com/egobook/app/ui/square/adapter/MySentLettersAdapter.kt @@ -2,10 +2,12 @@ package com.egobook.app.ui.square.adapter import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.egobook.app.databinding.ItemSquareLetterBinding +import com.egobook.app.domain.model.square.letter.LetterStatus import com.egobook.app.ui.square.model.letter.SentLetterModel import java.time.Instant import java.time.ZoneId @@ -30,6 +32,7 @@ class MySentLettersAdapter(private val onClicked: (Long) -> Unit): PagingDataAda inner class MyLetterViewHolder(private val binding: ItemSquareLetterBinding): RecyclerView.ViewHolder(binding.root) { fun bind(item: SentLetterModel) = with(binding) { tvItemSquareLetterDatetime.text = formatDate(createdDateTime = item.createdAt) + ivItemLetter.isVisible = item.status == LetterStatus.REPLIED || item.status == LetterStatus.AI_REPLIED root.setOnClickListener { onClicked(item.letterId) } diff --git a/app/src/main/java/com/egobook/app/ui/square/adapter/SquareAllRepliesAdapter.kt b/app/src/main/java/com/egobook/app/ui/square/adapter/SquareAllRepliesAdapter.kt index 81488a42..c2519e96 100644 --- a/app/src/main/java/com/egobook/app/ui/square/adapter/SquareAllRepliesAdapter.kt +++ b/app/src/main/java/com/egobook/app/ui/square/adapter/SquareAllRepliesAdapter.kt @@ -9,7 +9,7 @@ import androidx.recyclerview.widget.RecyclerView import com.egobook.app.databinding.ItemSquareQuestionReplyBinding import com.egobook.app.ui.square.model.question.UserTodayQuestionAnswerItemModel -class SquareAllRepliesAdapter(private val onReportClick: () -> Unit): PagingDataAdapter(diffUtil) { +class SquareAllRepliesAdapter(private val onReportClick: (Long) -> Unit): PagingDataAdapter(diffUtil) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -30,12 +30,12 @@ class SquareAllRepliesAdapter(private val onReportClick: () -> Unit): PagingData class SquareAllRepliesViewHolder( private val binding: ItemSquareQuestionReplyBinding, - private val onReportClick: () -> Unit + private val onReportClick: (Long) -> Unit ): RecyclerView.ViewHolder(binding.root) { fun bind(item: UserTodayQuestionAnswerItemModel) = with(binding) { tvItemSquareQuestionReplyUserContent.text = item.content ivSquareQuestionReplyReport.setOnClickListener { - onReportClick() + onReportClick(item.answerId) } } } diff --git a/app/src/main/java/com/egobook/app/ui/square/adapter/SquareDeferredLettersAdapter.kt b/app/src/main/java/com/egobook/app/ui/square/adapter/SquareDeferredLettersAdapter.kt new file mode 100644 index 00000000..0a5b0f98 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/square/adapter/SquareDeferredLettersAdapter.kt @@ -0,0 +1,80 @@ +package com.egobook.app.ui.square.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.egobook.app.databinding.ItemSquareDeferredLetterBinding +import com.egobook.app.ui.square.model.letter.DeferredLetterModel +import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +class SquareDeferredLettersAdapter( + private val onClicked: (DeferredLetterModel) -> Unit +): PagingDataAdapter(diffUtil) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SquareDeferredLettersViewHolder { + val binding = ItemSquareDeferredLetterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SquareDeferredLettersViewHolder(binding) + } + + override fun onBindViewHolder( + holder: SquareDeferredLettersViewHolder, + position: Int + ) { + val item = getItem(position) + if(item != null) { + holder.bind(item) + } + } + + inner class SquareDeferredLettersViewHolder( + private val binding: ItemSquareDeferredLetterBinding, + ): RecyclerView.ViewHolder(binding.root) { + fun bind(item: DeferredLetterModel) = with(binding) { + tvSquareDeferredLetterFromLabel.text = item.fromLabel + tvSquareDeferredLetterReplyDeadline.text = getRemainingTime(replyDeadlineAt = item.replyDeadlineAt) + root.setOnClickListener { + onClicked(item) + } + } + } + + companion object { + val diffUtil = object: DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: DeferredLetterModel, + newItem: DeferredLetterModel + ): Boolean { + return oldItem.letterId == newItem.letterId + } + + override fun areContentsTheSame( + oldItem: DeferredLetterModel, + newItem: DeferredLetterModel + ): Boolean { + return oldItem == newItem + } + + } + } +} + +private fun getRemainingTime(replyDeadlineAt: String): String { + val deadline: OffsetDateTime = OffsetDateTime.parse(replyDeadlineAt, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + val now: OffsetDateTime = OffsetDateTime.now(ZoneId.systemDefault()) + val duration: Duration = Duration.between(now, deadline) + return when { + duration.isNegative || duration.isZero -> "답장 유효기한 만료" + duration.toDays() > 0 -> "${duration.toDays()}일 남음" + duration.toHours() > 0 -> "${duration.toHours()}시간 남음" + duration.toMinutes() > 0 -> "${duration.toMinutes()}분 남음" + else -> "잠시 후 만료" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/model/friend/FriendRequestModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/friend/FriendRequestModel.kt index 5eb68d00..765debf8 100644 --- a/app/src/main/java/com/egobook/app/ui/square/model/friend/FriendRequestModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/model/friend/FriendRequestModel.kt @@ -6,12 +6,12 @@ data class FriendRequestModel( val requestId: Long, val userId: Long, val nickname: String, - val requestedAt: String + val level: Long, ) fun FriendRequest.toPresentation(): FriendRequestModel = FriendRequestModel( requestId = requestId, userId = userId, nickname = nickname, - requestedAt = requestedAt + level = level ) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/model/friend/SearchUserModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/friend/SearchUserModel.kt index b2973698..e1f0ccad 100644 --- a/app/src/main/java/com/egobook/app/ui/square/model/friend/SearchUserModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/model/friend/SearchUserModel.kt @@ -6,7 +6,7 @@ data class SearchUserModel( val userId: Long, val nickname: String, val level: Long, - val profileImageUrl: String + val profileImageUrl: String? = null ) fun SearchUser.toPresentation(): SearchUserModel = SearchUserModel( diff --git a/app/src/main/java/com/egobook/app/ui/square/model/letter/ArrivedPendingLetterModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/letter/ArrivedPendingLetterModel.kt index 18987347..fbf204bf 100644 --- a/app/src/main/java/com/egobook/app/ui/square/model/letter/ArrivedPendingLetterModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/model/letter/ArrivedPendingLetterModel.kt @@ -18,8 +18,8 @@ data class ArrivedPendingLetterItemModel( val status: LetterStatus, val mode: LetterMode, val fromLabel: String, + val backgroundColor: LetterBackgroundColor, val content: String, - val letterColor: LetterBackgroundColor, val arrivedAt: String, val replyDeadlineAt: String ): Parcelable @@ -37,5 +37,5 @@ fun ArrivedPendingLetterItem.toPresentation(): ArrivedPendingLetterItemModel = content = content, arrivedAt = arrivedAt, replyDeadlineAt = replyDeadlineAt, - letterColor = letterColor + backgroundColor = backgroundColor ) diff --git a/app/src/main/java/com/egobook/app/ui/square/model/letter/DeferredLetterModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/letter/DeferredLetterModel.kt new file mode 100644 index 00000000..e8603651 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/square/model/letter/DeferredLetterModel.kt @@ -0,0 +1,28 @@ +package com.egobook.app.ui.square.model.letter + +import com.egobook.app.domain.model.square.letter.DeferredLetter +import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode +import com.egobook.app.domain.model.square.letter.LetterStatus + +data class DeferredLetterModel( + val letterId: Long, + val status: LetterStatus, + val mode: LetterMode, + val fromLabel: String, + val backgroundColor: LetterBackgroundColor, + val contentPreview: String, + val arrivedAt: String, + val replyDeadlineAt: String +) + +fun DeferredLetter.toPresentation(): DeferredLetterModel = DeferredLetterModel( + letterId = letterId, + status = status, + mode = mode, + fromLabel = fromLabel, + backgroundColor = backgroundColor, + contentPreview = contentPreview, + arrivedAt = arrivedAt, + replyDeadlineAt = replyDeadlineAt +) diff --git a/app/src/main/java/com/egobook/app/ui/square/model/letter/ReceivedReplyModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/letter/ReceivedReplyModel.kt new file mode 100644 index 00000000..d1f254ca --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/square/model/letter/ReceivedReplyModel.kt @@ -0,0 +1,46 @@ +package com.egobook.app.ui.square.model.letter + +import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode +import com.egobook.app.domain.model.square.letter.ReceivedReplies +import com.egobook.app.domain.model.square.letter.ReceivedReply + + +data class ReceivedRepliesModel( + val content: List, + val page: Int, + val size: Int, + val hasNext: Boolean +) +data class ReceivedReplyModel( + val letterId: Long, + val replyId: Long, + val threadId: Long, + val replyContent: String, + val repliedAt: String, + val isAIGenerated: Boolean, + val isReported: Boolean, + val mode: LetterMode, + val fromLabel: String, + val letterColor: LetterBackgroundColor +) + +fun ReceivedReplies.toPresentation() = ReceivedRepliesModel( + content = content.map { it.toPresentation() }, + page = page, + size = size, + hasNext = hasNext +) + +fun ReceivedReply.toPresentation() = ReceivedReplyModel( + letterId = letterId, + replyId = replyId, + threadId = threadId, + replyContent = replyContent, + repliedAt = repliedAt, + isAIGenerated = isAIGenerated, + isReported = isReported, + mode = mode, + fromLabel = fromLabel, + letterColor = letterColor +) diff --git a/app/src/main/java/com/egobook/app/ui/square/model/letter/ReportLetterModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/letter/ReportContentModel.kt similarity index 60% rename from app/src/main/java/com/egobook/app/ui/square/model/letter/ReportLetterModel.kt rename to app/src/main/java/com/egobook/app/ui/square/model/letter/ReportContentModel.kt index 32a2fc45..6e06e60f 100644 --- a/app/src/main/java/com/egobook/app/ui/square/model/letter/ReportLetterModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/model/letter/ReportContentModel.kt @@ -1,14 +1,14 @@ package com.egobook.app.ui.square.model.letter -import com.egobook.app.domain.model.square.letter.ReportLetter +import com.egobook.app.domain.model.square.letter.ReportContent import com.egobook.app.domain.model.square.letter.ReportLetterType -data class ReportLetterModel( +data class ReportContentModel( val reason: ReportLetterType, val description: String? = null ) -fun ReportLetterModel.toDomain(): ReportLetter = ReportLetter( +fun ReportContentModel.toDomain(): ReportContent = ReportContent( reason = reason, description = description ) diff --git a/app/src/main/java/com/egobook/app/ui/square/model/letter/SentLetterWithReplyModel.kt b/app/src/main/java/com/egobook/app/ui/square/model/letter/SentLetterWithReplyModel.kt index 157c2070..14456c5a 100644 --- a/app/src/main/java/com/egobook/app/ui/square/model/letter/SentLetterWithReplyModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/model/letter/SentLetterWithReplyModel.kt @@ -15,6 +15,7 @@ data class SentLetterWithReplyModel( val backgroundColor: LetterBackgroundColor, val createdAt: String, val arrivedAt: String, + val fromLabel: String, val reply: LetterReplyModel? = null ) @@ -35,6 +36,7 @@ fun SentLetterWithReply.toPresentation() = SentLetterWithReplyModel( backgroundColor = backgroundColor, createdAt = createdAt, arrivedAt = arrivedAt, + fromLabel = fromLabel, reply = reply?.toPresentation() ) diff --git a/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterDialog.kt index 1f5e91cf..152a5973 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterDialog.kt @@ -11,8 +11,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import com.egobook.app.BlurLevel import com.egobook.app.R +import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.DialogArrivedPendingLetterBinding +import com.egobook.app.domain.model.square.ReportOrigin import com.egobook.app.removeScreenBlur import com.egobook.app.ui.square.model.letter.ArrivedPendingLetterItemModel import com.egobook.app.domain.model.square.letter.LetterBackgroundColor @@ -47,7 +50,7 @@ class ArrivedPendingLetterDialog( tvArrivedPendingLetterContent.text = letterInfo.content tvArrivedPendingLetterFromLabel.text = "From ${letterInfo.fromLabel}" cvArrivedPendingLetterContainer.backgroundTintList = - when(letterInfo.letterColor) { + when(letterInfo.backgroundColor) { LetterBackgroundColor.WHITE -> resources.getColorStateList(R.color.letter_bg_beige, null) LetterBackgroundColor.PINK -> resources.getColorStateList(R.color.letter_bg_pink, null) LetterBackgroundColor.GREEN -> resources.getColorStateList(R.color.letter_bg_green, null) @@ -61,7 +64,9 @@ class ArrivedPendingLetterDialog( viewModel.deferReplyLetter(letterId = letterInfo.letterId) } ivArrivedPendingLetterReport.setOnClickListener { - + val dialog = SquareReportDialog(origin = ReportOrigin.LETTER_ARRIVED, letterId = letterInfo.letterId).apply { isCancelable = false } + dialog.show(childFragmentManager, SquareReportDialog.TAG) + applyScreenBlur(BlurLevel.BASE) } btnArrivedPendingLetterGiveUp.setOnClickListener { dismiss() diff --git a/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterPopupDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterPopupDialog.kt index 0d8cb416..a486ba42 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterPopupDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/ArrivedPendingLetterPopupDialog.kt @@ -42,7 +42,7 @@ class ArrivedPendingLetterPopupDialog(private val letterInfo: ArrivedPendingLett } private fun initViews() = with(binding) { - tvArrivedPendingLetterTitle.text = "낯선 고북이의\n편지가 도착했어요" // TODO: 친구인지 익명인지 구분 필요 + tvArrivedPendingLetterTitle.text = "${letterInfo.fromLabel}의\n편지가 도착했어요" tvArrivedPendingLetterReplyDeadline.text = getRemainingTime(replyDeadlineAt = letterInfo.replyDeadlineAt) } diff --git a/app/src/main/java/com/egobook/app/ui/square/view/DetectAbusiveContentSuccessDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/DetectAbusiveContentSuccessDialog.kt index ea20c18d..10355cd2 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/DetectAbusiveContentSuccessDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/DetectAbusiveContentSuccessDialog.kt @@ -33,22 +33,17 @@ class DetectAbusiveContentSuccessDialog(private val status: LetterStatus, privat private fun initViews() = with(binding) { when(status) { LetterStatus.SENT -> { - btnDetectAbusiveSuccess.text = "잉크 1 획득!" // TODO: 조건에 따른 동적 변경 필요한 지 확인 + btnDetectAbusiveSuccess.text = "잉크 1 획득!" } LetterStatus.REPLIED -> { - val rewardList = mutableListOf() + var rewardText: String? = null replyItem?.rewards?.forEach { reward -> - when(reward.kind) { - ReplyReward.INK -> { - rewardList.add("${ReplyReward.INK.label} ${reward.amount}") // 잉크 1 - } - ReplyReward.EMPATHY -> { - rewardList.add("${ReplyReward.EMPATHY.label} ${reward.amount}") // 공감성 1 - } + if(reward.kind == ReplyReward.INK) { + rewardText = "${ReplyReward.INK.label} ${reward.amount} 획득!" + return@forEach } } - val totalRewards = rewardList.joinToString(", ") // 잉크 1, 공감성 1 - btnDetectAbusiveSuccess.text = "$totalRewards 획득!" // 잉크 1, 공감성 1 획득! + btnDetectAbusiveSuccess.text = rewardText ?: "확인" } else -> {} } @@ -56,6 +51,7 @@ class DetectAbusiveContentSuccessDialog(private val status: LetterStatus, privat private fun initListeners() = with(binding) { btnDetectAbusiveSuccess.setOnClickListener { + dismiss() removeScreenBlur() findNavController().popBackStack() } diff --git a/app/src/main/java/com/egobook/app/ui/square/view/FriendAddDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/FriendAddDialog.kt index 5755bbc5..36a80e63 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/FriendAddDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/FriendAddDialog.kt @@ -8,17 +8,18 @@ import android.os.Bundle import android.view.View import android.widget.Toast import android.content.ClipboardManager -import androidx.core.content.getSystemService +import android.view.inputmethod.InputMethodManager import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.bumptech.glide.Glide import com.egobook.app.R import com.egobook.app.databinding.DialogSquareAddFriendBinding import com.egobook.app.removeScreenBlur +import com.egobook.app.ui.square.adapter.FriendSearchAdapter import com.egobook.app.ui.square.model.friend.SearchUserModel import com.egobook.app.ui.square.viewmodel.FriendsViewModel import com.egobook.app.util.UiState @@ -28,7 +29,14 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { private lateinit var binding: DialogSquareAddFriendBinding private val viewModel: FriendsViewModel by activityViewModels() - private var receiverId: Long? = null + + private var lastAppliedPosition: Int? = null + private val adapter by lazy { + FriendSearchAdapter { userId, position -> + lastAppliedPosition = position + viewModel.requestFriendship(receiverId = userId) + } + } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return Dialog(requireContext()).apply { @@ -40,6 +48,7 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { super.onViewCreated(view, savedInstanceState) binding = DialogSquareAddFriendBinding.bind(view) fetchData() + initViews() initListeners() initObservers() } @@ -48,6 +57,10 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { viewModel.getUserId() } + private fun initViews() = with(binding) { + rvAddFriendSearchResultFound.adapter = adapter + } + private fun initListeners() = with(binding) { ivAddFriendBack.setOnClickListener { removeScreenBlur() @@ -57,13 +70,12 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { val keyword = etAddFriendUserKeyword.text.toString() if(keyword.isEmpty()) { Toast.makeText(context, "ID를 입력해주세요!", Toast.LENGTH_SHORT).show() + } else if(keyword == tvAddFriendAccountId.text.toString()) { + Toast.makeText(context, "자신의 ID는 검색할 수 없습니다!", Toast.LENGTH_SHORT).show() } else { - viewModel.searchUser(keyword = keyword) + viewModel.searchUserWithoutRequest(keyword = keyword) } } - btnAddFriendSearchResultApply.setOnClickListener { - viewModel.requestFriendship(receiverId = receiverId ?: -1L) - } btnAddFriendCopyId.setOnClickListener { val copyId = tvAddFriendAccountId.text.toString() val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -82,19 +94,20 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { is UiState.Failure -> {} UiState.Idle -> {} UiState.Loading -> {} - is UiState.Success -> { - val searchUserResult = state.data - if(searchUserResult == null) { + is UiState.Success> -> { + val searchUserList = state.data + if(searchUserList.isEmpty()) { Toast.makeText(context, "검색 결과가 존재하지 않습니다!", Toast.LENGTH_SHORT).show() - clAddFriendSearchResultFound.visibility = View.GONE - tvAddFriendSearchResultEmpty.visibility = View.VISIBLE + rvAddFriendSearchResultFound.isVisible = false + tvAddFriendSearchResultEmptyMain.isVisible = true + tvAddFriendSearchResultEmptySub.isVisible = true } else { - receiverId = searchUserResult.userId - tvAddFriendSearchResultEmpty.visibility = View.GONE - clAddFriendSearchResultFound.visibility = View.VISIBLE - tvAddFriendSearchName.text = searchUserResult.nickname - tvAddFriendSearchLevel.text = "LV ${searchUserResult.level}" - Glide.with(ivAddFriendSearchLevelImage).load(searchUserResult.profileImageUrl).into(ivAddFriendSearchLevelImage) + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(etAddFriendUserKeyword.windowToken, 0) // flags = 0 → "조건 따지지 않고 무조건 키보드를 내린다" + adapter.submitList(searchUserList.toMutableList()) + tvAddFriendSearchResultEmptyMain.isVisible = false + tvAddFriendSearchResultEmptySub.isVisible = false + rvAddFriendSearchResultFound.isVisible = true } } } @@ -108,9 +121,15 @@ class FriendAddDialog: DialogFragment(R.layout.dialog_square_add_friend) { UiState.Loading -> {} is UiState.Success -> { Toast.makeText(context, "친구 신청이 완료되었습니다", Toast.LENGTH_SHORT).show() - btnAddFriendSearchResultApply.isEnabled = false - btnAddFriendSearchResultApply.alpha = 0.5f - btnAddFriendSearchResultApply.text = "신청완료" + lastAppliedPosition?.let { + val viewHolder = rvAddFriendSearchResultFound.findViewHolderForAdapterPosition(it) as? FriendSearchAdapter.FriendSearchViewHolder + viewHolder?.binding?.apply { + btnAddFriendSearchResultApply.isEnabled = false + btnAddFriendSearchResultApply.alpha = 0.5f + btnAddFriendSearchResultApply.text = "신청완료" + } + } + lastAppliedPosition = null } } } diff --git a/app/src/main/java/com/egobook/app/ui/square/view/FriendsPendingListFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/FriendsPendingListFragment.kt index 6e13865f..49ad11fa 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/FriendsPendingListFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/FriendsPendingListFragment.kt @@ -1,9 +1,17 @@ package com.egobook.app.ui.square.view import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.MetricAffectingSpan +import android.util.TypedValue import androidx.fragment.app.Fragment import android.view.View +import android.widget.TextView import android.widget.Toast +import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle @@ -26,10 +34,16 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentFriendsPendingListBinding.bind(view) + initViews() fetchData() initObservers() } + private fun initViews() = with(binding) { + changePlaceholderTextStyle(text = "받은 친구 신청이 없어요\n신청이 도착하면 바로 알려드릴게요", view = tvFriendsPendingListReceivedPlaceholder) + changePlaceholderTextStyle(text = "보낸 친구 신청이 없어요\n친구의 ID를 입력해 신청을 보내보세요", view = tvFriendsPendingListSentPlaceholder) + } + private fun fetchData() { viewModel.fetchIncomingFriendRequestList() viewModel.fetchOutgoingFriendRequestList() @@ -49,9 +63,9 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li tvFriendsPendingListReceivedNum.text = friendRequestList.size.toString() if(friendRequestList.isEmpty()) { llFriendsPendingListReceived.removeAllViews() - tvFriendsPendingListReceivedEmpty.isVisible = true + tvFriendsPendingListReceivedPlaceholder.isVisible = true } else { - tvFriendsPendingListReceivedEmpty.isVisible = false + tvFriendsPendingListReceivedPlaceholder.isVisible = false llFriendsPendingListReceived.removeAllViews() // 기존에 추가되어 있던 뷰들 모두 제거 friendRequestList.forEach { friendRequest -> val itemView = layoutInflater.inflate( @@ -63,6 +77,7 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li } val itemBinding = ItemSquareFriendPendingReceivedListBinding.bind(itemView) itemBinding.tvItemFriendPendingListName.text = friendRequest.nickname + itemBinding.tvItemFriendPendingListLevel.text = "LV ${friendRequest.level}" itemBinding.btnItemSquareFriendPendingListDeny.setOnClickListener { viewModel.rejectFriendRequest(requestId = friendRequest.requestId) } @@ -86,9 +101,9 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li val friendRequestList = state.data if(friendRequestList.isEmpty()) { llFriendsPendingListSent.removeAllViews() - tvFriendsPendingListSentEmpty.isVisible = true + tvFriendsPendingListSentPlaceholder.isVisible = true } else { - tvFriendsPendingListSentEmpty.isVisible = false + tvFriendsPendingListSentPlaceholder.isVisible = false llFriendsPendingListSent.removeAllViews() // 기존에 추가되어 있던 뷰들 모두 제거 friendRequestList.forEach { friendRequest -> val itemView = layoutInflater.inflate( @@ -100,6 +115,7 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li } val itemBinding = ItemSquareFriendPendingSentListBinding.bind(itemView) itemBinding.tvItemFriendPendingSentListName.text = friendRequest.nickname + itemBinding.tvItemFriendPendingSentListLevel.text = "LV ${friendRequest.level}" itemBinding.btnItemSquareFriendPendingSentListCancel.setOnClickListener { viewModel.cancelFriendRequest(requestId = friendRequest.requestId) } @@ -134,7 +150,6 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li UiState.Idle -> {} UiState.Loading -> {} is UiState.Success -> { -// viewModel.fetchIncomingFriendRequestList() 이 방법은 어떨까? (실제 연결 시 고려해보기) Toast.makeText(context, "요청된 친구 신청이 수락되었습니다.", Toast.LENGTH_SHORT).show() val requestId = state.data val viewToRemove = llFriendsPendingListReceived.findViewWithTag(requestId) @@ -151,7 +166,6 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li UiState.Idle -> {} UiState.Loading -> {} is UiState.Success -> { -// viewModel.fetchOutgoingFriendRequestList() Toast.makeText(context, "요청된 친구 신청이 취소되었습니다.", Toast.LENGTH_SHORT).show() val requestId = state.data val viewToRemove = llFriendsPendingListSent.findViewWithTag(requestId) @@ -163,4 +177,35 @@ class FriendsPendingListFragment : Fragment(R.layout.fragment_friends_pending_li } } } + + private fun changePlaceholderTextStyle(text: String, view: TextView) { + val spannable = SpannableStringBuilder(text) + val newLineIndex = text.indexOf("\n") + val start = newLineIndex + 1 + val end = text.length + val sizeInPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14f, resources.displayMetrics).toInt() + spannable.setSpan( + AbsoluteSizeSpan(sizeInPx), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + val font = ResourcesCompat.getFont(requireContext(), R.font.arita_medium) + font?.let { typeface -> + spannable.setSpan( + object : MetricAffectingSpan() { + override fun updateDrawState(ds: TextPaint) { + ds.typeface = typeface + } + override fun updateMeasureState(paint: TextPaint) { + paint.typeface = typeface + } + }, + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + view.text = spannable + } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/view/LetterReplyFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/LetterReplyFragment.kt index 33315283..f5ed87e2 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/LetterReplyFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/LetterReplyFragment.kt @@ -20,6 +20,7 @@ import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentLetterReplyBinding import com.egobook.app.databinding.LayoutLetterTooltipPopupBinding import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode import com.egobook.app.domain.model.square.letter.LetterStatus import com.egobook.app.removeScreenBlur import com.egobook.app.ui.square.model.letter.AbusiveContentModel @@ -49,11 +50,16 @@ class LetterReplyFragment : Fragment(R.layout.fragment_letter_reply) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLetterReplyBinding.bind(view) + fetchData() initViews() initListeners() initObservers() } + private fun fetchData() { + viewModel.getUserInfo() + } + private fun initViews() = with(binding) { tvLetterReplyReceivedContent.text = letterItem.content cvLetterReplyColorBeige.isSelected = true @@ -112,7 +118,9 @@ class LetterReplyFragment : Fragment(R.layout.fragment_letter_reply) { } private fun showTooltipPopup(anchorView: View) { - val popupBinding = LayoutLetterTooltipPopupBinding.inflate(layoutInflater) + val popupBinding = LayoutLetterTooltipPopupBinding.inflate(layoutInflater).apply { + tvTooltipContent.text = if(letterItem.mode == LetterMode.RANDOM) "익명으로 상대에게 전달돼요\n비하나 과도한 비판 대신,\n공감부터 시작하는\n따뜻한 말을 보내주세요!" else "친구에게 편지가 전달돼요\n비하나 과도한 비판 대신,\n공감부터 시작하는\n따뜻한 말을 보내주세요!" + } val popupWindow = PopupWindow( popupBinding.root, LinearLayout.LayoutParams.WRAP_CONTENT, @@ -134,6 +142,28 @@ class LetterReplyFragment : Fragment(R.layout.fragment_letter_reply) { private fun initObservers() = with(binding) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.userInfo.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + val userNickname = state.data.nickname + when(letterItem.mode) { + LetterMode.FRIEND -> { + tvLetterReplyReceiver.text = "To ${letterItem.fromLabel}" + tvLetterReplySender.text = "From $userNickname" + } + LetterMode.RANDOM -> { + tvLetterReplyReceiver.text = "To 낯선 고북이" + tvLetterReplySender.text = "From 또다른 고북이" + } + } + } + } + } + } launch { viewModel.detectAbusiveContentResult.collect { state -> when (state) { diff --git a/app/src/main/java/com/egobook/app/ui/square/view/LetterSendDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/LetterSendDialog.kt index 4de761be..3865278a 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/LetterSendDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/LetterSendDialog.kt @@ -4,7 +4,7 @@ import android.app.Dialog import android.graphics.Color import android.os.Bundle import android.view.View -import android.widget.Toast + import android.widget.Toast import androidx.core.graphics.drawable.toDrawable import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels @@ -64,11 +64,9 @@ class LetterSendDialog(private val mode: LetterMode, private val friendInfo: Fri removeScreenBlur() dismiss() } - btnLetterSendApply.setOnClickListener { -// viewModel.detectAbusiveContent(text = letterContent) - Toast.makeText(context, "서버 점검중입니다.", Toast.LENGTH_SHORT).show() - removeScreenBlur() - dismiss() + btnLetterSend.setOnClickListener { + root.alpha = 0.0f + viewModel.detectAbusiveContent(text = letterContent) } } @@ -81,7 +79,6 @@ class LetterSendDialog(private val mode: LetterMode, private val friendInfo: Fri is UiState.Failure -> {} UiState.Idle -> {} UiState.Loading -> { - dismiss() // 얘가 중요하다 showLoadingDialog() } is UiState.Success -> { @@ -93,6 +90,7 @@ class LetterSendDialog(private val mode: LetterMode, private val friendInfo: Fri } dialog.show(parentFragmentManager, DetectAbusiveContentFailureDialog.TAG) applyScreenBlur(BlurLevel.BASE) + dismiss() } else { val letter = SendLetterModel( mode = mode, @@ -118,6 +116,7 @@ class LetterSendDialog(private val mode: LetterMode, private val friendInfo: Fri } dialog.show(parentFragmentManager, DetectAbusiveContentSuccessDialog.TAG) applyScreenBlur(BlurLevel.BASE) + dismiss() } } } @@ -145,4 +144,4 @@ class LetterSendDialog(private val mode: LetterMode, private val friendInfo: Fri companion object { const val TAG = "LetterSendDialog" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/view/LetterWriteFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/LetterWriteFragment.kt index 8fb73212..91afc050 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/LetterWriteFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/LetterWriteFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View +import android.widget.LinearLayout import android.widget.PopupWindow import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible @@ -20,11 +21,13 @@ import com.egobook.app.BlurLevel import com.egobook.app.R import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentLetterWriteBinding +import com.egobook.app.databinding.LayoutLetterTooltipPopupBinding import com.egobook.app.databinding.LayoutPopupFriendListBinding import com.egobook.app.domain.model.square.letter.LetterMode import com.egobook.app.ui.square.adapter.FriendPopupListAdapter import com.egobook.app.ui.square.model.friend.FriendListModel import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.ui.home.user.User import com.egobook.app.ui.square.viewmodel.LetterViewModel import com.egobook.app.util.UiState import dagger.hilt.android.AndroidEntryPoint @@ -42,6 +45,9 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { private lateinit var friendList: FriendListModel private var letterColor: LetterBackgroundColor = LetterBackgroundColor.WHITE + private var userNickname: String? = null + private var letterMode: LetterMode = LetterMode.RANDOM + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLetterWriteBinding.bind(view) @@ -53,6 +59,7 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { private fun fetchData() { viewModel.getFriendList() + viewModel.getUserInfo() } private fun initViews() = with(binding) { @@ -105,6 +112,23 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { dialog.show(childFragmentManager, LetterSendDialog.TAG) tvLetterWriteReceiver.text = "To 낯선 고북이" tvLetterWriteSender.text = "From 또다른 고북이" + letterMode = LetterMode.RANDOM + } + ivLetterWriteTooltip.setOnClickListener { + val popupBinding = LayoutLetterTooltipPopupBinding.inflate(layoutInflater) + popupBinding.tvTooltipContent.text = if(letterMode == LetterMode.RANDOM) "익명으로 상대에게 전달돼요\n비하나 과도한 비판 대신,\n공감부터 시작하는\n따뜻한 말을 보내주세요!" else "친구에게 편지가 전달돼요\n비하나 과도한 비판 대신,\n공감부터 시작하는\n따뜻한 말을 보내주세요!" + val popupWindow = PopupWindow( + popupBinding.root, + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + true + ) + popupBinding.root.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED + ) + val popupWidth = popupBinding.root.measuredWidth + popupWindow.showAsDropDown(ivLetterWriteTooltip, 0 - popupWidth + ivLetterWriteTooltip.width, 0) } } @@ -121,14 +145,14 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { */ private fun showFriendListPopup(anchorView: View) = with(binding) { val popupBinding = LayoutPopupFriendListBinding.inflate(layoutInflater) - val popupHeight = (200*resources.displayMetrics.density).toInt() val popupWindow = PopupWindow( - popupBinding.root, btnLetterSendFriend.width, popupHeight, true + popupBinding.root, btnLetterSendFriend.width, LinearLayout.LayoutParams.WRAP_CONTENT, true ) with(popupBinding.rvPopupFriendList) { adapter = FriendPopupListAdapter { friendInfo -> tvLetterWriteReceiver.text = "To ${friendInfo.name}" - tvLetterWriteSender.text = "From 로그인한 유저" // TODO: 나중에 유저 정보 받아오기 + tvLetterWriteSender.text = "From $userNickname" + letterMode = LetterMode.FRIEND popupWindow.dismiss() val dialog = LetterSendDialog(mode = LetterMode.FRIEND, friendInfo = friendInfo, letterContent = etLetterWriteContent.text.toString(), letterColor = letterColor).apply { isCancelable = false } dialog.show(childFragmentManager, LetterSendDialog.TAG) @@ -154,6 +178,11 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { }) } (popupBinding.rvPopupFriendList.adapter as FriendPopupListAdapter).submitList(friendList.friends) + popupBinding.root.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED + ) + val popupHeight = popupBinding.root.measuredHeight popupWindow.showAsDropDown( anchorView, 0, @@ -164,26 +193,40 @@ class LetterWriteFragment : Fragment(R.layout.fragment_letter_write) { private fun initObservers() = with(binding) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.friendList.collect { state -> - when(state) { - is UiState.Failure -> {} - UiState.Idle -> {} - UiState.Loading -> {} - is UiState.Success -> { - val friendList = state.data - if(friendList.friends.isEmpty()) { - btnLetterSendFriend.isVisible = false - val params = btnLetterSendAnonymous.layoutParams as ConstraintLayout.LayoutParams - params.startToEnd = ConstraintLayout.LayoutParams.UNSET - params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET - params.topToTop = ConstraintLayout.LayoutParams.UNSET - params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID - params.marginStart = (16 * resources.displayMetrics.density).toInt() - params.topToBottom = cvLetterContainer.id - params.topMargin = (28 * resources.displayMetrics.density).toInt() - btnLetterSendAnonymous.layoutParams = params - } else { - this@LetterWriteFragment.friendList = friendList + launch { + viewModel.friendList.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + val friendList = state.data + if(friendList.friends.isEmpty()) { + btnLetterSendFriend.isVisible = false + val params = btnLetterSendAnonymous.layoutParams as ConstraintLayout.LayoutParams + params.startToEnd = ConstraintLayout.LayoutParams.UNSET + params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET + params.topToTop = ConstraintLayout.LayoutParams.UNSET + params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + params.marginStart = (16 * resources.displayMetrics.density).toInt() + params.topToBottom = cvLetterContainer.id + params.topMargin = (28 * resources.displayMetrics.density).toInt() + btnLetterSendAnonymous.layoutParams = params + } else { + this@LetterWriteFragment.friendList = friendList + } + } + } + } + } + launch { + viewModel.userInfo.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + userNickname = state.data.nickname } } } diff --git a/app/src/main/java/com/egobook/app/ui/square/view/MyLetterDetailFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/MyLetterDetailFragment.kt index 4394d63e..46812076 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/MyLetterDetailFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/MyLetterDetailFragment.kt @@ -16,10 +16,14 @@ import com.egobook.app.BlurLevel import com.egobook.app.R import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentMyLetterDetailBinding +import com.egobook.app.domain.model.square.ReportOrigin import com.egobook.app.domain.model.square.letter.LetterBackgroundColor +import com.egobook.app.domain.model.square.letter.LetterMode +import com.egobook.app.ui.square.model.letter.ReceivedReplyModel import com.egobook.app.ui.square.model.letter.SentLetterWithReplyModel import com.egobook.app.ui.square.viewmodel.LetterViewModel import com.egobook.app.util.UiState +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.time.Instant import java.time.ZoneId @@ -34,6 +38,8 @@ class MyLetterDetailFragment : Fragment(R.layout.fragment_my_letter_detail) { private var replyId: Long? = null private var threadId: Long? = null private var isReplyReported: Boolean? = null + + private var myNickName: String? = null private val viewModel: LetterViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -71,9 +77,8 @@ class MyLetterDetailFragment : Fragment(R.layout.fragment_my_letter_detail) { } } ivMyLetterDetailReport.setOnClickListener { - if(isReplyReported == true) Toast.makeText(context, "답장이 이미 신고되었습니다.", Toast.LENGTH_SHORT).show() - else { - val dialog = SquareReportDialog(letterId = letterId, replyId = replyId ?: -1L).apply { isCancelable = false } + if(isReplyReported == true) Toast.makeText(context, "답장이 이미 신고되었습니다.", Toast.LENGTH_SHORT).show() else { + val dialog = SquareReportDialog(origin = ReportOrigin.LETTER_REPLY, letterId = letterId, replyId = replyId).apply { isCancelable = false } dialog.show(childFragmentManager, SquareReportDialog.TAG) applyScreenBlur(BlurLevel.BASE) } @@ -106,19 +111,8 @@ class MyLetterDetailFragment : Fragment(R.layout.fragment_my_letter_detail) { tvMyLetterDetailSentContent.text = data.sentContent cvMyLetterDetailSentContent.backgroundTintList = resources.getColorStateList(sentCardBackgroundColor, null) if(data.reply != null) { - replyId = data.reply.replyId - isReplyReported = data.reply.isReported - tvMyLetterDetailRepliedContent.text = data.reply.replyContent - if(data.reply.isAIGenerated) { - tvMyLetterDetailReceiver.text = "To 사용자 닉네임" // TODO: 실제 유저 닉네임으로 변경하기 - tvMyLetterDetailSender.text = "FROM. 당신의 고북" - tvMyLetterDetailAiGeneratedDescription.isVisible = true - val params = tvMyLetterDetailSender.layoutParams as ConstraintLayout.LayoutParams - params.topMargin = (72 * resources.displayMetrics.density).toInt() - } else { - // TODO: 친구/익명 유무에 따라 보낸이 받는이 이름 변경하기 - tvMyLetterDetailAiGeneratedDescription.isVisible = false - } + myNickName = data.fromLabel + viewModel.getReceivedReplyById(replyId = data.reply.replyId) } else { llMyLetterDetailReplied.isVisible = false } @@ -126,6 +120,50 @@ class MyLetterDetailFragment : Fragment(R.layout.fragment_my_letter_detail) { } } } + launch { + viewModel.receivedReplies.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + val replyItem = state.data + + replyId = replyItem.replyId + isReplyReported = replyItem.isReported + tvMyLetterDetailRepliedContent.text = replyItem.replyContent + cvMyLetterDetailReceivedContent.backgroundTintList = when(replyItem.letterColor) { + LetterBackgroundColor.WHITE -> resources.getColorStateList(R.color.letter_bg_beige, null) + LetterBackgroundColor.PINK -> resources.getColorStateList(R.color.letter_bg_pink, null) + LetterBackgroundColor.GREEN -> resources.getColorStateList(R.color.letter_bg_green, null) + LetterBackgroundColor.BLUE -> resources.getColorStateList(R.color.letter_bg_blue, null) + LetterBackgroundColor.PURPLE -> resources.getColorStateList(R.color.letter_bg_purple, null) + } + + if(replyItem.isAIGenerated) { + tvMyLetterDetailReceiver.text = "To $myNickName" + tvMyLetterDetailSender.text = "FROM. 당신의 고북" + tvMyLetterDetailAiGeneratedDescription.isVisible = true + val params = tvMyLetterDetailSender.layoutParams as ConstraintLayout.LayoutParams + params.topMargin = (72 * resources.displayMetrics.density).toInt() + } else { + tvMyLetterDetailAiGeneratedDescription.isVisible = false + when(replyItem.mode) { + LetterMode.RANDOM -> { + tvMyLetterDetailReceiver.text = "To 낯선 고북이" + tvMyLetterDetailSender.text = "From 또 다른 고북이" + } + LetterMode.FRIEND -> { + tvMyLetterDetailReceiver.text = "To $myNickName" + tvMyLetterDetailSender.text = "From ${replyItem.fromLabel}" + } + } + } + } + } + } + } + launch { viewModel.deleteLetterThreadResult.collect { state -> when(state) { diff --git a/app/src/main/java/com/egobook/app/ui/square/view/MyRepliesHistoryFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/MyRepliesHistoryFragment.kt index 8bd03ab6..0ae9dc61 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/MyRepliesHistoryFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/MyRepliesHistoryFragment.kt @@ -3,12 +3,14 @@ package com.egobook.app.ui.square.view import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import androidx.paging.LoadState import com.egobook.app.R import com.egobook.app.databinding.FragmentMyRepliesHistoryBinding import com.egobook.app.ui.square.adapter.MyRepliesHistoryAdapter @@ -63,6 +65,15 @@ class MyRepliesHistoryFragment : Fragment(R.layout.fragment_my_replies_history) } } } + launch { + adapter.loadStateFlow.collectLatest { loadStates -> + val isListEmpty = loadStates.refresh is LoadState.NotLoading && adapter.itemCount == 0 + tvSquareMyRepliesHistoryPlaceholderMain.isVisible = isListEmpty + tvSquareMyRepliesHistoryPlaceholderSub.isVisible = isListEmpty + rvMyRepliesHistory.isVisible = !isListEmpty + btnMyRepliesHistoryScrollUp.isVisible = !isListEmpty + } + } launch { viewModel.deleteMyQuestionAnswerResult.collect { state -> when(state) { diff --git a/app/src/main/java/com/egobook/app/ui/square/view/PremiumLetterBuyDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/PremiumLetterBuyDialog.kt index 6f0dbf95..f5c54e31 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/PremiumLetterBuyDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/PremiumLetterBuyDialog.kt @@ -79,8 +79,8 @@ class PremiumLetterBuyDialog(private val letterColor: LetterBackgroundColor) : is UiState.Success -> { val userInfo = state.data val currentInk = userInfo.ink.value - if (currentInk >= INK_PRICE) { - Toast.makeText(context, "구매 가능한 상태지만, 서버 점검 중입니다!", Toast.LENGTH_SHORT).show() + if (currentInk >= LETTER_PRICE) { + Toast.makeText(context, "서비스 점검 중입니다!", Toast.LENGTH_SHORT).show() // TODO: 편지 잉크 구매 API 연동하기 } else { Toast.makeText(context, "현재 잉크가 부족합니다!", Toast.LENGTH_SHORT).show() @@ -95,5 +95,6 @@ class PremiumLetterBuyDialog(private val letterColor: LetterBackgroundColor) : companion object { const val TAG = "PremiumLetterBuyDialog" + private const val LETTER_PRICE = 500 } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/view/SquareAllRepliesFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/SquareAllRepliesFragment.kt index 9839d70c..df7979a4 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/SquareAllRepliesFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/SquareAllRepliesFragment.kt @@ -1,5 +1,7 @@ package com.egobook.app.ui.square.view +import android.graphics.Canvas +import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.view.View import androidx.core.view.isVisible @@ -11,10 +13,12 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.paging.LoadState +import androidx.recyclerview.widget.RecyclerView import com.egobook.app.BlurLevel import com.egobook.app.R import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentSquareAllRepliesBinding +import com.egobook.app.domain.model.square.ReportOrigin import com.egobook.app.ui.square.adapter.SquareAllRepliesAdapter import com.egobook.app.ui.square.viewmodel.QuestionViewModel import kotlinx.coroutines.flow.collectLatest @@ -23,14 +27,24 @@ import kotlinx.coroutines.launch class SquareAllRepliesFragment : Fragment(R.layout.fragment_square_all_replies) { private lateinit var binding: FragmentSquareAllRepliesBinding private val adapter by lazy { - SquareAllRepliesAdapter { - + SquareAllRepliesAdapter { answerId -> + val dialog = SquareReportDialog(origin = ReportOrigin.TODAY_QUESTION_ANSWER, answerId = answerId).apply { isCancelable = false } + dialog.show(childFragmentManager, SquareReportDialog.TAG) + applyScreenBlur(BlurLevel.BASE) } } private val viewModel: QuestionViewModel by activityViewModels() private val args: SquareAllRepliesFragmentArgs by navArgs() + private val dividerDrawable by lazy { + GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + color = resources.getColorStateList(R.color.green_secondary, null) + setSize(0, (1*resources.displayMetrics.density).toInt()) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentSquareAllRepliesBinding.bind(view) @@ -47,6 +61,19 @@ class SquareAllRepliesFragment : Fragment(R.layout.fragment_square_all_replies) private fun initViews() = with(binding) { tvSquareAllRepliesTodayQuestion.text = "Q. \n${args.todayQuestionContent}" rvSquareAllReplies.adapter = adapter + rvSquareAllReplies.addItemDecoration(object: RecyclerView.ItemDecoration() { + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val left = parent.paddingLeft + val right = parent.width - parent.paddingRight + for(i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + val top = child.bottom + val bottom = top + dividerDrawable.intrinsicHeight + dividerDrawable.setBounds(left, top, right, bottom) + dividerDrawable.draw(c) + } + } + }) } private fun initListeners() = with(binding) { diff --git a/app/src/main/java/com/egobook/app/ui/square/view/SquareFragment.kt b/app/src/main/java/com/egobook/app/ui/square/view/SquareFragment.kt index 4f81fe21..c2958ff4 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/SquareFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/SquareFragment.kt @@ -1,5 +1,7 @@ package com.egobook.app.ui.square.view +import android.graphics.Canvas +import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -7,6 +9,8 @@ import android.view.View import android.widget.LinearLayout import android.widget.PopupWindow import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -15,6 +19,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.egobook.app.BlurLevel import com.egobook.app.R import com.egobook.app.applyScreenBlur @@ -22,7 +29,9 @@ import com.egobook.app.databinding.FragmentSquareBinding import com.egobook.app.databinding.LayoutPopupVisibilityTypeBinding import com.egobook.app.domain.model.square.question.AnswerVisibility import com.egobook.app.ui.square.adapter.MySentLettersAdapter +import com.egobook.app.ui.square.adapter.SquareDeferredLettersAdapter import com.egobook.app.ui.square.adapter.TodayQuestionFriendRepliesAdapter +import com.egobook.app.ui.square.model.letter.ArrivedPendingLetterItemModel import com.egobook.app.ui.square.model.letter.ArrivedPendingLetterModel import com.egobook.app.ui.square.model.question.SubmitStatus import com.egobook.app.ui.square.model.question.TodayAnswerModel @@ -32,13 +41,32 @@ import com.egobook.app.ui.square.viewmodel.QuestionViewModel import com.egobook.app.util.UiState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.ZonedDateTime class SquareFragment : Fragment(R.layout.fragment_square) { private lateinit var binding: FragmentSquareBinding private val questionViewModel: QuestionViewModel by activityViewModels() private val letterViewModel: LetterViewModel by activityViewModels() - private var visibilityType = AnswerVisibility.PUBLIC // 오늘의 질문 답변 제출할 때 필요한 변수 + private var visibilityType = AnswerVisibility.PUBLIC + + private val deferredLettersAdapter by lazy { + SquareDeferredLettersAdapter { item -> + val letterInfo = ArrivedPendingLetterItemModel( + letterId = item.letterId, + status = item.status, + mode = item.mode, + fromLabel = item.fromLabel, + backgroundColor = item.backgroundColor, + content = item.contentPreview, + arrivedAt = item.arrivedAt, + replyDeadlineAt = item.replyDeadlineAt + ) + val dialog = ArrivedPendingLetterDialog(letterInfo = letterInfo).apply { isCancelable = false } + dialog.show(childFragmentManager, ArrivedPendingLetterDialog.TAG) + } + } private val todayQuestionAdapter by lazy { TodayQuestionFriendRepliesAdapter() @@ -54,6 +82,13 @@ class SquareFragment : Fragment(R.layout.fragment_square) { private var todayQuestionContent: String? = null private var submitButtonStatus: SubmitStatus = SubmitStatus.CREATE + private val dividerDrawable by lazy { + GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + color = resources.getColorStateList(R.color.green_secondary, null) + setSize(0, (1*resources.displayMetrics.density).toInt()) + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -67,13 +102,33 @@ class SquareFragment : Fragment(R.layout.fragment_square) { private fun fetchData() { questionViewModel.getTodayQuestion() questionViewModel.getTodayFriendsReplies(size = 3) + letterViewModel.getDeferredLetters(size = 10) letterViewModel.getArrivedPendingLetter() letterViewModel.getSentLetters(size = 4) } private fun initViews() = with(binding) { + rvSquareDeferredLetters.adapter = deferredLettersAdapter rvSquareTodayQuestionFriendReply.adapter = todayQuestionAdapter + rvSquareTodayQuestionFriendReply.addItemDecoration(object: RecyclerView.ItemDecoration() { + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val left = parent.paddingLeft + val right = parent.width - parent.paddingRight + for(i in 0 until parent.childCount - 1) { + val child = parent.getChildAt(i) + val top = child.bottom + val bottom = top + dividerDrawable.intrinsicHeight + dividerDrawable.setBounds(left, top, right, bottom) + dividerDrawable.draw(c) + } + } + }) rvSquareSentLetter.adapter = sentLetterAdapter + val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL) + ContextCompat.getDrawable(requireContext(), R.drawable.divider_praise)?.let { + dividerItemDecoration.setDrawable(it) + } + rvSquareSentLetter.addItemDecoration(dividerItemDecoration) } private fun initListeners() = with(binding) { @@ -304,6 +359,13 @@ class SquareFragment : Fragment(R.layout.fragment_square) { } } } + launch { + letterViewModel.deferredLetters.collectLatest { pagingData -> + if(pagingData != null) { + deferredLettersAdapter.submitData(lifecycle, pagingData) + } + } + } launch { letterViewModel.sentLetters.collectLatest { pagingData -> if(pagingData != null) { @@ -313,10 +375,34 @@ class SquareFragment : Fragment(R.layout.fragment_square) { } launch { sentLetterAdapter.loadStateFlow.collectLatest { loadStates -> - val isRefreshing = loadStates.refresh is LoadState.Loading + val isListEmpty = loadStates.refresh is LoadState.NotLoading && sentLetterAdapter.itemCount == 0 - tvSquareSentLetterPlaceholder.isVisible = isListEmpty - rvSquareSentLetter.visibility = if(isListEmpty) View.INVISIBLE else View.VISIBLE + + if(!isListEmpty) { + val hasSentLetterToday = (0 until sentLetterAdapter.itemCount).any { index -> + val item = sentLetterAdapter.peek(index) + val createdDate = ZonedDateTime.parse(item?.createdAt).toLocalDate() + val today = LocalDate.now() + createdDate.isEqual(today) + } + cvSquareWriteLetter.isEnabled = !hasSentLetterToday + cvSquareWriteLetter.alpha = if(hasSentLetterToday) 0.4f else 1f + } + + tvSquareSentLetterPlaceholderMain.isVisible = isListEmpty + tvSquareSentLetterPlaceholderSub.isVisible = isListEmpty + rvSquareSentLetter.isVisible = !isListEmpty + + val layoutParams = llSquareTodayQuestionHeader.layoutParams as ConstraintLayout.LayoutParams + if(!isListEmpty) { + layoutParams.topToBottom = ConstraintLayout.LayoutParams.UNSET + layoutParams.topToBottom = rvSquareSentLetter.id + layoutParams.topMargin = (24 * resources.displayMetrics.density).toInt() + } else { + layoutParams.topToBottom = ConstraintLayout.LayoutParams.UNSET + layoutParams.topToBottom = tvSquareSentLetterPlaceholderSub.id + layoutParams.topMargin = (48 * resources.displayMetrics.density).toInt() + } } } } diff --git a/app/src/main/java/com/egobook/app/ui/square/view/SquareReportDialog.kt b/app/src/main/java/com/egobook/app/ui/square/view/SquareReportDialog.kt index 598afda0..bfef2a7c 100644 --- a/app/src/main/java/com/egobook/app/ui/square/view/SquareReportDialog.kt +++ b/app/src/main/java/com/egobook/app/ui/square/view/SquareReportDialog.kt @@ -16,17 +16,20 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.egobook.app.R import com.egobook.app.databinding.DialogSquareReportBinding +import com.egobook.app.domain.model.square.ReportOrigin import com.egobook.app.domain.model.square.letter.ReportLetterType import com.egobook.app.removeScreenBlur -import com.egobook.app.ui.square.model.letter.ReportLetterModel +import com.egobook.app.ui.square.model.letter.ReportContentModel import com.egobook.app.ui.square.viewmodel.LetterViewModel +import com.egobook.app.ui.square.viewmodel.QuestionViewModel import com.egobook.app.util.UiState import kotlinx.coroutines.launch -class SquareReportDialog(private val letterId: Long, private val replyId: Long) : +class SquareReportDialog(private val origin: ReportOrigin, private val letterId: Long? = null, private val replyId: Long? = null, private val answerId: Long? = null) : DialogFragment(R.layout.dialog_square_report) { private lateinit var binding: DialogSquareReportBinding - private val viewModel: LetterViewModel by activityViewModels() + private val letterViewModel: LetterViewModel by activityViewModels() + private val questionViewModel: QuestionViewModel by activityViewModels() private val reportReasonsWithoutEtc by lazy { listOf( binding.tvSquareReportAbuse, @@ -57,7 +60,6 @@ class SquareReportDialog(private val letterId: Long, private val replyId: Long) reportReasonsWithoutEtc.forEach { content -> content.setOnClickListener { reportReasonsWithoutEtc.forEach { it.isSelected = false } - tvSquareReportEtcNotClicked.isSelected = false llSquareReportEtcClicked.isVisible = false tvSquareReportEtcNotClicked.isVisible = true content.isSelected = true @@ -71,7 +73,6 @@ class SquareReportDialog(private val letterId: Long, private val replyId: Long) } tvSquareReportEtcNotClicked.setOnClickListener { reportReasonsWithoutEtc.forEach { it.isSelected = false } - it.isSelected = true llSquareReportEtcClicked.isVisible = true tvSquareReportEtcNotClicked.isVisible = false btnSquareReportSubmit.isEnabled = !etSquareReportEtcReason.text.isNullOrBlank() @@ -96,26 +97,74 @@ class SquareReportDialog(private val letterId: Long, private val replyId: Long) } }) btnSquareReportSubmit.setOnClickListener { - viewModel.reportRepliedLetter(replyId = replyId, reportLetter = ReportLetterModel( - reason = reportType, - description = if(reportType == ReportLetterType.OTHER) etSquareReportEtcReason.text.toString() else null - )) + when(origin) { + ReportOrigin.LETTER_ARRIVED -> { + letterViewModel.reportArrivedLetter(letterId = letterId ?: -1L, reportLetter = ReportContentModel( + reason = reportType, + description = if(reportType == ReportLetterType.OTHER) etSquareReportEtcReason.text.toString() else null + )) + } + ReportOrigin.LETTER_REPLY -> { + letterViewModel.reportRepliedLetter(replyId = replyId ?: -1L, reportLetter = ReportContentModel( + reason = reportType, + description = if(reportType == ReportLetterType.OTHER) etSquareReportEtcReason.text.toString() else null + )) + } + ReportOrigin.TODAY_QUESTION_ANSWER -> { + questionViewModel.reportTodayQuestionAnswer(answerId = answerId ?: -1L, request = ReportContentModel( + reason = reportType, + description = if(reportType == ReportLetterType.OTHER) etSquareReportEtcReason.text.toString() else null + )) + } + } } } private fun initObservers() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.reportRepliedLetterResult.collect { state -> - when(state) { - is UiState.Failure -> {} - UiState.Idle -> {} - UiState.Loading -> {} - is UiState.Success -> { - Toast.makeText(context, "답장이 신고되었습니다.", Toast.LENGTH_SHORT).show() - viewModel.getSentLetterWithReply(letterId = letterId) - removeScreenBlur() - dismiss() + launch { + letterViewModel.reportArrivedLetterResult.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + Toast.makeText(context, "편지가 신고되었습니다.", Toast.LENGTH_SHORT).show() + removeScreenBlur() + dismiss() + } + } + } + } + launch { + letterViewModel.reportRepliedLetterResult.collect { state -> + when(state) { + is UiState.Failure -> {} + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + Toast.makeText(context, "답장이 신고되었습니다.", Toast.LENGTH_SHORT).show() + letterViewModel.getSentLetterWithReply(letterId = letterId ?: -1L) + removeScreenBlur() + dismiss() + } + } + } + } + launch { + questionViewModel.reportTodayQuestionAnswerResult.collect { state -> + when(state) { + is UiState.Failure -> { + + } + UiState.Idle -> {} + UiState.Loading -> {} + is UiState.Success -> { + Toast.makeText(context, "답변이 신고되었습니다.", Toast.LENGTH_SHORT).show() + removeScreenBlur() + dismiss() + } } } } @@ -124,6 +173,6 @@ class SquareReportDialog(private val letterId: Long, private val replyId: Long) } companion object { - val TAG = "SquareReportDialog" + const val TAG = "SquareReportDialog" } } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/square/viewmodel/FriendsViewModel.kt b/app/src/main/java/com/egobook/app/ui/square/viewmodel/FriendsViewModel.kt index c55e6f40..e6bf46dd 100644 --- a/app/src/main/java/com/egobook/app/ui/square/viewmodel/FriendsViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/viewmodel/FriendsViewModel.kt @@ -94,14 +94,22 @@ class FriendsViewModel @Inject constructor( } } - private val _searchUserResult = MutableSharedFlow>() + private val _searchUserResult = MutableSharedFlow>>() val searchUserResult = _searchUserResult.asSharedFlow() - fun searchUser(keyword: String) { + fun searchUserWithoutRequest(keyword: String) { viewModelScope.launch { _searchUserResult.emit(UiState.Loading) - searchUserUseCase(keyword = keyword).onSuccess { domainList -> - _searchUserResult.emit(UiState.Success(domainList.map { it.toPresentation() }.firstOrNull())) + val outgoingFriendRequestsResult = getOutgoingFriendRequestsUseCase() + val searchUserResult = searchUserUseCase(keyword = keyword) + searchUserResult.onSuccess { searchUsers -> + outgoingFriendRequestsResult.onSuccess { outgoingRequests -> + val outgoingRequestIds = outgoingRequests.map { it.userId }.toSet() + val filteredList = searchUsers.filterNot { user -> outgoingRequestIds.contains(user.userId) }.map { it.toPresentation() } + _searchUserResult.emit(UiState.Success(filteredList)) + }.onFailure { error -> + _searchUserResult.emit(UiState.Failure(error.message)) + } }.onFailure { error -> _searchUserResult.emit(UiState.Failure(error.message)) } diff --git a/app/src/main/java/com/egobook/app/ui/square/viewmodel/LetterViewModel.kt b/app/src/main/java/com/egobook/app/ui/square/viewmodel/LetterViewModel.kt index e2b244ad..42748ee3 100644 --- a/app/src/main/java/com/egobook/app/ui/square/viewmodel/LetterViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/viewmodel/LetterViewModel.kt @@ -11,10 +11,13 @@ import com.egobook.app.domain.usecase.letter.DeferReplyLetterUseCase import com.egobook.app.domain.usecase.letter.DeleteLetterThreadUseCase import com.egobook.app.domain.usecase.letter.DetectAbusiveContentUseCase import com.egobook.app.domain.usecase.letter.GetArrivedPendingLetterUseCase +import com.egobook.app.domain.usecase.letter.GetDeferredLettersUseCase +import com.egobook.app.domain.usecase.letter.GetReceivedReplyByIdUseCase import com.egobook.app.domain.usecase.letter.GetSentLetterWithReplyUseCase import com.egobook.app.domain.usecase.letter.GetSentLettersUseCase import com.egobook.app.domain.usecase.letter.GiveUpReplyLetterUseCase import com.egobook.app.domain.usecase.letter.ReplyLetterUseCase +import com.egobook.app.domain.usecase.letter.ReportArrivedLetterUseCase import com.egobook.app.domain.usecase.letter.ReportRepliedLetterUseCase import com.egobook.app.domain.usecase.letter.SendLetterUseCase import com.egobook.app.ui.home.user.User @@ -22,8 +25,10 @@ import com.egobook.app.ui.square.model.friend.FriendListModel import com.egobook.app.ui.square.model.friend.toPresentation import com.egobook.app.ui.square.model.letter.AbusiveContentModel import com.egobook.app.ui.square.model.letter.ArrivedPendingLetterModel +import com.egobook.app.ui.square.model.letter.DeferredLetterModel +import com.egobook.app.ui.square.model.letter.ReceivedReplyModel import com.egobook.app.ui.square.model.letter.ReplyLetterModel -import com.egobook.app.ui.square.model.letter.ReportLetterModel +import com.egobook.app.ui.square.model.letter.ReportContentModel import com.egobook.app.ui.square.model.letter.SendLetterModel import com.egobook.app.ui.square.model.letter.SentLetterModel import com.egobook.app.ui.square.model.letter.SentLetterWithReplyModel @@ -51,8 +56,11 @@ class LetterViewModel @Inject constructor( private val getSentLettersUseCase: GetSentLettersUseCase, private val getSentLetterWithReplyUseCase: GetSentLetterWithReplyUseCase, private val reportRepliedLetterUseCase: ReportRepliedLetterUseCase, + private val reportArrivedLetterUseCase: ReportArrivedLetterUseCase, private val deleteLetterThreadUseCase: DeleteLetterThreadUseCase, - private val getUserInfoUseCase: GetUserInfoUseCase + private val getDeferredLettersUseCase: GetDeferredLettersUseCase, + private val getUserInfoUseCase: GetUserInfoUseCase, + private val getReceivedReplyByIdUseCase: GetReceivedReplyByIdUseCase ): ViewModel() { private val _friendList = MutableStateFlow>(UiState.Idle) @@ -178,13 +186,27 @@ class LetterViewModel @Inject constructor( } } + private val _reportArrivedLetterResult = MutableSharedFlow>() + val reportArrivedLetterResult = _reportArrivedLetterResult.asSharedFlow() + + fun reportArrivedLetter(letterId: Long, reportLetter: ReportContentModel) { + viewModelScope.launch { + _reportArrivedLetterResult.emit(UiState.Loading) + reportArrivedLetterUseCase(letterId = letterId, reportContent = reportLetter.toDomain()).onSuccess { + _reportArrivedLetterResult.emit(UiState.Success(it)) + }.onFailure { error -> + _reportArrivedLetterResult.emit(UiState.Failure(error.message)) + } + } + } + private val _reportRepliedLetterResult = MutableSharedFlow>() val reportRepliedLetterResult = _reportRepliedLetterResult.asSharedFlow() - fun reportRepliedLetter(replyId: Long, reportLetter: ReportLetterModel) { + fun reportRepliedLetter(replyId: Long, reportLetter: ReportContentModel) { viewModelScope.launch { _reportRepliedLetterResult.emit(UiState.Loading) - reportRepliedLetterUseCase(replyId = replyId, reportLetter = reportLetter.toDomain()).onSuccess { + reportRepliedLetterUseCase(replyId = replyId, reportContent = reportLetter.toDomain()).onSuccess { _reportRepliedLetterResult.emit(UiState.Success(it)) }.onFailure { error -> _reportRepliedLetterResult.emit(UiState.Failure(error.message)) @@ -206,6 +228,31 @@ class LetterViewModel @Inject constructor( } } + private val _deferredLetters = MutableStateFlow?>(null) + val deferredLetters = _deferredLetters.asStateFlow() + + fun getDeferredLetters(size: Int) { + viewModelScope.launch { + getDeferredLettersUseCase(size = size).cachedIn(viewModelScope).collectLatest { pagingData -> + _deferredLetters.value = pagingData.map { it.toPresentation() } + } + } + } + + private val _receivedReplies = MutableStateFlow>(UiState.Idle) + val receivedReplies = _receivedReplies.asStateFlow() + + fun getReceivedReplyById(replyId: Long) { + viewModelScope.launch { + _receivedReplies.value = UiState.Loading + getReceivedReplyByIdUseCase(replyId = replyId).onSuccess { domainItem -> + _receivedReplies.value = UiState.Success(domainItem.toPresentation()) + }.onFailure { error -> + _receivedReplies.value = UiState.Failure(error.message) + } + } + } + private val _userInfo = MutableSharedFlow>() val userInfo = _userInfo.asSharedFlow() diff --git a/app/src/main/java/com/egobook/app/ui/square/viewmodel/QuestionViewModel.kt b/app/src/main/java/com/egobook/app/ui/square/viewmodel/QuestionViewModel.kt index 8541e541..f7408b9f 100644 --- a/app/src/main/java/com/egobook/app/ui/square/viewmodel/QuestionViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/square/viewmodel/QuestionViewModel.kt @@ -12,6 +12,9 @@ import com.egobook.app.domain.usecase.GetTodayFriendsRepliesUseCase import com.egobook.app.domain.usecase.GetTodayQuestionUseCase import com.egobook.app.domain.usecase.SubmitTodayAnswerUseCase import com.egobook.app.domain.usecase.UpdateTodayAnswerUseCase +import com.egobook.app.domain.usecase.question.ReportTodayQuestionAnswerUseCase +import com.egobook.app.ui.square.model.letter.ReportContentModel +import com.egobook.app.ui.square.model.letter.toDomain import com.egobook.app.ui.square.model.question.MyTodayQuestionAnswerItemModel import com.egobook.app.ui.square.model.question.TodayAnswerModel import com.egobook.app.ui.square.model.question.TodayQuestionModel @@ -36,7 +39,8 @@ class QuestionViewModel @Inject constructor( private val getTodayFriendsRepliesUseCase: GetTodayFriendsRepliesUseCase, private val getTodayAllUserRepliesUseCase: GetTodayAllUserRepliesUseCase, private val updateTodayAnswerUseCase: UpdateTodayAnswerUseCase, - private val deleteMyQuestionAnswerUseCase: DeleteMyQuestionAnswerUseCase + private val deleteMyQuestionAnswerUseCase: DeleteMyQuestionAnswerUseCase, + private val reportTodayQuestionAnswerUseCase: ReportTodayQuestionAnswerUseCase ): ViewModel() { private val _todayQuestion = MutableStateFlow>(UiState.Idle) @@ -127,4 +131,18 @@ class QuestionViewModel @Inject constructor( } } } + + private val _reportTodayQuestionAnswerResult = MutableSharedFlow>() + val reportTodayQuestionAnswerResult = _reportTodayQuestionAnswerResult.asSharedFlow() + + fun reportTodayQuestionAnswer(answerId: Long, request: ReportContentModel) { + viewModelScope.launch { + _reportTodayQuestionAnswerResult.emit(UiState.Loading) + reportTodayQuestionAnswerUseCase(answerId = answerId, request = request.toDomain()).onSuccess { + _reportTodayQuestionAnswerResult.emit(UiState.Success(it)) + }.onFailure { error -> + _reportTodayQuestionAnswerResult.emit(UiState.Failure(error.message)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_crown.png b/app/src/main/res/drawable/ic_crown.png deleted file mode 100644 index aa2ba03f..00000000 Binary files a/app/src/main/res/drawable/ic_crown.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_crown.xml b/app/src/main/res/drawable/ic_crown.xml new file mode 100644 index 00000000..a16f71d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_crown.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_letter_send.xml b/app/src/main/res/layout/dialog_letter_send.xml index 143b8306..4935982b 100644 --- a/app/src/main/res/layout/dialog_letter_send.xml +++ b/app/src/main/res/layout/dialog_letter_send.xml @@ -51,10 +51,10 @@ app:cornerRadius="4dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_letter_send_description" - app:layout_constraintEnd_toStartOf="@id/btn_letter_send_apply"/> + app:layout_constraintEnd_toStartOf="@id/btn_letter_send"/> - - - - - - - - - - - - - - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="264dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_search_friend" + tools:itemCount="2"/> + + diff --git a/app/src/main/res/layout/fragment_ego_room_statistics.xml b/app/src/main/res/layout/fragment_ego_room_statistics.xml index 562e32ad..306cf1d3 100644 --- a/app/src/main/res/layout/fragment_ego_room_statistics.xml +++ b/app/src/main/res/layout/fragment_ego_room_statistics.xml @@ -28,6 +28,8 @@ android:id="@+id/tv_counseling_statistics_total_count_title" android:layout_width="0dp" android:layout_height="wrap_content" + android:minHeight="24sp" + android:gravity="center_vertical" android:paddingVertical="8dp" android:text="전체 감정 기록 횟수" android:textColor="@color/neutral" @@ -44,24 +46,36 @@ android:layout_marginTop="12dp" android:layout_marginEnd="16dp" android:src="@drawable/img_emotion_very_happy" + app:layout_constraintTop_toBottomOf="@id/tv_counseling_statistics_total_count_title" app:layout_constraintEnd_toStartOf="@id/hbc_counseling_statistics_total_count_very_happy" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_counseling_statistics_total_count_title" /> + app:layout_constraintStart_toStartOf="parent"/> + + + @@ -375,7 +407,6 @@ android:layout_width="32dp" android:layout_height="32dp" android:src="@drawable/img_emotion_sad" - android:layout_marginBottom="16dp" app:layout_constraintTop_toBottomOf="@id/iv_counseling_statistics_trend_neutral" app:layout_constraintStart_toStartOf="@id/iv_counseling_statistics_trend_neutral" app:layout_constraintBottom_toTopOf="@id/iv_counseling_statistics_trend_very_sad"/> @@ -385,20 +416,18 @@ android:layout_height="32dp" android:src="@drawable/img_emotion_very_sad" app:layout_constraintTop_toBottomOf="@id/iv_counseling_statistics_trend_sad" - app:layout_constraintStart_toStartOf="@id/iv_counseling_statistics_trend_sad" - app:layout_constraintBottom_toTopOf="@id/cv_counseling_statistics_average_last_month"/> + app:layout_constraintStart_toStartOf="@id/iv_counseling_statistics_trend_sad" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/cv_counseling_statistics_average_last_month"/> @@ -29,79 +29,78 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - tools:text="3" + android:fontFamily="@font/arita_semibold" android:textColor="@color/neutral_subtle" android:textSize="14sp" - android:fontFamily="@font/arita_semibold" app:layout_constraintBottom_toBottomOf="@id/tv_friends_pending_list_received_title" app:layout_constraintStart_toEndOf="@id/tv_friends_pending_list_received_title" - app:layout_constraintTop_toTopOf="@id/tv_friends_pending_list_received_title" /> - - + app:layout_constraintTop_toTopOf="@id/tv_friends_pending_list_received_title" + tools:text="3" /> - - + + + + + - - + app:layout_constraintTop_toBottomOf="@id/fl_friends_pending_list_received" /> - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/tv_friends_pending_list_sent_title"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_letter_reply.xml b/app/src/main/res/layout/fragment_letter_reply.xml index e30ba77c..7070e9b6 100644 --- a/app/src/main/res/layout/fragment_letter_reply.xml +++ b/app/src/main/res/layout/fragment_letter_reply.xml @@ -90,7 +90,7 @@ + app:layout_constraintTop_toTopOf="@id/iv_letter_write_tooltip" /> + app:layout_constraintTop_toBottomOf="@id/iv_letter_write_tooltip"/> - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_my_letter_detail.xml b/app/src/main/res/layout/fragment_my_letter_detail.xml index 9b93af49..469607e1 100644 --- a/app/src/main/res/layout/fragment_my_letter_detail.xml +++ b/app/src/main/res/layout/fragment_my_letter_detail.xml @@ -152,7 +152,7 @@ android:id="@+id/tv_my_letter_detail_receiver" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="To. 낯선 고북이" + tools:text="To 낯선 고북이" android:textColor="@color/neutral" android:textSize="14sp" android:fontFamily="@font/arita_semibold" @@ -205,7 +205,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:text="From 또 다른 고북이" + tools:text="From 또 다른 고북이" android:textColor="@color/neutral" android:textSize="14sp" android:fontFamily="@font/arita_semibold" diff --git a/app/src/main/res/layout/fragment_my_replies_history.xml b/app/src/main/res/layout/fragment_my_replies_history.xml index 8be05d65..baa43a7e 100644 --- a/app/src/main/res/layout/fragment_my_replies_history.xml +++ b/app/src/main/res/layout/fragment_my_replies_history.xml @@ -34,6 +34,40 @@ android:fontFamily="@font/arita_semibold"/> + + + + diff --git a/app/src/main/res/layout/fragment_square.xml b/app/src/main/res/layout/fragment_square.xml index a0ed0f73..8fa3b93a 100644 --- a/app/src/main/res/layout/fragment_square.xml +++ b/app/src/main/res/layout/fragment_square.xml @@ -25,12 +25,14 @@ + android:textSize="20sp" /> @@ -56,12 +58,14 @@ android:id="@+id/tv_square_letter_title" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minHeight="24sp" + android:gravity="center_vertical" android:layout_marginHorizontal="16dp" android:layout_marginTop="24dp" + android:fontFamily="@font/arita_semibold" android:text="편지" android:textColor="@color/neutral" android:textSize="16sp" - android:fontFamily="@font/arita_semibold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ll_square_header" /> @@ -70,17 +74,26 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="20dp" + android:layout_marginHorizontal="16dp" android:scrollbars="none" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/tv_square_letter_title" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_square_letter_title"> + android:orientation="horizontal"> + + - - + android:layout_marginTop="8dp"/> + + android:textSize="16sp" /> + android:textSize="14sp" /> + + + android:textColor="@color/neutral" + android:textSize="14sp" + app:layout_constraintEnd_toEndOf="@id/tv_square_sent_letter_placeholder_main" + app:layout_constraintStart_toStartOf="@id/tv_square_sent_letter_placeholder_main" + app:layout_constraintTop_toBottomOf="@id/tv_square_sent_letter_placeholder_main" /> + tools:itemCount="4" + tools:listitem="@layout/item_square_letter" /> + android:textSize="16sp" /> + android:textSize="14sp" /> + tools:text="Q. 다시 태어난다면 어느 나라에서 태어나고 싶으신가요?" /> @@ -311,10 +342,10 @@ android:layout_marginTop="24dp" android:layout_marginBottom="24dp" android:backgroundTint="@color/neutral" + android:fontFamily="@font/arita_semibold" android:text="작성하기" android:textColor="@color/layer_white" android:textSize="12sp" - android:fontFamily="@font/arita_semibold" app:cornerRadius="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/tv_square_today_question_description_unanswered" @@ -361,16 +392,16 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginTop="24dp" + android:fontFamily="@font/arita_semibold" + android:lineHeight="30sp" android:textAlignment="center" + android:textColor="@color/neutral" + android:textSize="20sp" android:translationZ="10dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:text="Q. 다시 태어난다면 어느 나라에서 태어나고 싶으신가요?" - android:textColor="@color/neutral" - android:textSize="20sp" - android:fontFamily="@font/arita_semibold" - android:lineHeight="21sp"/> + tools:text="Q. 다시 태어난다면 어느 나라에서 태어나고 싶으신가요?" /> + tools:text="사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답 사용자가 작성한 답" /> @@ -441,16 +472,16 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginTop="24dp" + android:fontFamily="@font/arita_semibold" + android:lineHeight="30sp" android:textAlignment="center" + android:textColor="@color/neutral" + android:textSize="20sp" android:translationZ="10dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:text="Q. 다시 태어난다면 어느 나라에서 태어나고 싶으신가요?" - android:textColor="@color/neutral" - android:textSize="20sp" - android:fontFamily="@font/arita_semibold" - android:lineHeight="21sp"/> + tools:text="Q. 다시 태어난다면 어느 나라에서 태어나고 싶으신가요?" /> + android:textColor="@color/neutral" + android:textColorHint="@color/neutral_subtle" + android:textSize="14sp" /> + android:textSize="14sp" /> @@ -504,24 +536,26 @@ android:id="@+id/ll_square_today_question_visibility_type" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginVertical="24dp" + android:gravity="center_vertical" android:orientation="horizontal" android:paddingHorizontal="16dp" android:paddingVertical="6.5dp" - android:layout_marginVertical="24dp" - android:gravity="center_vertical" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@id/cv_square_today_question_input" - app:layout_constraintTop_toBottomOf="@id/cv_square_today_question_input" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintTop_toBottomOf="@id/cv_square_today_question_input"> + android:textSize="14sp" /> + android:textSize="16sp" /> + android:textSize="14sp" /> 3 + android:src="@drawable/ic_square_view_all" /> + app:layout_constraintTop_toBottomOf="@id/ll_square_friends_answer_header" /> + app:layout_constraintTop_toBottomOf="@id/tv_square_today_question_friend_reply_placeholder_main" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/ll_square_friends_answer_header" + tools:itemCount="3" + tools:listitem="@layout/item_square_friend_answer" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_square_all_replies.xml b/app/src/main/res/layout/fragment_square_all_replies.xml index 345bb6e9..d0d0e9b0 100644 --- a/app/src/main/res/layout/fragment_square_all_replies.xml +++ b/app/src/main/res/layout/fragment_square_all_replies.xml @@ -78,13 +78,15 @@ android:id="@+id/tv_square_all_replies_placeholder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="아직 전체 유저의 답변이 없습니다\n답을 하지 않았어요" + android:text="아직 전체 유저의 답변이 없습니다" android:textColor="@color/neutral" android:textSize="20sp" android:fontFamily="@font/arita_bold" android:lineHeight="21sp" android:letterSpacing="-0.02" android:textAlignment="center" + android:visibility="gone" + app:layout_constraintVertical_bias="0.2" app:layout_constraintTop_toBottomOf="@id/cv_square_all_replies_today_question" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -108,9 +110,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" - android:backgroundTint="@color/brand" + android:contentDescription="Go To First Of All Replies!" android:src="@drawable/ic_arrow_up" + android:backgroundTint="@color/brand" app:tint="@color/layer_white" + app:shapeAppearanceOverlay="@style/Circle" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_friend.xml b/app/src/main/res/layout/item_search_friend.xml new file mode 100644 index 00000000..238afaf1 --- /dev/null +++ b/app/src/main/res/layout/item_search_friend.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_square_deferred_letter.xml b/app/src/main/res/layout/item_square_deferred_letter.xml new file mode 100644 index 00000000..7af22f38 --- /dev/null +++ b/app/src/main/res/layout/item_square_deferred_letter.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_square_friend_answer.xml b/app/src/main/res/layout/item_square_friend_answer.xml index b56a8965..251adfcb 100644 --- a/app/src/main/res/layout/item_square_friend_answer.xml +++ b/app/src/main/res/layout/item_square_friend_answer.xml @@ -3,6 +3,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" + android:paddingVertical="16dp" + android:clipToPadding="false" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> @@ -12,38 +14,42 @@ android:layout_height="24dp" android:src="@drawable/level_type_1" android:translationZ="1dp" - android:layout_marginTop="16dp" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent"/> + android:layout_marginTop="-8dp" + android:layout_marginStart="-8dp" + app:layout_constraintTop_toTopOf="@id/civ_item_square_friend_answer_user_image" + app:layout_constraintStart_toStartOf="@id/civ_item_square_friend_answer_user_image"/> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toBottomOf="@id/civ_item_square_friend_answer_user_image" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_square_friend_pending_received_list.xml b/app/src/main/res/layout/item_square_friend_pending_received_list.xml index f6fe9655..2174bdfe 100644 --- a/app/src/main/res/layout/item_square_friend_pending_received_list.xml +++ b/app/src/main/res/layout/item_square_friend_pending_received_list.xml @@ -36,7 +36,7 @@ android:id="@+id/tv_item_friend_pending_list_level" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="LV 9999" + tools:text="LV 9999" android:textColor="@color/neutral" android:textSize="12sp" android:fontFamily="@font/arita_semibold" diff --git a/app/src/main/res/layout/item_square_friend_pending_sent_list.xml b/app/src/main/res/layout/item_square_friend_pending_sent_list.xml index e9d738c8..8b3a4e92 100644 --- a/app/src/main/res/layout/item_square_friend_pending_sent_list.xml +++ b/app/src/main/res/layout/item_square_friend_pending_sent_list.xml @@ -4,7 +4,8 @@ android:layout_height="wrap_content" android:background="@color/layer_white" android:layout_marginBottom="16dp" - xmlns:app="http://schemas.android.com/apk/res-auto"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="wrap_content" + android:paddingVertical="12dp" + android:layout_marginTop="8dp" + android:orientation="horizontal"> + android:src="@drawable/ic_letter"/> + android:layout_marginStart="8dp"/> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_square_question_reply.xml b/app/src/main/res/layout/item_square_question_reply.xml index 17dc9e5b..da0dd598 100644 --- a/app/src/main/res/layout/item_square_question_reply.xml +++ b/app/src/main/res/layout/item_square_question_reply.xml @@ -4,6 +4,8 @@ android:layout_height="wrap_content" android:background="@color/white" android:layout_marginBottom="16dp" + android:paddingVertical="16dp" + android:clipToPadding="false" xmlns:app="http://schemas.android.com/apk/res-auto"> + android:layout_marginTop="-8dp" + android:layout_marginStart="-8dp" + app:layout_constraintTop_toTopOf="@id/civ_item_square_question_reply_user_image" + app:layout_constraintStart_toStartOf="@id/civ_item_square_question_reply_user_image"/> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> @@ -49,7 +51,6 @@ app:layout_constraintStart_toEndOf="@id/civ_item_square_question_reply_user_image"/> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_letter_tooltip_popup.xml b/app/src/main/res/layout/layout_letter_tooltip_popup.xml index b62fd507..211cca37 100644 --- a/app/src/main/res/layout/layout_letter_tooltip_popup.xml +++ b/app/src/main/res/layout/layout_letter_tooltip_popup.xml @@ -11,6 +11,7 @@ app:strokeWidth="0dp" app:cardCornerRadius="12dp"> + app:startDestination="@id/menu_square">