diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt index 7aa462a6..28be4da8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt @@ -46,8 +46,9 @@ data class CreatorResponse( @SerializedName("introduce") val introduce: String = "", @SerializedName("instagramUrl") val instagramUrl: String? = null, @SerializedName("youtubeUrl") val youtubeUrl: String? = null, - @SerializedName("websiteUrl") val websiteUrl: String? = null, - @SerializedName("blogUrl") val blogUrl: String? = null, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String? = null, + @SerializedName("fancimmUrl") val fancimmUrl: String? = null, + @SerializedName("xUrl") val xUrl: String? = null, @SerializedName("isAvailableChat") val isAvailableChat: Boolean = true, @SerializedName("isFollow") val isFollow: Boolean, @SerializedName("isNotify") val isNotify: Boolean, @@ -112,5 +113,3 @@ data class GetCreatorActivitySummary( @SerializedName("liveContributorCount") val liveContributorCount: Int, @SerializedName("contentCount") val contentCount: Int ) - - diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt index e48d29b3..1a105ce1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -37,8 +37,9 @@ data class GetRoomDetailManager( @SerializedName("introduce") val introduce: String, @SerializedName("youtubeUrl") val youtubeUrl: String?, @SerializedName("instagramUrl") val instagramUrl: String?, - @SerializedName("websiteUrl") val websiteUrl: String?, - @SerializedName("blogUrl") val blogUrl: String?, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?, + @SerializedName("fancimmUrl") val fancimmUrl: String?, + @SerializedName("xUrl") val xUrl: String?, @SerializedName("profileImageUrl") val profileImageUrl: String, @SerializedName("isCreator") val isCreator: Boolean ) : Parcelable diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt index f7a44c00..c19c7994 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt @@ -33,6 +33,7 @@ import kr.co.vividnext.sodalive.settings.language.LocaleHelper import org.koin.android.ext.android.inject import java.util.Locale import java.util.TimeZone +import androidx.core.net.toUri class LiveRoomDetailFragment( private val roomId: Long, @@ -273,26 +274,14 @@ class LiveRoomDetailFragment( } if ( - manager.websiteUrl.isNullOrBlank() || - !URLUtil.isValidUrl(manager.websiteUrl) + manager.kakaoOpenChatUrl.isNullOrBlank() || + !URLUtil.isValidUrl(manager.kakaoOpenChatUrl) ) { - binding.ivManagerWebsite.visibility = View.GONE + binding.ivManagerOpenChat.visibility = View.GONE } else { - binding.ivManagerWebsite.visibility = View.VISIBLE - binding.ivManagerWebsite.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.websiteUrl))) - } - } - - if ( - manager.blogUrl.isNullOrBlank() || - !URLUtil.isValidUrl(manager.blogUrl) - ) { - binding.ivManagerBlog.visibility = View.GONE - } else { - binding.ivManagerBlog.visibility = View.VISIBLE - binding.ivManagerBlog.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.blogUrl))) + binding.ivManagerOpenChat.visibility = View.VISIBLE + binding.ivManagerOpenChat.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, manager.kakaoOpenChatUrl.toUri())) } } @@ -304,7 +293,7 @@ class LiveRoomDetailFragment( } else { binding.ivManagerInstagram.visibility = View.VISIBLE binding.ivManagerInstagram.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.instagramUrl))) + startActivity(Intent(Intent.ACTION_VIEW, manager.instagramUrl.toUri())) } } @@ -316,7 +305,7 @@ class LiveRoomDetailFragment( } else { binding.ivManagerYoutube.visibility = View.VISIBLE binding.ivManagerYoutube.setOnClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.youtubeUrl))) + startActivity(Intent(Intent.ACTION_VIEW, manager.youtubeUrl.toUri())) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt index aedbe7c8..6dc2fd25 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt @@ -11,8 +11,9 @@ data class GetLiveRoomUserProfileResponse( @SerializedName("gender") val gender: String, @SerializedName("instagramUrl") val instagramUrl: String, @SerializedName("youtubeUrl") val youtubeUrl: String, - @SerializedName("websiteUrl") val websiteUrl: String, - @SerializedName("blogUrl") val blogUrl: String, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String, + @SerializedName("fancimmUrl") val fancimmUrl: String?, + @SerializedName("xUrl") val xUrl: String?, @SerializedName("introduce") val introduce: String, @SerializedName("tags") val tags: String, @SerializedName("isSpeaker") val isSpeaker: Boolean?, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt index 18778fb0..ef337368 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt @@ -12,8 +12,9 @@ data class MyPageResponse( @SerializedName("point") val point: Int, @SerializedName("youtubeUrl") val youtubeUrl: String?, @SerializedName("instagramUrl") val instagramUrl: String?, - @SerializedName("websiteUrl") val websiteUrl: String?, - @SerializedName("blogUrl") val blogUrl: String?, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?, + @SerializedName("fancimmUrl") val fancimmUrl: String?, + @SerializedName("xUrl") val xUrl: String?, @SerializedName("liveReservationCount") val liveReservationCount: Int, @SerializedName("likeCount") val likeCount: Int, @SerializedName("isAuth") val isAuth: Boolean diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileResponse.kt index ddcf9cc5..8d7ddeda 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileResponse.kt @@ -15,8 +15,7 @@ data class ProfileResponse( @SerializedName("rewardCan") val rewardCan: Int, @SerializedName("youtubeUrl") val youtubeUrl: String?, @SerializedName("instagramUrl") val instagramUrl: String?, - @SerializedName("blogUrl") val blogUrl: String?, - @SerializedName("websiteUrl") val websiteUrl: String?, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String?, @SerializedName("fancimmUrl") val fancimmUrl: String?, @SerializedName("xUrl") val xUrl: String?, @SerializedName("introduce") val introduce: String, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt index d854b651..81199311 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt @@ -75,16 +75,6 @@ class ProfileUpdateActivity : BaseActivity( } private fun bindData() { - compositeDisposable.add( - binding.etBlog.textChanges().skip(1) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe { - viewModel.blogUrl = it.toString() - } - ) - compositeDisposable.add( binding.etFancimm.textChanges().skip(1) .debounce(500, TimeUnit.MILLISECONDS) @@ -106,12 +96,12 @@ class ProfileUpdateActivity : BaseActivity( ) compositeDisposable.add( - binding.etWebsite.textChanges().skip(1) + binding.etOpenChat.textChanges().skip(1) .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe { - viewModel.websiteUrl = it.toString() + viewModel.kakaoOpenChatUrl = it.toString() } ) @@ -160,8 +150,7 @@ class ProfileUpdateActivity : BaseActivity( binding.tvNickname.text = it.nickname it.youtubeUrl?.let { url -> binding.etYoutube.setText(url) } it.instagramUrl?.let { url -> binding.etInstagram.setText(url) } - it.websiteUrl?.let { url -> binding.etWebsite.setText(url) } - it.blogUrl?.let { url -> binding.etBlog.setText(url) } + it.kakaoOpenChatUrl?.let { url -> binding.etOpenChat.setText(url) } it.fancimmUrl?.let { url -> binding.etFancimm.setText(url) } it.xUrl?.let { url -> binding.etX.setText(url) } binding.etIntroduce.setText(it.introduce) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateRequest.kt index 0232e1a7..57d3c915 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateRequest.kt @@ -17,8 +17,7 @@ data class ProfileUpdateRequest( @SerializedName("introduce") val introduce: String? = null, @SerializedName("youtubeUrl") val youtubeUrl: String? = null, @SerializedName("instagramUrl") val instagramUrl: String? = null, - @SerializedName("websiteUrl") val websiteUrl: String? = null, - @SerializedName("blogUrl") val blogUrl: String? = null, + @SerializedName("kakaoOpenChatUrl") val kakaoOpenChatUrl: String? = null, @SerializedName("fancimmUrl") val fancimmUrl: String? = null, @SerializedName("xUrl") val xUrl: String? = null, @SerializedName("isVisibleDonationRank") val isVisibleDonationRank: Boolean? = null, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt index 81f38659..3742d81e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt @@ -20,8 +20,7 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM var youtubeUrl = "" var instagramUrl = "" - var websiteUrl = "" - var blogUrl = "" + var kakaoOpenChatUrl = "" var fancimmUrl = "" var xUrl = "" var introduce = "" @@ -67,8 +66,7 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM profileResponse = it.data youtubeUrl = profileResponse.youtubeUrl ?: "" instagramUrl = profileResponse.instagramUrl ?: "" - websiteUrl = profileResponse.websiteUrl ?: "" - blogUrl = profileResponse.blogUrl ?: "" + kakaoOpenChatUrl = profileResponse.kakaoOpenChatUrl ?: "" fancimmUrl = profileResponse.fancimmUrl ?: "" xUrl = profileResponse.xUrl ?: "" introduce = profileResponse.introduce @@ -134,8 +132,7 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM if ( profileResponse.youtubeUrl != youtubeUrl || profileResponse.instagramUrl != instagramUrl || - profileResponse.blogUrl != blogUrl || - profileResponse.websiteUrl != websiteUrl || + profileResponse.kakaoOpenChatUrl != kakaoOpenChatUrl || profileResponse.fancimmUrl != fancimmUrl || profileResponse.xUrl != xUrl || profileResponse.gender != _genderLiveData.value || @@ -156,13 +153,8 @@ class ProfileUpdateViewModel(private val repository: UserRepository) : BaseViewM } else { null }, - blogUrl = if (profileResponse.blogUrl != blogUrl) { - blogUrl - } else { - null - }, - websiteUrl = if (profileResponse.websiteUrl != websiteUrl) { - websiteUrl + kakaoOpenChatUrl = if (profileResponse.kakaoOpenChatUrl != kakaoOpenChatUrl) { + kakaoOpenChatUrl } else { null }, diff --git a/app/src/main/res/layout/activity_profile_update.xml b/app/src/main/res/layout/activity_profile_update.xml index 6df6b1f2..1205f9de 100644 --- a/app/src/main/res/layout/activity_profile_update.xml +++ b/app/src/main/res/layout/activity_profile_update.xml @@ -367,50 +367,17 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="6.7dp" android:fontFamily="@font/medium" - android:text="@string/screen_profile_update_website_label" + android:text="@string/screen_profile_update_open_chat_label" android:textColor="@color/color_eeeeee" android:textSize="12sp" /> - - - - - - - - - - - Instagram URL YouTube channel YouTube channel URL - Website - Website URL - Blog - Blog URL + Open Chat + Open Chat URL FancimM FancimM URL X diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e96af4bb..88c39770 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -660,10 +660,8 @@ Instagram URL YouTubeチャンネル YouTubeチャンネル URL - ウェブサイト - ウェブサイト URL - ブログ - ブログ URL + オープンチャット + オープンチャット URL FancimM FancimM URL X diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d0cdc14..858a359a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -659,10 +659,8 @@ 인스타그램 URL 유튜브 채널 유튜브 채널 URL - 웹사이트 - 웹사이트 URL - 블로그 - 블로그 URL + 오픈채팅 + 오픈채팅 URL 팬심M 팬심M URL X diff --git a/docs/20260224_프로필SNS오픈채팅전환.md b/docs/20260224_프로필SNS오픈채팅전환.md new file mode 100644 index 00000000..1fcba13d --- /dev/null +++ b/docs/20260224_프로필SNS오픈채팅전환.md @@ -0,0 +1,61 @@ +- [x] 1단계: 프로필 SNS 도메인 필드를 `websiteUrl`/`blogUrl`에서 `kakaoOpenChatUrl`로 전환한다. + - 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateRequest.kt`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateViewModel.kt` +- [x] 2단계: 프로필 수정 화면 입력 항목을 `instagram`, `youtube`, `kakaoOpenChatUrl`, `fancimm`, `x` 순서로 정리한다. + - 대상 파일: `app/src/main/res/layout/activity_profile_update.xml`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/profile/ProfileUpdateActivity.kt` +- [x] 3단계: 프로필 수정 화면 문자열 리소스에서 Website/Blog 라벨·힌트를 제거하고 OpenChat 문구를 추가한다. + - 대상 파일: `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml` +- [x] 4단계: 크리에이터/유저 프로필 조회 응답 모델의 SNS 필드 구성을 동일 규격으로 맞춘다. + - 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt` +- [x] 5단계: 프로필 SNS 노출 UI(상세/라이브 상세)에서 Website/Blog 노출 및 클릭 처리를 OpenChat 기준으로 교체한다. + - 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt`, `app/src/main/res/layout/fragment_live_room_detail.xml`, `app/src/main/res/layout/layout_user_profile.xml` + - 아이콘 점검 파일: `app/src/main/res/drawable-xxhdpi/ic_website_blue.png`, `app/src/main/res/drawable-xxhdpi/ic_blog_blue.png`, `app/src/main/res/drawable-xxhdpi/ic_website_circle.png`, `app/src/main/res/drawable-xxhdpi/ic_blog_circle.png`, `app/src/main/res/drawable-xxhdpi/ic_login_kakao.png` +- [x] 6단계: 변경 영향 범위 컴파일/테스트를 수행하고 결과를 문서 하단 검증 기록에 누적한다. + - 대상 명령: `./gradlew :app:testDebugUnitTest`, `./gradlew :app:assembleDebug` +- [x] 7단계: 기존 SNS URL 필드 구성 지점에 `fancimmUrl`, `xUrl`를 추가 반영한다. + - 대상 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt`, `app/src/main/java/kr/co/vividnext/sodalive/mypage/MyPageResponse.kt` + +## 검증 기록 + +### 1) 작업계획 문서 작성 +- 무엇: 프로필 SNS 필드 전환 및 프로필 수정 UI 변경 작업을 단계별 체크리스트로 정의. +- 왜: 일부만 반영된 상태에서 누락 파일 없이 일관되게 마이그레이션하기 위함. +- 어떻게: + - `websiteUrl`, `blogUrl`, `kakaoOpenChatUrl`, `et_website`, `et_blog` 키워드 기준으로 Kotlin/XML 사용처를 탐색. + - 단계별로 실제 수정 후보 파일 경로를 체크리스트에 연결. +- 결과: + - 계획 문서 `docs/20260224_프로필SNS오픈채팅전환.md` 생성 완료. + - 구현은 아직 미수행(체크박스 전체 미완료 상태 유지). + +### 2) 프로필 SNS 필드/수정 UI 오픈채팅 전환 구현 +- 무엇: SNS 필드 규격을 `instagram`, `youtube`, `kakaoOpenChatUrl`, `fancimm`, `x`로 통일하고 프로필 수정 UI에서 Website/Blog 입력을 제거 후 Open Chat 입력으로 대체. +- 왜: 서버/클라이언트 모델 및 UI의 SNS 항목을 동일 스펙으로 맞추고, 부분 반영 상태를 해소하기 위함. +- 어떻게: + - 모델/요청/뷰모델(`ProfileUpdateRequest`, `ProfileResponse`, `ProfileUpdateViewModel`)의 `websiteUrl`/`blogUrl` 사용부를 `kakaoOpenChatUrl`로 변경. + - 프로필 수정 화면(`activity_profile_update.xml`, `ProfileUpdateActivity.kt`)에서 `et_website`/`et_blog` 제거 후 `et_open_chat` 입력으로 교체. + - 조회 응답 모델(`GetCreatorProfileResponse`, `GetLiveRoomUserProfileResponse`, `GetRoomDetailResponse`, `MyPageResponse`) 필드명 통일. + - 노출 UI(`fragment_live_room_detail.xml`, `layout_user_profile.xml`)와 클릭 처리(`LiveRoomDetailFragment.kt`)를 오픈채팅 기준으로 변경. + - 문자열 리소스(`values/strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`)에서 Website/Blog 문구 제거 후 Open Chat 문구 추가. +- 결과: + - app/src/main 기준 `websiteUrl`, `blogUrl`, `et_website`, `et_blog`, 관련 문자열 키가 제거되고 `kakaoOpenChatUrl`/`et_open_chat`/`screen_profile_update_open_chat_*` 기준으로 정렬됨. + +### 3) 진단/테스트/빌드 검증 +- 무엇: 변경 파일 정합성과 빌드 안정성을 검증. +- 왜: 필드명/뷰 ID/바인딩 변경으로 인한 컴파일 오류를 사전에 확인하기 위함. +- 어떻게: + - `lsp_diagnostics`를 수정된 Kotlin/XML 파일 전체에 실행 시도. + - `./gradlew :app:testDebugUnitTest :app:assembleDebug` 실행. +- 결과: + - LSP: 현재 환경에서 Kotlin/XML LSP 미설정으로 진단 도구 사용 불가(`No LSP server configured for extension: .kt/.xml`). + - Gradle: `BUILD SUCCESSFUL` (unit test + debug assemble 통과). + +### 4) `fancimmUrl`/`xUrl` 필드 추가 반영 +- 무엇: 기존 SNS URL 필드가 정의된 응답 모델 구간에 `fancimmUrl`, `xUrl`를 추가. +- 왜: SNS URL 필드 스키마를 모델 간 일관되게 유지하고, 서버 응답 확장 시 누락 파싱을 방지하기 위함. +- 어떻게: + - `GetCreatorProfileResponse.CreatorResponse`, `GetLiveRoomUserProfileResponse`, `GetRoomDetailManager`, `MyPageResponse`에 `@SerializedName("fancimmUrl")`, `@SerializedName("xUrl")` 필드를 추가. + - `grep`으로 `fancimmUrl|xUrl` 사용처를 재탐색해 대상 파일에 반영 여부 확인. + - `lsp_diagnostics` 실행 시도 후 `./gradlew :app:testDebugUnitTest :app:assembleDebug` 수행. +- 결과: + - 대상 4개 모델 파일에 `fancimmUrl`, `xUrl` 필드 추가 완료. + - LSP: Kotlin LSP 미설정으로 진단 도구 사용 불가(`No LSP server configured for extension: .kt`). + - Gradle: `BUILD SUCCESSFUL` (unit test + debug assemble 통과).