Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#48 프로필 화면 구현 #139

Merged
merged 47 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c87bc17
feat(MainActivity): 로그인 하거나 온보딩 화면에서 회원 정보 등록 후 메인 화면으로 이동하도록 변경
ki960213 Jul 28, 2023
16e263e
feat(MainActivity): 처음 메인 화면에 가면 프로필 화면이 보이도록 구현
ki960213 Jul 28, 2023
9d88772
feat(MainActivity): 하단 내비게이션 뷰의 스타일 설정 및 테마 변경
ki960213 Jul 28, 2023
a64be8f
feat(MainActivity): 메인 화면에 가면 처음에 행사 목록 화면으로 이동하도록 구현
ki960213 Jul 28, 2023
3534dd9
feat(MainActivity): 하단 내비게이션 뷰 메뉴 클릭 시 화면이 변경되도록 구현
ki960213 Jul 28, 2023
e2c2c6f
feat: 폰트 추가 및 하단 내비게이션 뷰의 스타일 지정
ki960213 Jul 28, 2023
fe77950
feat(MyProfileFragment): 회원 정보 화면 xml 일부 구현
ki960213 Jul 28, 2023
de86666
feat(MyProfileFragment): 회원 정보 화면 일부 구현
ki960213 Jul 29, 2023
a50e817
feat(MyProfileFragment): 회원의 프로필에서 활동 내역을 보여주는 기능 구현
ki960213 Jul 29, 2023
5a3192f
refactor(MyProfileScreenUiState): MyProfileScreenUiState의 속성명 변경
ki960213 Jul 29, 2023
4577a10
refactor(MyProfileFragment): 리사이클러 뷰 설정하는 코드 리팩터링
ki960213 Jul 29, 2023
e66ba27
refactor(fragment_my_profile): 바인딩 어댑터 속성명 변경
ki960213 Jul 29, 2023
b1e5ded
feat(fragment_my_profile): xml 수정
ki960213 Jul 29, 2023
81d00ed
feat(item_myprofile_activities): xml 수정
ki960213 Jul 29, 2023
a214fd6
refactor(MemberWithoutActivitiesApiModel): 클래스명 변경
ki960213 Jul 29, 2023
8caac36
refactor(MyProfileFragment): 리사이클러 뷰들 초기화 하는 로직 리팩터링
ki960213 Jul 30, 2023
8b6ae13
refactor(bg_myprofile_job.xml): 마이 프로필 화면의 직무 태그 배경의 파일명 변경
ki960213 Jul 30, 2023
5b5e0c5
refactor(color_bottom_navigation_view_icon.xml): 필요없는 주석 제거
ki960213 Jul 30, 2023
94cb706
feat(fragment_my_profile): 프로필 이미지가 원 모양으로 보이도록 변경
ki960213 Jul 31, 2023
c44439b
feat(MyProfileFragment): 아래로 당기면 새로고침하는 기능 구현
ki960213 Jul 31, 2023
bacd0ee
feat(fragment_my_profile.xml): progress bar가 화면의 가운데에 오도록 변경
ki960213 Jul 31, 2023
933d0bd
refactor(SwipeRefreshLayoutBindingAdapter): SwipeRefreshLayout의 새로고침 …
ki960213 Jul 31, 2023
aace342
refactor(Member1): 회원의 활동 목록을 요청하는 메서드 명 변경
ki960213 Jul 31, 2023
0319245
refactor(MemberRepository, MemberService): 데이터를 반환하는 함수들의 네이밍 패턴 변경
ki960213 Jul 31, 2023
696f2ac
refactor(ActivitiesAssociatedByActivityType, ActivityApiModel): ApiMo…
ki960213 Jul 31, 2023
b27e3a5
feat(LoginActivity): 로그인 성공 시 LoginActivity가 사라지도록 변경
ki960213 Jul 31, 2023
11582ed
refactor(MainActivity): 변수명 변경
ki960213 Jul 31, 2023
8470ca7
fix: 바뀐 api 명세에 맞춰 ApiModel 수정
ki960213 Jul 31, 2023
0828bc2
fix(nanum_square.xml): 각 폰트마다 fontWeight 설정
ki960213 Jul 31, 2023
b7544c9
fix(fragment_my_profile.xml): 스크롤이 되지 않는 버그 수정
ki960213 Jul 31, 2023
006b04a
refactor(member/dto): 서버에서 받아오는 응답 dto에 @SerialName 추가
ki960213 Aug 1, 2023
e69b430
feat(MemberService): 특정 회원의 활동 이력을 조회하는 Api의 수정에 따라 Service 코드 변경
ki960213 Aug 1, 2023
a218c2c
feat(MainActivity): 하단 내비게이션으로 프래그먼트를 선택할 때 show/hide 방식으로 보여주도록 변경
ki960213 Aug 1, 2023
d3948ce
refactor(MyProfileFragment): 코드 컨벤션 지키도록 변경
ki960213 Aug 1, 2023
d4781c6
feat(MyProfileFragment): 로그인 되어있지 않으면 로그인 화면으로 이동하도록 변경
ki960213 Aug 1, 2023
e4dba6d
fix(myProfile/adapter): areItemsTheSame 메서드와 areContentsTheSame 메서드 사…
ki960213 Aug 1, 2023
6a94947
refactor(myProfile/adapter): 리사이클러 뷰 어댑터와 뷰 홀더 파일 분리
ki960213 Aug 1, 2023
f9f90d2
refactor(MyProfileFragment): 리사이클러 뷰 아이템을 설정하는 로직을 바인딩 어댑터를 사용하는 게 아니…
ki960213 Aug 1, 2023
ef7d87a
refactor(ActivitiesAdapterDecoration): 싱글톤 패턴 제거
ki960213 Aug 1, 2023
3913d17
feat(ImageViewBindingAdapter): 이미지 뷰 바인딩 어댑터로 에러 이미지를 설정할 수 있도록 변경
ki960213 Aug 1, 2023
bd24a29
refactor(MemberRepositoryImpl): 데이터 소스 모델과 데이터 모델을 매핑하는 로직을 Repositor…
ki960213 Aug 1, 2023
aeb2b30
fix(MemberServiceImpl): 회원 정보 요청 에러 해결
ki960213 Aug 1, 2023
f87e92e
feat(fragment_my_profile.xml): 내 프로필 화면의 한 줄 소개 너비 넓힘
ki960213 Aug 1, 2023
5a91bca
feat: android-main과 머지
ki960213 Aug 1, 2023
127a874
style: ktlint 적용
ki960213 Aug 1, 2023
f35a5de
fix(IntExt): px 확장 변수 제거 및 dp 확장 변수 변경
ki960213 Aug 1, 2023
bf98e2b
Merge remote-tracking branch 'origin/android-main' into Feature/#48-프…
ki960213 Aug 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion android/2023-emmsale/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ android {
dataBinding {
enable = true
}
tasks.withType(Test::class) {
useJUnitPlatform()
testLogging {
events.addAll(arrayOf(org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED))
}
}
}

fun getApiKey(propertyKey: String): String {
Expand All @@ -66,16 +72,23 @@ dependencies {
implementation("androidx.browser:browser:1.5.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.google.firebase:firebase-messaging-ktx:23.2.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("androidx.fragment:fragment-ktx:1.6.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:mockwebserver:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.github.bumptech.glide:glide:4.15.1")

implementation(platform("com.google.firebase:firebase-bom:32.2.0"))
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-messaging:23.2.0")

testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")
testImplementation("io.mockk:mockk-android:1.13.5")
testImplementation("io.mockk:mockk-agent:1.13.5")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2")
}
11 changes: 6 additions & 5 deletions android/2023-emmsale/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,21 @@
android:theme="@style/Theme.Emmsale"
android:usesCleartextTraffic="true"
tools:targetApi="33">
<activity
android:name=".presentation.ui.main.MainActivity"
android:exported="false" />
<activity
android:name=".presentation.ui.onboarding.OnboardingActivity"
android:exported="true" />

<activity
android:name=".presentation.ui.login.LoginActivity"
android:launchMode="singleTask"
android:exported="true">
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />

Expand All @@ -52,4 +53,4 @@
</service>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.emmsale.data.activity

data class Activity1(
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금은 DataModel 을 이렇게 Activity1 과 Acitivty 이런 방식으로 두지만, 추후에 시간이 여유롭다면 Acitivity1 하나만 있는게 좋을 것 같습니다. 나중에Onboarding 을 Activity1 의 데이터 모델을 이용하여 해결했으면 좋겠네요.

Copy link
Member

Choose a reason for hiding this comment

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

커디 프로젝트,,, 빠른 리팩토링이 시급합니다..

val id: Long,
val activityType: ActivityType,
val name: String
tmdgh1592 marked this conversation as resolved.
Show resolved Hide resolved
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.emmsale.data.activity

enum class ActivityType {
EDUCATION, CLUB, EVENT, JOB
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.emmsale.data.member

import com.emmsale.data.activity.Activity1
import com.emmsale.data.activity.ActivityType

data class Member1(
val id: Long,
val name: String,
val description: String,
val imageUrl: String,
val activities: Map<ActivityType, List<Activity1>>,
) {
operator fun get(activityType: ActivityType): List<Activity1> =
activities[activityType] ?: listOf()
ki960213 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ package com.emmsale.data.member
import com.emmsale.data.common.ApiResult

interface MemberRepository {

suspend fun fetchMember(memberId: Long): ApiResult<Member1>

ki960213 marked this conversation as resolved.
Show resolved Hide resolved
suspend fun updateMember(member: Member): ApiResult<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
package com.emmsale.data.member

import com.emmsale.data.common.ApiError
import com.emmsale.data.common.ApiResult
import com.emmsale.data.common.ApiSuccess
import com.emmsale.data.common.handleApi
import com.emmsale.data.member.dto.MemberApiModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext

class MemberRepositoryImpl(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val memberService: MemberService,
) : MemberRepository {

override suspend fun fetchMember(memberId: Long): ApiResult<Member1> = withContext(dispatcher) {
val memberResponseDeferred = async { memberService.fetchMember(memberId) }
val activitiesResponseDeferred = async { memberService.fetchActivities() }
tmdgh1592 marked this conversation as resolved.
Show resolved Hide resolved
val memberResponse = memberResponseDeferred.await()
val activitiesResponse = activitiesResponseDeferred.await()
ki960213 marked this conversation as resolved.
Show resolved Hide resolved

val memberApiModel = memberResponse.body() ?: return@withContext ApiError(
memberResponse.code(),
memberResponse.errorBody().toString()
)
val activitiesApiModel = activitiesResponse.body() ?: return@withContext ApiError(
activitiesResponse.code(),
activitiesResponse.errorBody().toString()
)

ApiSuccess(
Member1(
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 모델 클래스에서 책임지도록 하면 좋을 것 같습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어떤 모델 클래스에서 책임지는 게 좋다고 생각하시나요?
우선 서버의 응답값을 데이터 모델로 생성하는 작업은 데이터 모델이 할 일은 아니라고 생각합니다. 비즈니스 로직이 아니기 때문입니다.
그리고 데이터 모델을 만드는 데 두 종류의 ApiModel이 필요합니다. 그래서 이 작업을 둘 중 하나에 맡긴다면 둘 중 하나는 다른 응답값에 의존하기 때문에 좋지 않다고 생각합니다. 두 ApiModel은 서로 다른 서버와의 통신에 필요하기 때문에 하나의 통신이 다른 통신에 의존하는 형태는 좋지 않다고 생각합니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 그렇네요 제가 착각했습니다 ㅎㅎ 아니면 private 함수로 따로 뺴도 좋을 것 같아요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

아니면

val member = memberApiModel.toData()
val activities = activitiesApiModel.associate { it.getActivityType() to it.toData() }
ApiSuccess(member,activities)

이런 방법은 어떤가요

id = memberApiModel.id,
name = memberApiModel.name,
description = memberApiModel.description,
imageUrl = memberApiModel.imageUrl,
activities = activitiesApiModel.associate { it.getActivityType() to it.toData() }
)
)
}

override suspend fun updateMember(member: Member): ApiResult<Unit> = withContext(dispatcher) {
handleApi(memberService.updateMember(MemberApiModel.from(member))) { }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
package com.emmsale.data.member

import com.emmsale.data.member.dto.MemberActivitiesBindActivityTypeApiModel
import com.emmsale.data.member.dto.MemberApiModel
import com.emmsale.data.member.dto.MemberWithoutActivitiesApiModel
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

interface MemberService {

@GET("members/{memberId}")
suspend fun fetchMember(@Path("memberId") memberId: Long): Response<MemberWithoutActivitiesApiModel>

@GET("members/activities")
suspend fun fetchActivities(): Response<List<MemberActivitiesBindActivityTypeApiModel>>

@POST("/members")
suspend fun updateMember(@Body member: MemberApiModel): Response<Unit>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.emmsale.data.member.dto

import com.emmsale.data.activity.Activity1
import com.emmsale.data.activity.ActivityType
import kotlinx.serialization.Serializable

@Serializable
data class MemberActivitiesBindActivityTypeApiModel(
Copy link
Member

Choose a reason for hiding this comment

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

MemberActivitiesBindActivityTypeApiModel에서 BindActivity는 무엇을 의미하는건가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

활동 타입으로 묶인 회원의 활동 목록입니다! 이름이 이상하군요!
클래스 명을 ActivitiesAssociatedByActivityTypeApiModel로 바꾸는 게 나을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

제생각엔 이거 저희가 처음 말했던

data class Activity(
id:Long.
type: ActivityType
name:String
)

이 데이터 모델을 공통으로 쓰는게 좋아보이긴 합니다...
만약 이렇게 바꾸는게 이번 스프린트때는 힘들다면 다음 스프린트때 바꾸는게 좋아보여요. 지금 ActivitiesAssociatedByActivityTypeApiModel 이것만 봐도 항상 네이밍 짓기가 되게 힘들 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네이밍 짓기가 힘들지만 ApiModel의 클래스명은 꽤 자세히 적어야 한다고 생각합니다. 왜냐하면 외부 서비스의 메세지 형태는 매우 다양해질 수 있기 때문입니다.

Copy link
Member

Choose a reason for hiding this comment

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

두 분 다 맞는 말이예요!
저도 클래스명은 구체적으로 작성하는 것이 좋다고 생각합니다.
다만, 위 클래스명은 3자가 봤을 때 오히려 너무 길어서 파악하기 힘들어 보이기도 하네요 🥲

Copy link
Member

Choose a reason for hiding this comment

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

활동 타입으로 묶인 회원의 활동 목록입니다! 이름이 이상하군요!
클래스 명을 ActivitiesAssociatedByActivityTypeApiModel로 바꾸는 게 나을까요?

사실 위 네이밍도 어떤 역할을 하는지 명확히 이해하기가 힘듭니다..!
조금 더 개선된 모델명이 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저도 길면 가독성이 떨어진다는 것에 공감하지만, 짧으면서 ApiModel의 정확한 의미를 반영하는 이름이 떠오르지 않네요ㅠㅠ

val activityType: String,
val memberActivityResponses: List<MemberActivityApiModel>,
Copy link
Member

Choose a reason for hiding this comment

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

이건 선택이지만, @SerialName(...)을 설정해두면, 혹시 나중에 프로퍼티명이 실수나 고의로 변경된다고 하더라도, 역/직렬화 이름은 그대로 유지되기 때문에 버그가 발생하는 상황을 방지할 수 있습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dto는 딱 외부 서비스와의 통신을 위한 데이터 묶음이므로 프로퍼티 명을 외부 서비스와 관련 없는 이름으로 선언할 필요가 없다고 생각합니다.
따라서 나중에 프로퍼티 명을 변경할 이유가 없다고 생각합니다...! 그냥 외부 서비스에서 제공하는 이름 그대로 선언하는 것이 생각하기에도 편하다고 생각합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 개인적으로 부나와 같은 의견으로 명시해주는게 좋아보이긴 합니다.
만약 명시를 안한다고 해도, 저희 3명이 모두 명시를 안한다거나, 모두 명시를 한다거나 하는식으로 통일을 해야할 것 같습니다. (저도 명시를 해주고 있습니다)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

혹시 @SerialName의 값과 ApiModel의 속성명이 달라지는 경우가 있나요?
외부 서비스의 응답값이 Json일 때, 속성명이 코틀린 언어에 맞지 않는 문법으로 되어있다면 어쩔 수 없이 @SerialName을 써야하지만, 그렇지 않은 경우에는 @SerialName을 쓰는 것은 불필요하다고 생각합니다!

Copy link
Member

Choose a reason for hiding this comment

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

저의 경우, 서버에서 제공하는 변수명은 remaningDays이지만, 클라이언트에서는 dDay로 사용하고 싶은 경우가 있었습니다.
물론 이럴 떄에도 remainingDays라는 네이밍을 서버에 요청해서 dDay로 바꾸는 것은 어떤지 상의할 수 있지만,
협업에서는 이러한 단순한 작업 하나도 과정이 너무나도 길다는 것입니다.

그래서 클라이언트는 @SerialName 어노테이션을 통해 원하는 변수명으로 변환하여 사용하기도 합니다.
토마스가 사용하는 프로퍼티명이 서버에서 제공해주는 변수명과 비교했을 때 불편하지 않다면, 위 리뷰를 반영할지 여부는 선택사항입니다.

그 외에도 어노테이션을 사용하면 IDE에서 노란색으로 표기해주기 때문에 눈에 더 확 띈다는 점은 보너스이구요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

서버에서 제공하는 변수명이 무슨 뜻인지 알기 힘든 경우 @SerialName을 사용하는 것이 좋아보이네요!
이러한 경우에 대비하기 위해 @SerialName을 사용하는 것은 좋다고 생각이 들었습니다.
@SerialName을 사용하지 않았을 때 dto의 변수명을 바꾸려면 새로 @SerialName 애노테이션을 추가해야 하므로 처음부터 서버와 통신하기 위한 이름, 개발자가 쉽게 파악하기 위한 이름을 따로 구분하는 게 좋아보이네요 👍

) {
fun toData(): List<Activity1> {
return memberActivityResponses.map {
Activity1(
id = it.id,
activityType = getActivityType(),
name = it.name
)
}
}

fun getActivityType(): ActivityType =
when (this.activityType) {
"동아리" -> ActivityType.CLUB
"컨퍼런스" -> ActivityType.EVENT
"교육" -> ActivityType.EDUCATION
"직무" -> ActivityType.JOB
else -> throw IllegalStateException("회원의 활동 Json 데이터를 도메인 모델로 매핑하는 데 실패했습니다. 서버와 Api 스펙을 다시 상의해보세요.")
ki960213 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

ActivityType에게 역할을 부여해도 좋을 것 같습니다!
ActivityType이 바뀌어도 MemberActivitiesBindActivityTypeApiModel는 변경될 필요가 없어집니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Api 요청 시 반환되는 값 중 "activityType": "동아리" 이러한 값들은 서버와 협상한 일종의 신호라고 생각합니다.
"동아리라는 코드는 활동 중 동아리 타입이야." 이런 식으로... 동아리가 아니라 "c"라는 값이 동아리 타입이라고 할 수도 있습니다. 즉, 이러한 서버와의 협상에 사용되는 신호가 ActivityType이라는 도메인 로직을 담당하는 데이터 모델에 포함되면 안된다고 생각합니다.
ActivityType은 도메인 로직을 위한 코드이고, MemberActivitiesBindActivityTypeApiModel이라는 서버와 통신하기 위한 dto로서의 코드라는 점을 고려해본다면 협상 신호를 데이터 모델로 매핑하는 일은 데이터 모델이 할 일은 아니라고 생각합니다. 데이터 모델이 비즈니스 로직이 아닌 서버 통신을 위한 로직을 담당하는 것은 좋지 않다고 생각합니다!

Copy link
Member

Choose a reason for hiding this comment

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

현재 커디 프로젝트에는 도메인 모델이 없어서 ActivityType이 도메인 로직을 수행할 것이라고는 생각하지 못했네요!
도메인 로직은 도메인 모델이 수행해야 하지 않을까요?
지금은 서버에서 모든 로직을 처리해주고, 사실상 값만 전달해주기 때문에 데이터 모델로 보고 작업해주셔도 무방하다고 생각이 듭니다.

또한, 위와 같은 상황을 위해, 데이터 모델과 도메인 모델이라는 개념이 각각 존재하는 것이 아닐까 싶네요 : )

}
}

@Serializable
data class MemberActivityApiModel(
val id: Long,
val name: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.emmsale.data.member.dto

import kotlinx.serialization.Serializable

@Serializable
data class MemberWithoutActivitiesApiModel(
val id: Long,
val name: String,
val description: String = "",
val imageUrl: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.emmsale.databinding.ActivityLoginBinding
import com.emmsale.presentation.common.extension.checkPostNotificationPermission
import com.emmsale.presentation.common.extension.showToast
import com.emmsale.presentation.ui.login.uistate.LoginUiState
import com.emmsale.presentation.ui.main.MainActivity
import com.emmsale.presentation.ui.onboarding.OnboardingActivity
import com.emmsale.presentation.utils.builder.uri
import com.google.android.material.snackbar.Snackbar
Expand Down Expand Up @@ -64,8 +65,7 @@ class LoginActivity : AppCompatActivity() {
}

private fun navigateToMain() {
// startActivity(MainActivity.getIntent(this))
finish()
MainActivity.startActivity(this)
}
ki960213 marked this conversation as resolved.
Show resolved Hide resolved

private fun navigateToOnboarding() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.emmsale.presentation.ui.main

import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.emmsale.R
import com.emmsale.databinding.ActivityMainBinding
import com.emmsale.presentation.ui.main.events.EventsFragment
import com.emmsale.presentation.ui.main.myProfile.MyProfileFragment

class MainActivity : AppCompatActivity() {

private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)

initBottomNavigationView()
}

private fun initBottomNavigationView() {
val bnvMain = binding.bnvMain
ki960213 marked this conversation as resolved.
Show resolved Hide resolved

bnvMain.setOnItemSelectedListener {
when(it.itemId) {
R.id.mi_main_profile -> changeFragment(MyProfileFragment())
R.id.mi_main_home -> changeFragment(EventsFragment())
Copy link
Member

Choose a reason for hiding this comment

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

탭을 선택할 때마다 매번 Fragment를 새로 생성하고 있습니다.

EventsFragment는 매번 새로운 데이터를 불러오기 위함일 수 있지만,
MyProfileFragment처럼 서버에서 가져오는 값이 항상 동일한 경우에는 매번 생성할 필요가 없다고 생각해요!

혹시 위와 같이 구현하신 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이때 당시 새로고침 기능을 구현하지 않아서 프래그먼트를 매번 생성하도록 했습니다. 지금은 새로고침 기능을 구현했으니 show/hide로 바꾸겠습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

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

ViewPager 2를 이용하면 더 쉽게 구현 가능할것 같아요! ViewPager 사기입니다

Copy link
Collaborator Author

@ki960213 ki960213 Jul 31, 2023

Choose a reason for hiding this comment

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

화면을 스와이프해서 넘기는 기능은 사용하지 않을거고 단지 탭을 클릭했을 때 다른 프래그먼트를 숨기는 걸 원하는 데, ViewPager를 사용하는 게 더 나을까요? 궁금합니다 😕

Copy link
Member

Choose a reason for hiding this comment

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

화면이 2개라면 ViewPager까지 사용해야 하나 싶네요.
개인적으로 지금 구현도 충분하다고 생각합니다 : )
자칫하다 오버 엔지니어링이 될 수도 있다는 생각이 들어요!

}
return@setOnItemSelectedListener true
}

bnvMain.selectedItemId = R.id.mi_main_home
}

private fun changeFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction().replace(R.id.fcv_main, fragment).commit()
ki960213 marked this conversation as resolved.
Show resolved Hide resolved
}

companion object {
fun startActivity(context: Context) {
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
}
Comment on lines +60 to +63
Copy link
Member

Choose a reason for hiding this comment

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

아래와 같이 한 줄에 작성해도 좋을 것 같아요~!

fun startActivity(context: Context) {
    startActivity(Intent(context, MainActivity::class.java))
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

인텐트를 생성하고 액티비티를 시작하는 두 작업을 한 줄에 작성하는 게 더 보기 좋을까요? ❓ 궁금하네요ㅠㅠ

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.emmsale.presentation.ui.main.events

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.emmsale.R

class EventsFragment : Fragment() {

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_events, container, false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.emmsale.presentation.ui.main.myProfile

import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import com.emmsale.R
import com.emmsale.databinding.FragmentMyProfileBinding
import com.emmsale.presentation.base.fragment.BaseFragment
import com.emmsale.presentation.common.extension.showToast
import com.emmsale.presentation.ui.main.myProfile.adapter.ActivitiesAdapter
import com.emmsale.presentation.ui.main.myProfile.adapter.JobsAdapter
import com.emmsale.presentation.ui.main.myProfile.itemDecoration.ActivitiesAdapterDecoration

class MyProfileFragment : BaseFragment<FragmentMyProfileBinding>() {
override val layoutResId: Int = R.layout.fragment_my_profile

private val viewModel: MyProfileViewModel by viewModels {
MyProfileViewModel.factory
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

initDataBinding()
initUiLogic()
initRecyclerViews()

viewModel.fetchMember()
}

private fun initDataBinding() {
binding.viewModel = viewModel
}

private fun initUiLogic() {
viewModel.uiState.observe(viewLifecycleOwner) {
if (it.isError) {
context?.showToast(it.errorMessage)
}
}
}
ki960213 marked this conversation as resolved.
Show resolved Hide resolved

private fun initRecyclerViews() {
initJobsRecyclerView()
initActivitiesRecyclerView()
}

private fun initJobsRecyclerView() {
binding.rvMyprofileJobs.apply {
adapter = JobsAdapter()
itemAnimator = null
}
}

private fun initActivitiesRecyclerView() {
listOf(
binding.rvMyprofileEducations,
binding.rvMyprofileClubs,
binding.rvMyprofileEvents
).forEach {
it.apply {
adapter = ActivitiesAdapter()
itemAnimator = null
addItemDecoration(ActivitiesAdapterDecoration.getInstance())
}
}
}
}
Loading