Skip to content

[Feat] API 통신 구조 세팅 및 프로필 키워드 조회 API 연동#135

Merged
nahy-512 merged 20 commits into
developfrom
feat/#128-api-setting
Jan 19, 2026
Merged

[Feat] API 통신 구조 세팅 및 프로필 키워드 조회 API 연동#135
nahy-512 merged 20 commits into
developfrom
feat/#128-api-setting

Conversation

@nahy-512
Copy link
Copy Markdown
Contributor

@nahy-512 nahy-512 commented Jan 19, 2026

📮 관련 이슈

📌 작업 내용

  • 구권아 기반으로 레이어 나누다보니까 mapper랑 repository 위치가 애매해지는 것 같아, 우선은 repository를 클래스로 가져가고, 직접 구현하기로 했습니다.
  • 서버 통신 구조 세팅하는 김에 화면들 합치고, 화면 이동 및 구현 잘 보기 위해 더미 데이터 넣어놓았어요.

📸 스크린샷

화면 이동 프로필 키워드 조회 API
Screen_Recording_20260119-090842_Flint.1.mp4
Screen_Recording_20260119-092703_Flint.mp4

😅 미구현

  • 네비게이션 뒤로가기 스택 관리
  • 프로필 세세한 로직 (키워드 Row 배치 등)

🫛 To. 리뷰어

  • 원래 내 프로필에 리프레시 버튼 빼기로 했는데, 로직 구현 참고하라고 임시로 넣어놨어요.
  • UiModel 말고, domain 레이어의 Model 사용하게끔 진행해주시면 됩니다! 논의 배경 및 컨벤션 관련 문서 업데이트 해둘게요.
  • 컨벤션 참고 문서: https://www.notion.so/2dc50cdb714e804e890cd07bc824a8a0

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 사용자 프로필에 키워드 섹션 추가
    • 저장된 콘텐츠 관리 기능 강화
    • 로딩 상태 표시 개선
  • 개선 사항

    • 애플리케이션 전반의 콘텐츠 관리 용어 통일
    • API 인프라 확장 및 안정화
    • 저장소 구조 정리 및 최적화
    • 프로필 화면 사용자 경험 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@nahy-512 nahy-512 self-assigned this Jan 19, 2026
@nahy-512 nahy-512 requested a review from a team as a code owner January 19, 2026 00:29
@nahy-512 nahy-512 added 🔖 API feat - API 연동 Refactor ♻️ 기능 변경 없이 코드 구조를 개선하여 가독성, 유지보수성, 성능을 향상 labels Jan 19, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

API 설계 구조를 개선하기 위해 의존성 주입 모듈을 재정렬하고, 단일 샘플 API에서 도메인별 API로 마이그레이션했습니다. DTO 및 도메인 모델 패키지를 재구성하고, 영화 관련 용어를 콘텐츠로 통일하며, 새로운 리포지토리와 사용자 키워드 조회 기능을 추가했습니다.

Changes

Cohort / File(s) 변경 사항
DI 모듈 리팩토링
app/src/main/java/com/flint/core/common/di/RepositoryModule.kt, SampleModule.kt, ServiceModule.kt
Hilt @Module 제거, 새로운 API provider 메서드 6개(UserApi, BookmarkApi, CollectionApi, ContentApi, HomeApi, SearchApi) 추가
API 인터페이스 재설계
app/src/main/java/com/flint/data/api/FlintApi.kt (삭제), UserApi.kt, BookmarkApi.kt, CollectionApi.kt, ContentApi.kt, HomeApi.kt, SearchApi.kt (신규), AuthApi.kt
샘플 FlintApi 제거, 도메인별 6개 새로운 API 인터페이스 추가, AuthApi 임포트 경로 업데이트
DTO 패키지 재구성
app/src/main/java/com/flint/data/dto/auth/request/*, auth/response/*, user/response/*
패키지 경로 변경(request.auth → auth.request), 샘플 DTO(SampleRequestDto, SampleResponseDto) 제거, UserKeywordsResponseDto 신규 추가
매퍼 재구성
app/src/main/java/com/flint/data/mapper/SampleMapper.kt (삭제), auth/SignupMapper.kt, auth/SocialVerifyMapper.kt (삭제), domain/mapper/auth/* (신규), user/KeywordMapper.kt (신규)
데이터 레이어 매퍼 제거, 도메인 레이어 매퍼로 이동
리포지토리 리팩토링
app/src/main/java/com/flint/data/repositoryImpl/DefaultAuthRepository.kt, DefaultFlintRepository.kt (삭제), app/src/main/java/com/flint/data/util/SafeApiCall.kt (삭제), domain/repository/AuthRepository.kt, UserRepository.kt, BookmarkRepository.kt, CollectionRepository.kt, ContentRepository.kt, HomeRepository.kt, SearchRepository.kt (신규)
구체적 클래스 제거, 도메인 리포지토리 클래스로 통합, 새로운 suspendRunCatching 유틸 추가
도메인 모델 패키지화
app/src/main/java/com/flint/domain/model/collection/CollectionModel.kt, collection/CollectionDetailModel.kt (신규), content/ContentModel.kt, user/AuthorModel.kt, user/UserKeywordResponseModel.kt (신규), OttModel.kt, PreferenceKeywordModel.kt (삭제)
도메인 모델 서브패키지화(collection, content, user), 테스트 데이터(Fake/FakeList) 추가, 일부 모델 삭제
Enum 타입 변경
app/src/main/java/com/flint/domain/type/PreferenceType.kt, OttType.kt, ProviderType.kt
PreferenceType 상수 대소문자 통일(Green→GREEN), OttType.getOtts() 메서드 제거
UI 화면 이름 통일 (Film → Content)
app/src/main/java/com/flint/presentation/collectioncreate/*, onboarding/*, savedcontent/* (신규), savedfilm/* (삭제)
파일명/함수명/클래스명 일관성 있게 변경, 경로 이동(savedfilm→savedcontent)
프로필 기능 추가
app/src/main/java/com/flint/presentation/profile/ProfileUiState.kt (신규), ProfileViewModel.kt (신규), ProfileScreen.kt, component/ProfileKeywordSection.kt
새로운 UI State 및 ViewModel 추가, 사용자 키워드 조회 기능 구현, 컴포넌트 모델 업데이트
탐색/라우팅 업데이트
app/src/main/java/com/flint/core/navigation/Route.kt, presentation/main/MainNavigator.kt, MainNavHost.kt, profile/navigation/ProfileNavigation.kt, collectioncreate/navigation/CollectionCreateNavigation.kt, onboarding/navigation/OnboardingNavigation.kt, savedcontent/navigation/SavedContentNavigation.kt (신규)
Route 이름 변경(Film→Content), 탐색 함수 이름 업데이트, 새로운 SavedContent 네비게이션 추가
기타 화면 업데이트
presentation/home/HomeScreen.kt, collectiondetail/CollectionDetailScreen.kt, collectionlist/CollectionListScreen.kt, explore/ExploreScreen.kt, login/LoginViewModel.kt
임포트 경로 업데이트, 매개변수 추가/수정, 샘플 데이터 적용

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProfileRoute
    participant ProfileViewModel
    participant UserRepository
    participant UserApi
    participant Backend

    User->>ProfileRoute: 프로필 화면 진입
    ProfileRoute->>ProfileViewModel: hiltViewModel() 생성
    ProfileViewModel->>ProfileViewModel: loadInitialData() 호출
    ProfileViewModel->>UserRepository: getUserKeywords(userId)
    UserRepository->>UserApi: getUserKeywords(userId)
    UserApi->>Backend: GET /api/v1/users/{userId}/keywords
    Backend-->>UserApi: UserKeywordsResponseDto
    UserApi-->>UserRepository: Result<List<UserKeywordResponseModel>>
    UserRepository->>UserRepository: DTO를 Model로 매핑
    UserRepository-->>ProfileViewModel: Result<List<UserKeywordResponseModel>>
    ProfileViewModel->>ProfileViewModel: UiState.Success로 업데이트
    ProfileViewModel-->>ProfileRoute: StateFlow<UiState<ProfileUiState>>
    ProfileRoute->>User: ProfileScreen 렌더링
Loading
sequenceDiagram
    actor User
    participant ServiceModule
    participant Retrofit
    participant ApiService
    participant NetworkLayer

    User->>ServiceModule: 의존성 주입 초기화
    ServiceModule->>ServiceModule: provideUserApi(retrofit)
    ServiceModule->>ServiceModule: provideBookmarkApi(retrofit)
    ServiceModule->>ServiceModule: provideCollectionApi(retrofit)
    ServiceModule->>ServiceModule: provideContentApi(retrofit)
    ServiceModule->>ServiceModule: provideHomeApi(retrofit)
    ServiceModule->>ServiceModule: provideSearchApi(retrofit)
    ServiceModule->>Retrofit: Retrofit.create(API 인터페이스)
    Retrofit-->>ServiceModule: API 구현체
    ServiceModule-->>ApiService: `@Singleton으로` 캐싱
    ApiService->>NetworkLayer: API 호출 준비 완료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50분

Possibly related PRs

Suggested labels

Feat ✨, ⚙️ Setting

Suggested reviewers

  • giovannijunseokim
  • kimjw2003

Poem

🐰 API 구조를 다시 짜니,
도메인별로 깔끔하게 정렬되고,
영화는 이제 콘텐츠가 되어,
프로필에선 키워드가 반짝반짝,
새로운 리포지토리들이 일 나누며,
깨끗한 아키텍처를 향해 깡충! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항(API 통신 구조 세팅 및 프로필 키워드 조회 API 연동)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 관련 이슈, 작업 내용, 스크린샷, 미구현 사항, 리뷰어 참고사항을 포함하여 템플릿 구조를 대부분 따르고 있습니다.
Linked Issues check ✅ Passed PR이 연결된 이슈 #128의 주요 목표들(구조 변경, 예시 API 연동, 컨벤션 문서 업데이트)을 충족하고 있습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 API 통신 구조 세팅, 프로필 키워드 조회 API 연동, 화면 통합 등 연결된 이슈의 범위 내에 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/src/main/java/com/flint/presentation/home/HomeScreen.kt (1)

36-55: 프로덕션 라우트에 더미 데이터/미구현 핸들러가 남아 있음

HomeRoute가 실제 화면 진입점인데 CollectionModel.FakeList/ContentModel.FakeListTODO 핸들러를 넘기고 있어 API 연동·탐색 이동·저장 콘텐츠 동작이 모두 무력화됩니다. 최소한 라우트 파라미터로 실제 상태/핸들러를 주입해 실제 플로우를 보장해야 합니다.

🔧 제안 수정안 (이 파일 기준)
 fun HomeRoute(
     paddingValues: PaddingValues,
+    recommendCollectionModelList: ImmutableList<CollectionModel>,
+    savedContentModelList: ImmutableList<ContentModel>,
+    recentCollectionModelList: ImmutableList<CollectionModel>,
     navigateToCollectionList: () -> Unit,
     navigateToCollectionDetail: (collectionId: String) -> Unit,
     navigateToCollectionCreate: () -> Unit,
+    navigateToExplore: () -> Unit,
+    onSavedContentItemClick: (contentId: Long) -> Unit,
 ) {
     HomeScreen(
-        recentCollectionModelList = CollectionModel.FakeList,
-        recommendCollectionModelList = CollectionModel.FakeList,
-        savedContentModelList = ContentModel.FakeList,
+        recentCollectionModelList = recentCollectionModelList,
+        recommendCollectionModelList = recommendCollectionModelList,
+        savedContentModelList = savedContentModelList,
         navigateToCollectionCreate = {
             navigateToCollectionCreate()
         },
-        navigateToExplore = {
-            // TODO navigate to explore
-        },
+        navigateToExplore = navigateToExplore,
         onRecentCollectionItemClick = { collectionId ->
             navigateToCollectionDetail(collectionId)
         },
         onRecentCollectionAllClick = navigateToCollectionList,
         onRecommendCollectionItemClick = { collectionId ->
             navigateToCollectionDetail(collectionId)
         },
-        onSavedContentItemClick = { contentId ->
-            // TODO show OttListBottomSheet
-        },
+        onSavedContentItemClick = onSavedContentItemClick,
         modifier = Modifier.padding(paddingValues),
     )
 }
app/src/main/java/com/flint/core/designsystem/component/listView/SavedContentsSection.kt (1)

72-83: 접근성: 클릭 아이콘에 설명 추가와 터치 영역 확대 필요

contentDescription = null이면 스크린리더에서 버튼의 기능을 알 수 없습니다. 또한 paddingnoRippleClickable 이후에 적용되어 실제 클릭 가능한 영역이 12.dp만큼 감소합니다. 터치 타겟 최소 기준(48.dp)을 고려하여 접근성을 개선해야 합니다.

수정 제안
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.res.stringResource
@@
                 Icon(
                     imageVector = ImageVector.vectorResource(R.drawable.ic_more),
-                    contentDescription = null,
+                    contentDescription = stringResource(R.string.content_more),
                     tint = Color.Unspecified,
                     modifier =
                         Modifier
                             .size(48.dp)
+                            .padding(12.dp)
-                            .noRippleClickable {
-                                onAllClick()
-                            }.padding(12.dp),
+                            .noRippleClickable {
+                                onAllClick()
+                            },
                 )

./app/src/main/res/values/strings.xml에 아래 문자열을 추가하세요:

<string name="content_more">더보기</string>
app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt (1)

70-87: Route가 실제 데이터/네비게이션을 무시하고 고정 더미로 동작합니다.

collectionId/navigateToCollectionList가 사용되지 않고, 화면이 하드코딩된 샘플로 고정되어 실제 API 연동 흐름을 막습니다. 릴리즈 전에는 ViewModel 상태 기반 데이터로 교체하고, 더미는 Preview/디버그 전용으로 분리하는 게 안전합니다.

app/src/main/java/com/flint/domain/model/collection/CollectionModel.kt (1)

16-46: FakeList의 collectionId가 모두 빈 문자열입니다.

LazyVerticalGrid에서 key = { it.collectionId }를 사용하고 있는데, FakeList의 두 아이템 모두 collectionId = ""로 동일합니다. 이는 Compose에서 중복 키 경고 또는 예상치 못한 동작을 유발할 수 있습니다.

🐛 FakeList에 고유한 ID 부여
 val FakeList =
     persistentListOf(
         CollectionModel(
-            collectionId = "",
+            collectionId = "fake-1",
             collectionTitle = "컬렉션 제목",
             ...
         ),
         CollectionModel(
-            collectionId = "",
+            collectionId = "fake-2",
             collectionTitle = "컬렉션 제목2",
             ...
         ),
     )
🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/data/api/UserApi.kt`:
- Line 6: UserApi.kt contains an unused import: remove the unused import
statement "import retrofit2.http.Header" from the top of UserApi.kt; ensure no
remaining references to Header are needed by any functions or annotations in the
UserApi interface (e.g., methods inside UserApi), and run a build/IDE
auto-import cleanup to verify there are no other unused imports.

In
`@app/src/main/java/com/flint/data/dto/user/response/UserKeywordsResponseDto.kt`:
- Around line 12-24: The UserKeywordsResultDto data class currently defines
non-nullable properties without defaults, which will fail deserialization if API
returns null; update UserKeywordsResultDto by either assigning sensible default
values (e.g., empty string, 0, 0.0f) to properties color, rank, name,
percentage, imageUrl or make those properties nullable (String?, Int?, Float?)
to match SocialVerifyResponseDto style so coerceInputValues=true can safely
handle missing/null fields during JSON deserialization.

In `@app/src/main/java/com/flint/domain/model/user/UserKeywordResponseModel.kt`:
- Around line 14-15: The current declaration inside UserKeywordResponseModel
uses a member extension syntax (val UserKeywordResponseModel.preferenceType)
which prevents instance.property access and also calls
PreferenceType.valueOf(color) which throws on unknown values; change it to a
normal property on the class (e.g., val preferenceType: PreferenceType) and
implement safe parsing of color (use PreferenceType.values().firstOrNull {
it.name == color } ?: fallback or runCatching { PreferenceType.valueOf(color)
}.getOrElse { /* fallback */ }) so invalid color strings don't throw.

In `@app/src/main/java/com/flint/domain/repository/AuthRepository.kt`:
- Around line 1-22: The current AuthRepository class in the domain layer depends
on data.api.AuthApi, violating clean architecture; change AuthRepository to an
interface in the domain package (interface AuthRepository with suspend fun
signup(...) and suspend fun socialVerify(...), referencing SignupRequestModel,
SignupResponseModel, SocialVerifyRequestModel, SocialVerifyResponseModel), then
create a new implementation class (e.g., AuthRepositoryImpl) in the data layer
that injects AuthApi and implements those methods by delegating to
api.signup(...).data.toModel() and api.socialVerify(...).data.toModel(); update
DI bindings to bind AuthRepository to AuthRepositoryImpl and remove direct data
imports from the domain file.
- Line 10: AuthRepository.kt (and likewise ProfileViewModel.kt) currently import
jakarta.inject.Inject; change those imports to use javax.inject.Inject so DI
annotation usage matches the rest of the project (ServiceModule, NetworkModule,
DataStoreModule, etc.). Locate the import line in AuthRepository.kt and
ProfileViewModel.kt and replace jakarta.inject.Inject with javax.inject.Inject,
keeping the `@Inject` annotations on constructors/fields unchanged.

In
`@app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt`:
- Around line 150-166: rotateKeywordByRank currently uses
topRanks.zip(bottomRanks) which drops any remaining items when sizes differ
(losing keywords for odd counts); update the interleaving logic to iterate up to
max(topRanks.size, bottomRanks.size) and append pairs where present (or
alternately append the leftover items after the zip), ensuring all items from
sortedByRank are preserved; change the code around topRanks, bottomRanks and the
final flatMap/toPersistentList return to include remaining entries from the
longer list so no keywords are lost.
🧹 Nitpick comments (11)
app/src/main/java/com/flint/data/api/HomeApi.kt (1)

3-5: 빈 인터페이스는 필요 시점에 메서드 정의 후 추가를 권장합니다.
현재는 메서드가 없어 역할이 불명확하니, 실제 엔드포인트 정의가 준비되면 추가하거나 TODO/이슈로 추적하는 편이 명확합니다.

app/src/main/java/com/flint/data/api/ContentApi.kt (1)

3-8: 실제 엔드포인트 정의 전까지는 추가를 보류하는 편이 명확합니다.
주석만 있는 빈 API는 사용처에 혼란을 줄 수 있어, 메서드 정의가 준비되면 추가하거나 TODO/이슈로 추적해 주세요.

app/src/main/java/com/flint/data/api/SearchApi.kt (1)

3-5: 메서드 정의가 준비된 시점에 추가하는 것을 권장합니다.
현재 상태로는 역할이 모호하니 실제 계약이 준비되면 도입하거나 TODO/이슈로 관리해 주세요.

app/src/main/java/com/flint/data/api/BookmarkApi.kt (1)

3-9: 빈 인터페이스 스캐폴드입니다.

현재 이 인터페이스는 메서드 시그니처 없이 주석만 포함되어 있습니다. API 구조 세팅 단계에서 의도적인 것으로 보이지만, 추후 구현 시 누락되지 않도록 TODO 주석이나 이슈 트래킹이 필요할 수 있습니다.

메서드 시그니처를 미리 정의해두시겠습니까? 예시:

interface BookmarkApi {
    // 컬렉션 북마크 유저 조회
    suspend fun getCollectionBookmarkUsers(collectionId: Long): Response<...>

    // 컬렉션 북마크 토글
    suspend fun toggleCollectionBookmark(collectionId: Long): Response<...>

    // 콘텐츠 북마크 토글
    suspend fun toggleContentBookmark(contentId: Long): Response<...>
}
app/src/main/java/com/flint/data/api/CollectionApi.kt (1)

3-10: 빈 인터페이스 노출 최소화 제안

시그니처 없이 public 인터페이스만 추가되면 의미 없는 타입 의존이 생길 수 있습니다. 실제 계약이 확정되기 전이라면 TODO/이슈 링크로 의도를 명확히 하거나, 준비 완료 시점에 추가하는 방식을 고려해 주세요.

app/src/main/java/com/flint/core/designsystem/component/indicator/FlintLoadingIndicator.kt (1)

12-22: 재사용성을 위해 modifier를 인자로 받도록 개선

현재는 항상 전체 화면을 채워 다른 맥락에서 재사용하기 어렵습니다. modifier(및 필요 시 배경색)를 인자로 받아 유연하게 쓰도록 해 주세요.

♻️ 개선 예시
 `@Composable`
-fun FlintLoadingIndicator() {
+fun FlintLoadingIndicator(
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = Color.Transparent,
+) {
     Box(
         modifier =
-            Modifier
+            modifier
                 .fillMaxSize()
-                .background(Color.Transparent),
+                .background(backgroundColor),
         contentAlignment = Alignment.Center,
     ) {
         CircularProgressIndicator()
     }
 }
app/src/main/java/com/flint/domain/type/PreferenceType.kt (1)

16-39: API 색상 문자열 대소문자 계약 확인 + 안전 매핑 헬퍼 권장.

valueOf를 사용하는 호출부가 있다면, 서버가 항상 대문자를 준다는 보장이 없을 때 런타임 예외가 납니다. 대소문자 무시 매핑 헬퍼를 추가하고 호출부에서 사용하도록 권장합니다.

♻️ 안전 매핑 헬퍼 예시
 enum class PreferenceType(
     val color: Color,
     `@DrawableRes` val backgroundRes: Int,
 ) {
     GREEN(...),
     ORANGE(...),
     YELLOW(...),
     BLUE(...),
     PINK(...),
+    ;
+
+    companion object {
+        fun fromColor(color: String): PreferenceType? =
+            values().firstOrNull { it.name.equals(color, ignoreCase = true) }
+    }
 }
app/src/main/java/com/flint/domain/mapper/user/KeywordMapper.kt (1)

1-13: Clean Architecture 레이어 의존성 위반

도메인 레이어(com.flint.domain)에서 데이터 레이어(com.flint.data.dto)를 직접 import하고 있습니다. Clean Architecture 원칙에 따르면 도메인 레이어는 외부 레이어에 의존하지 않아야 합니다.

매퍼를 데이터 레이어(com.flint.data.mapper)로 이동하는 것을 권장합니다.

♻️ 제안하는 구조 변경
-package com.flint.domain.mapper.user
+package com.flint.data.mapper.user

import com.flint.data.dto.user.response.UserKeywordsResultDto
import com.flint.domain.model.user.UserKeywordResponseModel

fun UserKeywordsResultDto.toModel(): UserKeywordResponseModel =
    UserKeywordResponseModel(
        color = color,
        rank = rank,
        name = name,
        percentage = percentage,
        imageUrl = imageUrl,
    )
app/src/main/java/com/flint/domain/repository/UserRepository.kt (1)

1-13: 도메인 레이어에서 데이터 레이어 의존

UserRepository가 도메인 패키지에 있지만 데이터 레이어의 UserApi를 직접 의존하고 있습니다. 일반적인 Clean Architecture에서는 Repository 구현체를 데이터 레이어에 두고, 도메인 레이어에는 인터페이스만 정의합니다.

PR 목표에 "repository를 클래스 형태로 직접 구현하도록 구조 조정"이 언급되어 있어 의도된 설계로 보이지만, 향후 테스트 용이성과 레이어 분리를 위해 인터페이스 분리를 고려해 주세요.

app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (1)

41-57: 리프레시 시 로딩 상태 및 에러 처리 개선이 필요합니다.

현재 구현에서 두 가지 개선점이 있습니다:

  1. API 호출 전 로딩 상태(UiState.Loading)를 emit하지 않아 사용자에게 피드백이 없습니다.
  2. 실패 시 UiState.Failure로 전체 상태가 덮어씌워져 기존 프로필 데이터가 손실됩니다.
♻️ 제안된 개선
 fun refreshProfileKeyword() {
     viewModelScope.launch {
+        // 옵션 1: 로딩 상태 표시
+        // _uiState.emit(UiState.Loading)
+        
         userRepository.getUserKeywords(userId = 800370427074376635).fold(
             onFailure = {
                 Timber.d("onFailure: $it")
-                _uiState.emit(UiState.Failure)
+                // 기존 상태 유지하면서 에러 로깅만 수행하거나,
+                // 별도의 에러 이벤트 채널 사용 고려
+                // _uiState.emit(UiState.Failure)
             },
             onSuccess = { result ->
                 Timber.d("onSuccess: $result")
                 _uiState.updateSuccess {
                     it.copy(
                         keywords = result.toImmutableList(),
                     )
                 }
             },
         )
     }
 }
app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt (1)

38-54: Empty 및 Failure 상태에 대한 UI 피드백이 누락되었습니다.

UiState.EmptyUiState.Failure 상태가 else -> {}로 처리되어 사용자에게 아무런 피드백이 표시되지 않습니다. 최소한 에러 메시지나 재시도 버튼을 표시하는 것이 좋습니다.

♻️ 제안된 개선
 when (val state = uiState) {
     is UiState.Loading -> {
         FlintLoadingIndicator()
     }

     is UiState.Success -> {
         ProfileScreen(
             modifier = Modifier.padding(paddingValues),
             uiState = state.data,
             onRefreshClick = viewModel::refreshProfileKeyword,
             onCollectionItemClick = navigateToCollectionDetail,
             onCollectionMoreClick = navigateToCollectionList,
         )
     }

-    else -> {}
+    is UiState.Failure -> {
+        // TODO: 에러 UI 구현 (재시도 버튼 등)
+    }
+
+    is UiState.Empty -> {
+        // 초기 로딩 또는 빈 상태 처리
+    }
 }

Comment thread app/src/main/java/com/flint/data/api/UserApi.kt Outdated
Comment on lines +12 to +24
@Serializable
data class UserKeywordsResultDto(
@SerialName("color")
val color: String,
@SerialName("rank")
val rank: Int,
@SerialName("name")
val name: String,
@SerialName("percentage")
val percentage: Float,
@SerialName("imageUrl")
val imageUrl: String,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

kotlinx.serialization null values non-nullable fields deserialization

💡 Result:

Short answer

  • By default, kotlinx.serialization throws when JSON contains null for a non-nullable Kotlin property (you get a SerializationException / MissingFieldException / type error).
  • To handle/allow nulls for non-nullable fields, use one of:
    1. Make the property nullable or give it a default value.
    2. Configure the JSON format to coerce invalid inputs: Json { coerceInputValues = true } — this treats null-for-non-nullable as a “missing” value and will substitute the property's default (if any) or otherwise still fail if no default. [1][2]
    3. Use explicitNulls and coerceInputValues together for specific behaviors (explicitNulls=false treats absent fields as null for nullable properties). [1][3]
    4. Implement a custom serializer to handle nulls in whatever custom way you need. [4]

Relevant docs/issues

  • explicitNulls and behavior when field is absent vs explicit null. [1]
  • coerceInputValues: treats null inputs for non-nullable types (and unknown enum values) as missing so defaults/nulls may be used. [3]
  • MissingFieldException for required (non-optional) properties. [2]

If you want, tell me which behavior you want (coerce to default, treat as null, ignore field, or custom mapping) and I’ll show the exact Json config or a custom serializer example.


🏁 Script executed:

# Check the structure of the file and surrounding context
find . -name "UserKeywordsResponseDto.kt" -type f | head -20

Repository: imflint/Flint-Android

Length of output: 144


🏁 Script executed:

# Look for API specs or documentation in the repository
find . -name "*api*" -o -name "*spec*" -o -name "*openapi*" | grep -i -E "(yaml|json|txt|md)" | head -20

Repository: imflint/Flint-Android

Length of output: 47


🏁 Script executed:

# Check for similar DTO files to see how they handle nullable fields
find . -path "*/dto/*" -name "*.kt" -type f | head -20

Repository: imflint/Flint-Android

Length of output: 508


🏁 Script executed:

# Look for test files or mock responses that might show API response structure
find . -name "*test*" -o -name "*mock*" | grep -i -E "(keyword|user)" | head -20

Repository: imflint/Flint-Android

Length of output: 47


🏁 Script executed:

# Read the actual DTO file to see full structure
cat -n ./app/src/main/java/com/flint/data/dto/user/response/UserKeywordsResponseDto.kt

Repository: imflint/Flint-Android

Length of output: 783


🏁 Script executed:

# Check a few other DTO files to see patterns in how they handle nullable fields
head -50 ./app/src/main/java/com/flint/data/dto/base/BaseResponse.kt
head -50 ./app/src/main/java/com/flint/data/dto/auth/response/SignupResponseDto.kt

Repository: imflint/Flint-Android

Length of output: 705


🏁 Script executed:

# Look for JSON serialization configuration in the codebase
find . -name "*.kt" -type f | xargs rg -l "Json\s*{|coerceInputValues|explicitNulls" | head -20

Repository: imflint/Flint-Android

Length of output: 195


🏁 Script executed:

# Search for any error handling or try-catch related to deserialization
rg -l "SerializationException|MissingFieldException" --type kt | head -10

Repository: imflint/Flint-Android

Length of output: 91


🏁 Script executed:

# Search for JSON configuration in the codebase (fixed regex)
rg "Json\s*\{" -A 5 | head -50

Repository: imflint/Flint-Android

Length of output: 588


🏁 Script executed:

# Search for coerceInputValues or explicitNulls configuration
rg "coerceInputValues|explicitNulls" | head -20

Repository: imflint/Flint-Android

Length of output: 255


🏁 Script executed:

# Look at how DTOs are used - find deserialization code
find . -name "*.kt" -type f | xargs rg -l "fromJson|Json\.decodeFromString|kotlinx\.serialization" | head -20

Repository: imflint/Flint-Android

Length of output: 687


🏁 Script executed:

# Check if there are any API responses or test data showing actual structure
find . -name "*.json" -type f | head -20

Repository: imflint/Flint-Android

Length of output: 105


🏁 Script executed:

# Look for any other DTOs with nullable fields to understand the pattern
rg ":\s*\w+\?" --type-add 'kt:*.kt' -t kt app/src/main/java/com/flint/data/dto/ | head -30

Repository: imflint/Flint-Android

Length of output: 516


🏁 Script executed:

# Verify the NetworkModule.kt configuration more thoroughly
cat -n app/src/main/java/com/flint/core/common/di/NetworkModule.kt | head -100

Repository: imflint/Flint-Android

Length of output: 2671


🏁 Script executed:

# Check if UserKeywordsResultDto is used anywhere to see if actual issues occur
rg "UserKeywordsResultDto" -A 3 -B 3

Repository: imflint/Flint-Android

Length of output: 2048


기본값(default value) 추가 또는 nullable 필드 사용 권장

프로젝트의 JSON 역직렬화 설정(coerceInputValues = true)은 null 값을 누락된 필드로 처리하도록 구성되어 있습니다. 다만, 이 클래스의 필드들에는 기본값이 없으므로 API 응답에서 null이 반환되는 경우 역직렬화에 실패할 수 있습니다.

SocialVerifyResponseDto와 같이 nullable 필드에 기본값을 지정하거나, 필드를 nullable로 선언하여 안전성을 높이는 것을 권장합니다.

🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/data/dto/user/response/UserKeywordsResponseDto.kt`
around lines 12 - 24, The UserKeywordsResultDto data class currently defines
non-nullable properties without defaults, which will fail deserialization if API
returns null; update UserKeywordsResultDto by either assigning sensible default
values (e.g., empty string, 0, 0.0f) to properties color, rank, name,
percentage, imageUrl or make those properties nullable (String?, Int?, Float?)
to match SocialVerifyResponseDto style so coerceInputValues=true can safely
handle missing/null fields during JSON deserialization.

Comment thread app/src/main/java/com/flint/domain/model/user/UserKeywordResponseModel.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/AuthRepository.kt
Comment thread app/src/main/java/com/flint/domain/repository/AuthRepository.kt Outdated
Comment on lines +150 to +166
// TODO: 배치 로직 수정 필요
private fun rotateKeywordByRank(keywordList: ImmutableList<UserKeywordResponseModel>): ImmutableList<UserKeywordResponseModel> {
if (keywordList.size < 2) return keywordList

val sortedByRank = keywordList.sortedBy { it.rank }

// rank 1, 2, 3 (상위 3개) → Large
val topRanks = sortedByRank.take(3)
// rank 4, 5, 6 (하위 3개) → Small
val bottomRanks = sortedByRank.drop(3)

// 번갈아 배치: 1, 4, 2, 5, 3, 6
return topRanks
.zip(bottomRanks)
.flatMap { (top, bottom) -> listOf(top, bottom) }
.toPersistentList()
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 19, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

rotateKeywordByRank에서 키워드가 손실될 수 있습니다.

zip() 함수는 두 리스트 중 짧은 쪽의 길이만큼만 결합합니다. 예를 들어 키워드가 5개인 경우, topRanks는 3개, bottomRanks는 2개가 되어 zip 결과는 4개만 반환됩니다.

🐛 제안된 수정
 private fun rotateKeywordByRank(keywordList: ImmutableList<UserKeywordResponseModel>): ImmutableList<UserKeywordResponseModel> {
     if (keywordList.size < 2) return keywordList
 
     val sortedByRank = keywordList.sortedBy { it.rank }
 
     // rank 1, 2, 3 (상위 3개) → Large
     val topRanks = sortedByRank.take(3)
     // rank 4, 5, 6 (하위 3개) → Small
     val bottomRanks = sortedByRank.drop(3)
 
-    // 번갈아 배치: 1, 4, 2, 5, 3, 6
-    return topRanks
-        .zip(bottomRanks)
-        .flatMap { (top, bottom) -> listOf(top, bottom) }
-        .toPersistentList()
+    // 번갈아 배치하되 남은 요소도 포함
+    val result = mutableListOf<UserKeywordResponseModel>()
+    val maxSize = maxOf(topRanks.size, bottomRanks.size)
+    for (i in 0 until maxSize) {
+        if (i < topRanks.size) result.add(topRanks[i])
+        if (i < bottomRanks.size) result.add(bottomRanks[i])
+    }
+    return result.toPersistentList()
 }
📝 Committable suggestion

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

Suggested change
// TODO: 배치 로직 수정 필요
private fun rotateKeywordByRank(keywordList: ImmutableList<UserKeywordResponseModel>): ImmutableList<UserKeywordResponseModel> {
if (keywordList.size < 2) return keywordList
val sortedByRank = keywordList.sortedBy { it.rank }
// rank 1, 2, 3 (상위 3개) → Large
val topRanks = sortedByRank.take(3)
// rank 4, 5, 6 (하위 3개) → Small
val bottomRanks = sortedByRank.drop(3)
// 번갈아 배치: 1, 4, 2, 5, 3, 6
return topRanks
.zip(bottomRanks)
.flatMap { (top, bottom) -> listOf(top, bottom) }
.toPersistentList()
}
private fun rotateKeywordByRank(keywordList: ImmutableList<UserKeywordResponseModel>): ImmutableList<UserKeywordResponseModel> {
if (keywordList.size < 2) return keywordList
val sortedByRank = keywordList.sortedBy { it.rank }
// rank 1, 2, 3 (상위 3개) → Large
val topRanks = sortedByRank.take(3)
// rank 4, 5, 6 (하위 3개) → Small
val bottomRanks = sortedByRank.drop(3)
// 번갈아 배치하되 남은 요소도 포함
val result = mutableListOf<UserKeywordResponseModel>()
val maxSize = maxOf(topRanks.size, bottomRanks.size)
for (i in 0 until maxSize) {
if (i < topRanks.size) result.add(topRanks[i])
if (i < bottomRanks.size) result.add(bottomRanks[i])
}
return result.toPersistentList()
}
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt`
around lines 150 - 166, rotateKeywordByRank currently uses
topRanks.zip(bottomRanks) which drops any remaining items when sizes differ
(losing keywords for odd counts); update the interleaving logic to iterate up to
max(topRanks.size, bottomRanks.size) and append pairs where present (or
alternately append the leftover items after the zip), ensuring all items from
sortedByRank are preserved; change the code around topRanks, bottomRanks and the
final flatMap/toPersistentList return to include remaining entries from the
longer list so no keywords are lost.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

나중에 고칠게

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@nahy-512, 네, 알겠습니다! 나중에 수정하시면 됩니다. 👍

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/domain/repository/AuthRepository.kt`:
- Around line 17-21: Replace the synchronous runCatching usage in the suspend
functions signup and socialVerify with the coroutine-friendly suspendRunCatching
(following the UserRepository pattern) so CancellationException and
TimeoutCancellationException are handled correctly; update the calls in
AuthRepository's signup(SignupRequestModel) and
socialVerify(SocialVerifyRequestModel) to wrap api.signup(...).data.toModel()
and api.socialVerify(...).data.toModel() with suspendRunCatching, and ensure the
Result type remains Result<SignupResponseModel> and
Result<SocialVerifyResponseModel>; consider returning or mapping to your
project-wide custom error types later to distinguish network/server/timeout
errors.

In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt`:
- Around line 41-44: The refreshProfileKeyword function is using a hardcoded
userId (800370427074376635) which breaks real-user lookups; update
refreshProfileKeyword in ProfileViewModel to obtain the userId dynamically
(either accept a userId parameter, use an existing ViewModel property, or fetch
the current user id from the session/userRepository) and replace the hardcoded
value in the call to userRepository.getUserKeywords; ensure
viewModelScope.launch still wraps the call and that any callers of
refreshProfileKeyword are updated to pass/initialize the real user id if you
choose the parameter/constructor injection approach.
- Around line 45-49: The onSuccess logging in ProfileViewModel is printing the
entire result object (Timber.d("onSuccess: $result")); change this to log only
the minimal non-sensitive fields needed (e.g., userId, keywords summary, or a
sanitized flag) inside the onSuccess lambda of the same network callback, avoid
logging full objects or personal preference lists, and ensure Timber.d uses a
concise message like "onSuccess: userId=%s, keywordsCount=%d" or similar so
sensitive data in result is not emitted.
🧹 Nitpick comments (2)
app/src/main/java/com/flint/domain/model/user/UserKeywordResponseModel.kt (1)

19-74: 도메인 모델에 UI용 더미 데이터가 섞여 있습니다.

프리뷰/테스트용 더미 데이터는 presentation 또는 test 영역으로 분리하는 편이 계층 순수성을 지키는 데 유리합니다. 필요하다면 debug 전용 소스로 분리해 주세요.

학습 내용에 따라, 도메인 레이어는 UI/샘플 데이터와 분리하는 구조가 권장됩니다.

app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (1)

33-36: 초기 Fake 데이터 방출은 실제 로딩 흐름과 충돌할 수 있습니다.

출시 코드에서는 UiState.Loading 등으로 분리하고, 더미 데이터는 디버그 전용으로만 노출하는 구조를 권장합니다.

Comment thread app/src/main/java/com/flint/domain/repository/AuthRepository.kt Outdated
Comment thread app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt Outdated
Copy link
Copy Markdown
Contributor

@giovannijunseokim giovannijunseokim left a comment

Choose a reason for hiding this comment

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

LGTM 💪

Comment on lines +12 to +23
@Composable
fun FlintLoadingIndicator() {
Box(
modifier =
Modifier
.fillMaxSize()
.background(Color.Transparent),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: CircularProgressIndicatorBox로 감싸신 이유가 있을까요? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Box로 감싸주지 않으면 위치 지정을 못 해줘서 CircularProgressIndicator가 우측 상단에서 돌아갑니당

Comment on lines +38 to +39
isAllVisible: Boolean = false,
onAllClick: () -> Unit = {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: 개인적으로는 디폴트 파라미터를 잘 정의하지 않는 편이에요. 해당 파라미터는 넘기지 않아도 괜찮다. 라는 인식을 주는 것 같아서요.

ButtononClick에 디폴트 파라미터를 사용하지 않는 것처럼, contentDescription의 디폴트 파라미터가 null이 아닌 것처럼,

단순히 개발 편의성만 고려하기보다 해당 파라미터가 해당 컴포넌트에 얼마나 필수적인지가 반영되면 좋겠습니다. 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

저도 디폴트 파라미터를 별로 두지 않는 편인데, 이번에 변경사항을 반영하려다 '혹시 나중에도 달라지려나?' 싶은 생각으로 추가한 부분이었습니다. 개발 편의성보다 필수성을 고려하면 좋겠다는 말씀이 공감가서, 해당 부분 수정하도록 하겠습니다


@Serializable
data class UserKeywordsResponseDto(
@SerialName("keywords")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: @SerialName을 전부 붙여주는 이유는 무엇인가요? JSON의 키 이름이 변경되면 그 때 붙여줘도 되지 않을까요? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

저도 해당 고민을 했던 적이 있는데, dto의 경우 @SerialName를 모두 붙여주는 것이 명확하다고 생각하고 붙이는 걸 습관화하게 되었습니다.
서버 키가 snake_case처럼 Kotlin 네이밍과 충돌하는 경우도 종종 있는데, 그런 경우 코틀린 프로퍼티 이름은 컨벤션에 맞추고 @SerialName을 따로 붙여주는 식으로도 많이 하는 것 같아요. 그리고 서버의 키가 변경됐을 때 어느 시점에는 @SerialName을 조정해야 하는데, 처음부터 붙여두면 변경 시 어떤 필드의 계약이 바뀌었는지가 어노테이션 diff로 명확히 남아 좋은 것 같습니다!

accessToken = accessToken,
refreshToken = refreshToken,
userId = userId,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: SignupRequestModel.toDto()의 경우 SignupRequestModel의 멤버 함수로 둘 수 있을 것 같아요. 확장함수로 구현하신 이유가 있나요? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@kimjw2003 구현 부분이었어서, 답변 남겨주시면 감사하겠습니다ㅎㅎ

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.toModel과의 통일성을 중요하다 생각해서 확장함수로 진행하였습니다

private val api: AuthApi,
) {
suspend fun signup(model: SignupRequestModel): Result<SignupResponseModel> =
runCatching { api.signup(model.toDto()).data.toModel() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: 한 줄에 접근 및 메서드 호출이 많아서 읽기 어려울 수 있을 것 같아요. model.toDto()dto 등의 변수에 담는다거나, api.signup(model.toDto())response 변수에 담거나 하면 가독성이 올라갈 것 같습니다. 👍


companion object {
fun getOtts(): List<OttType> = OttType.entries
fun getOtts(): List<OttType> = entries
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: 필요한 함수일까요? entries를 그대로 사용할 수도 있을 것 같아서요. 👀

Copy link
Copy Markdown
Contributor Author

@nahy-512 nahy-512 Jan 19, 2026

Choose a reason for hiding this comment

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

'OTT 목록을 가져온다'라는 의미에서 getOtts라는 함수를 만들었을 때 바로 가져다 쓸 수 있지 않을까 했었는데, 생각해보니 불필요할 것도 같네요ㅎㅎ 수정하겠습니다

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt`:
- Around line 38-54: The current when on uiState leaves the else branch empty so
error/empty states render nothing; add explicit branches for UiState.Error and a
possible UiState.Empty (or use the else branch) to render a minimal error/empty
UI (e.g., an ErrorState or EmptyState composable) that preserves layout (use
Modifier.padding(paddingValues)) and provides a retry action wired to
viewModel::refreshProfileKeyword (or a retry lambda), and update callers of
ProfileScreen to accept and display an error/empty composable so users see a
message and a retry button instead of a blank screen.
♻️ Duplicate comments (1)
app/src/main/java/com/flint/domain/repository/AuthRepository.kt (1)

18-22: 체이닝된 호출의 가독성 개선을 고려해보세요.

이전 리뷰에서 언급된 것처럼, 한 줄에 여러 메서드 호출이 체이닝되어 있어 가독성이 떨어질 수 있습니다. 중간 결과를 변수에 할당하면 디버깅과 코드 이해가 쉬워집니다.

♻️ 가독성 개선 제안
-        suspend fun signup(model: SignupRequestModel): Result<SignupResponseModel> =
-            suspendRunCatching { api.signup(model.toDto()).data.toModel() }
+        suspend fun signup(model: SignupRequestModel): Result<SignupResponseModel> =
+            suspendRunCatching {
+                val response = api.signup(model.toDto())
+                response.data.toModel()
+            }

-        suspend fun socialVerify(model: SocialVerifyRequestModel): Result<SocialVerifyResponseModel> =
-            suspendRunCatching { api.socialVerify(model.toDto()).data.toModel() }
+        suspend fun socialVerify(model: SocialVerifyRequestModel): Result<SocialVerifyResponseModel> =
+            suspendRunCatching {
+                val response = api.socialVerify(model.toDto())
+                response.data.toModel()
+            }
🧹 Nitpick comments (2)
app/src/main/java/com/flint/presentation/onboarding/OnboardingOttScreen.kt (1)

34-40: ViewModel 연동 시 하드코딩된 nickname 값 교체 필요.

현재 nickname = "user"가 하드코딩되어 있습니다. 주석에 명시된 대로 ViewModel 연동 시 실제 사용자 닉네임을 가져오도록 변경이 필요합니다. PR 설명에 따르면 현재는 동작 확인용이므로 이해하지만, 추후 작업 시 참고 부탁드립니다.

app/src/main/java/com/flint/core/common/di/interceptor/TokenInterceptor.kt (1)

11-33: runBlocking로 인한 네트워크 스레드 블로킹 가능성 확인 필요

Line 19-22에서 DataStore Flow를 runBlocking으로 읽고 있어, 디스크 I/O가 발생하면 OkHttp 스레드가 블로킹되어 요청 지연이 생길 수 있습니다. 가능한 경우 PreferencesManager에 메모리 캐시(예: StateFlow/StateIn)를 두고 여기서는 캐시 값을 읽는 방식으로 전환하거나, 최소한 현재 구현이 충분히 빠른지 확인해 주세요.

Comment on lines +38 to +54
when (val state = uiState) {
is UiState.Loading -> {
FlintLoadingIndicator()
}

is UiState.Success -> {
ProfileScreen(
modifier = Modifier.padding(paddingValues),
uiState = state.data,
onRefreshClick = viewModel::refreshProfileKeyword,
onCollectionItemClick = navigateToCollectionDetail,
onCollectionMoreClick = navigateToCollectionList,
)
}

else -> {}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

에러/빈 상태가 현재는 빈 화면으로 끝납니다.

UiState.Success/Loading 외에는 no‑op이라 오류/빈 상태에서 아무 UI도 렌더링되지 않습니다. 최소한 빈/오류 상태 UI 또는 재시도 동선을 표시해 주세요.

🛠️ 빈 상태라도 레이아웃을 유지하는 예시
-        else -> {}
+        else -> {
+            ProfileScreen(
+                modifier = Modifier.padding(paddingValues),
+                uiState = ProfileUiState.Empty,
+                onRefreshClick = viewModel::refreshProfileKeyword,
+                onCollectionItemClick = navigateToCollectionDetail,
+                onCollectionMoreClick = navigateToCollectionList,
+            )
+        }
🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt` around
lines 38 - 54, The current when on uiState leaves the else branch empty so
error/empty states render nothing; add explicit branches for UiState.Error and a
possible UiState.Empty (or use the else branch) to render a minimal error/empty
UI (e.g., an ErrorState or EmptyState composable) that preserves layout (use
Modifier.padding(paddingValues)) and provides a retry action wired to
viewModel::refreshProfileKeyword (or a retry lambda), and update callers of
ProfileScreen to accept and display an error/empty composable so users see a
message and a retry button instead of a blank screen.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt`:
- Around line 14-17: The getUserKeywords implementation currently maps
apiService.getUserKeywords(userId).data.keywords without checking
BaseResponse.status, so responses with HTTP 200 but status != 0 are treated as
success; update suspend fun getUserKeywords (the suspendRunCatching block around
apiService.getUserKeywords) to inspect the returned BaseResponse.status and if
status indicates failure (e.g., status != 0) convert to Result.failure by
throwing or returning a specific exception (e.g., ApiException with the
status/message) before mapping data.keywords; alternatively implement the same
status check in a shared HTTP interceptor/response wrapper so all repository
methods (including getUserKeywords) get failure semantics for non-zero status.
♻️ Duplicate comments (1)
app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (1)

40-43: 하드코딩된 userId 제거 필요

실 사용자 기준 동작을 위해 userId를 주입/전달받는 방식으로 변경이 필요합니다.

💡 제안 변경
-        fun refreshProfileKeyword() {
+        fun refreshProfileKeyword(userId: String) {
             viewModelScope.launch {
-                userRepository.getUserKeywords(userId = "800370427074376635").fold( // TODO: 임시 userId
+                userRepository.getUserKeywords(userId = userId).fold(

Comment on lines +14 to +17
suspend fun getUserKeywords(userId: String): Result<List<UserKeywordResponseModel>> =
suspendRunCatching {
apiService.getUserKeywords(userId).data.keywords.map {
it.toModel()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# BaseResponse와 관련 DTO 정의 찾기
echo "=== BaseResponse 검색 ===" 
rg -t kotlin 'data class BaseResponse|class BaseResponse' -C 3

echo -e "\n=== UserKeywords 관련 DTO 검색 ==="
rg -t kotlin 'UserKeywordResponseModel|UserKeywordsResponseDto|UserKeywordsResultDto|data class.*Keyword' -C 2

echo -e "\n=== UserApi getUserKeywords 검색 ==="
rg -t kotlin 'fun getUserKeywords|suspend fun getUserKeywords' -C 3

echo -e "\n=== UserRepository의 전체 함수 검색 ==="
fd UserRepository.kt -x cat -n {}

Repository: imflint/Flint-Android

Length of output: 16382


🏁 Script executed:

#!/bin/bash
# BaseResponse 전체 내용 확인
echo "=== BaseResponse.kt 전체 ===" 
cat app/src/main/java/com/flint/data/dto/base/BaseResponse.kt

echo -e "\n=== UserKeywordsResponseDto 전체 ===" 
cat app/src/main/java/com/flint/data/dto/user/response/UserKeywordsResponseDto.kt

echo -e "\n=== 다른 Repository의 에러 처리 패턴 ===" 
rg -t kotlin 'suspendRunCatching|BaseResponse' app/src/main/java/com/flint/domain/repository/ -B2 -A5 | head -60

echo -e "\n=== suspendRunCatching 정의 ===" 
rg -t kotlin 'fun suspendRunCatching|suspend fun.*runCatching' -C3

Repository: imflint/Flint-Android

Length of output: 4248


🏁 Script executed:

#!/bin/bash
# suspendRunCatching 정의 찾기
echo "=== suspendRunCatching 정의 ===" 
rg -t kotlin 'fun suspendRunCatching|suspend fun.*RunCatching' app/src/main/java/com/flint/core -B2 -A10

echo -e "\n=== suspendRunCatching 사용 패턴 (다른 API 호출들) ===" 
rg -t kotlin 'suspendRunCatching.*api\.' app/src/main/java/com/flint/domain/repository -B1 -A3 | head -80

echo -e "\n=== status 필드 체크 패턴 ===" 
rg -t kotlin 'status.*==|if.*status' app/src/main/java/com/flint -C2 | head -50

Repository: imflint/Flint-Android

Length of output: 2170


🏁 Script executed:

#!/bin/bash
# 최종 확인: HTTP 상태 코드와 status 필드의 관계, 그리고 실제 에러 응답 처리 
echo "=== suspendRunCatching 전체 코드 ===" 
cat app/src/main/java/com/flint/core/common/util/SuspendRunCatching.kt

echo -e "\n=== UserApi 레트로핏 호출 구조 ===" 
cat app/src/main/java/com/flint/data/api/UserApi.kt

echo -e "\n=== 코드베이스 내 status 필드 명시적 체크 ===" 
rg -t kotlin '\.status\s*[!=]' app/src/main/java/com/flint --max-count=10

Repository: imflint/Flint-Android

Length of output: 1231


BaseResponse의 status 필드를 검증하고 API 실패 응답 처리 추가 필요

data.keywords는 non-nullable이므로 null 접근은 문제가 아니지만, API가 HTTP 200으로 응답하면서 status 필드가 실패를 나타내는 경우(예: status ≠ 0) suspendRunCatching은 예외가 발생하지 않아 실패 응답을 성공으로 처리하게 됩니다. API 응답의 status 필드를 명시적으로 검증하고 실패 상태를 Result.failure로 변환하거나, 공통 HTTP 인터셉터에서 status 검증을 처리해 주세요.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt` around lines
14 - 17, The getUserKeywords implementation currently maps
apiService.getUserKeywords(userId).data.keywords without checking
BaseResponse.status, so responses with HTTP 200 but status != 0 are treated as
success; update suspend fun getUserKeywords (the suspendRunCatching block around
apiService.getUserKeywords) to inspect the returned BaseResponse.status and if
status indicates failure (e.g., status != 0) convert to Result.failure by
throwing or returning a specific exception (e.g., ApiException with the
status/message) before mapping data.keywords; alternatively implement the same
status check in a shared HTTP interceptor/response wrapper so all repository
methods (including getUserKeywords) get failure semantics for non-zero status.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt (2)

46-64: navigateToAddContent가 사용되지 않습니다.

navigateToAddContent 파라미터가 선언되었지만 CollectionCreateScreen에 전달되지 않아, Line 261의 "작품 추가하기" 버튼이 동작하지 않습니다.

🛠️ 수정 제안

CollectionCreateScreenonAddContentClick 파라미터를 추가하고 연결하세요:

 fun CollectionCreateRoute(
     paddingValues: PaddingValues,
     navigateToAddContent: () -> Unit,
 ) {
     // ...
     CollectionCreateScreen(
         thumbnailImageUrl = "",
         contentList = contentList.toImmutableList(),
         onRemoveContent = { contentList.remove(it) },
         onBackClick = {},
         onGalleryClick = {},
         onCoverDeleteClick = {},
+        onAddContentClick = navigateToAddContent,
     )
 }

그리고 CollectionCreateScreen 함수 시그니처와 버튼에도 연결이 필요합니다.


254-270: "작품 추가하기" 버튼의 onClick이 빈 람다입니다.

Line 261의 onClick = {}가 빈 람다로 되어 있어 버튼을 눌러도 아무 동작이 없습니다. navigateToAddContent와 연결되어야 합니다.

🛠️ 수정 제안

CollectionCreateScreenonAddContentClick 파라미터를 추가하세요:

 fun CollectionCreateScreen(
     thumbnailImageUrl: String,
     contentList: ImmutableList<CollectionContentUiModel>,
     onRemoveContent: (CollectionContentUiModel) -> Unit,
     onBackClick: () -> Unit,
     onGalleryClick: () -> Unit,
     onCoverDeleteClick: () -> Unit,
+    onAddContentClick: () -> Unit,
 ) {

그리고 버튼에 연결:

                     FlintIconButton(
                         text = "작품 추가하기",
                         iconRes = R.drawable.ic_plus,
                         state = FlintButtonState.ColorOutline,
-                        onClick = {},
+                        onClick = onAddContentClick,
                         modifier = ...
                     )
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt`:
- Around line 39-45: Remove the duplicate local data class definition of
CollectionContentUiModel in AddContentScreen.kt and update the file to import
and use the single canonical model
com.flint.presentation.collectioncreate.model.CollectionContentUiModel instead;
delete the local data class block (the data class named CollectionContentUiModel
with contentId, imageUrl, title, director, createdYear) and add the appropriate
import, then ensure any usages in AddContentScreen (references to
CollectionContentUiModel) remain unchanged and compile against the imported
model.
- Around line 29-37: AddContentRoute is not wiring its parameters into the
screen: pass the incoming paddingValues into AddContentScreen and replace the
empty onBackClick lambda with the provided navigateToCollectionCreate callback
so the back navigation works; update the AddContentRoute function signature
usage to call AddContentScreen(paddingValues = paddingValues, onBackClick =
navigateToCollectionCreate) so both paddingValues and navigateToCollectionCreate
are actually used.

In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt`:
- Around line 39-41: In the UiState.Loading branch the FlintLoadingIndicator is
rendered without applying the provided paddingValues, causing overlap with the
bottom navigation; wrap FlintLoadingIndicator in a composable that applies
padding (e.g., Box(modifier = Modifier.padding(paddingValues))) or pass padding
to the indicator itself, and add the required import for
androidx.compose.foundation.layout.Box; update the UiState.Loading case to use
Box and apply paddingValues so the loading UI respects system/inset paddings.
♻️ Duplicate comments (1)
app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt (1)

53-53: 에러/빈 상태가 현재는 빈 화면으로 끝납니다.

UiState.Error 등의 상태에서 아무 UI도 렌더링되지 않습니다. 최소한 에러 메시지나 재시도 버튼을 표시하는 것이 좋습니다.

🧹 Nitpick comments (7)
app/src/main/java/com/flint/core/designsystem/component/image/SelectedContentItem.kt (1)

38-47: 접근성 개선 필요: contentDescription이 명확하지 않습니다.

contentDescription = "x"는 스크린 리더 사용자에게 의미 있는 정보를 제공하지 않습니다. "선택 해제" 또는 "항목 제거"와 같이 아이콘의 기능을 설명하는 문구로 변경해 주세요.

♻️ 제안하는 수정 사항
         Icon(
             imageVector = ImageVector.vectorResource(id = R.drawable.ic_deselect),
-            contentDescription = "x",
+            contentDescription = "선택 해제",
             tint = Color.Unspecified,
             modifier =
                 Modifier
                     .align(Alignment.TopEnd)
                     .size(48.dp)
                     .clickable { onRemoveClick() },
         )
app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt (1)

50-50: var 대신 val 사용 권장

mutableStateListOf는 이미 mutable한 컬렉션이므로 변수 자체를 var로 선언할 필요가 없습니다. val로 변경하면 의도치 않은 재할당을 방지할 수 있습니다.

♻️ 제안
-    var selectedContents = remember { mutableStateListOf<CollectionContentUiModel>() }
+    val selectedContents = remember { mutableStateListOf<CollectionContentUiModel>() }
app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt (1)

6-13: paddingValues 파라미터가 사용되지 않습니다.

SavedContentListRoute에서 paddingValues를 받지만 SavedContentListScreen에 전달하거나 사용하지 않습니다. 향후 구현 시 padding 적용이 필요할 것으로 보입니다.

♻️ 향후 구현을 위한 제안
 `@Composable`
-fun SavedContentListRoute(paddingValues: PaddingValues) {
-    SavedContentListScreen()
+fun SavedContentListRoute(paddingValues: PaddingValues) {
+    SavedContentListScreen(
+        modifier = Modifier.padding(paddingValues),
+    )
 }

 `@Composable`
-fun SavedContentListScreen() {
+fun SavedContentListScreen(
+    modifier: Modifier = Modifier,
+) {
+    // TODO: Implement UI
 }
app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt (1)

88-110: Preview 함수명 개선 제안

Preview 함수명 CollectionCreateContentSectionPreview가 Line 74의 CollectionCreateContentSelect.kt에 있는 CollectionCreateContentSectionPreview와 동일합니다. 파일 간 명확한 구분을 위해 이 파일의 preview 함수명을 좀 더 구체적으로 변경하는 것을 권장합니다.

제안된 변경
-private fun CollectionCreateContentSectionPreview() {
+private fun CollectionCreateContentSectionWithImagePreview() {
app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt (1)

72-86: Preview 함수명이 프리뷰 대상과 불일치합니다.

CollectionCreateContentSectionPreview라는 이름이지만 실제로는 CollectionCreateContentSelect를 프리뷰하고 있습니다. 명확성을 위해 CollectionCreateContentSelectPreview로 변경하는 것이 좋겠습니다.

제안된 변경
-private fun CollectionCreateContentSectionPreview() {
+private fun CollectionCreateContentSelectPreview() {
app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt (1)

306-319: 프리뷰에도 누락된 파라미터 추가가 필요합니다.

위에서 제안한 onAddContentClick 파라미터를 추가하면 프리뷰에도 해당 파라미터를 전달해야 합니다.

♻️ 수정 제안
 `@Preview`
 `@Composable`
 fun CollectionCreateScreenPreview() {
     FlintTheme {
         CollectionCreateScreen(
             thumbnailImageUrl = "",
             contentList = CollectionContentUiModel.dummyContentList,
             onRemoveContent = {},
             onBackClick = {},
             onGalleryClick = {},
             onCoverDeleteClick = {},
+            onAddContentClick = {},
         )
     }
 }
app/src/main/java/com/flint/presentation/savedcontent/SavedContentListItem.kt (1)

202-221: Preview 함수명이 실제 미리보기 대상과 불일치합니다.

SavedContentListItemBookmarkPreview라는 이름이지만 실제로는 전체 SavedContentListItem을 미리보기하고 있습니다. 명확성을 위해 SavedContentListItemPreview로 변경하는 것을 권장합니다.

♻️ 제안된 수정
 `@Preview`
 `@Composable`
-private fun SavedContentListItemBookmarkPreview() {
+private fun SavedContentListItemPreview() {
     FlintTheme {

Comment on lines 29 to 37
@Composable
fun AddFilmRoute(
fun AddContentRoute(
paddingValues: PaddingValues,
navigateToCollectionCreate: () -> Unit,
) {
AddFilmScreen(
AddContentScreen(
onBackClick = {},
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용되지 않는 파라미터 및 미연결된 콜백

AddContentRoute에서 다음 이슈가 있습니다:

  1. paddingValuesAddContentScreen에 전달되지 않음
  2. navigateToCollectionCreate가 사용되지 않음
  3. onBackClick이 빈 람다 {}로 전달됨 - 실제 뒤로가기 동작이 작동하지 않음
🔧 제안
 `@Composable`
 fun AddContentRoute(
     paddingValues: PaddingValues,
     navigateToCollectionCreate: () -> Unit,
+    navigateUp: () -> Unit,
 ) {
     AddContentScreen(
-        onBackClick = {},
+        onBackClick = navigateUp,
+        modifier = Modifier.padding(paddingValues),
     )
 }
📝 Committable suggestion

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

Suggested change
@Composable
fun AddFilmRoute(
fun AddContentRoute(
paddingValues: PaddingValues,
navigateToCollectionCreate: () -> Unit,
) {
AddFilmScreen(
AddContentScreen(
onBackClick = {},
)
}
`@Composable`
fun AddContentRoute(
paddingValues: PaddingValues,
navigateToCollectionCreate: () -> Unit,
navigateUp: () -> Unit,
) {
AddContentScreen(
onBackClick = navigateUp,
modifier = Modifier.padding(paddingValues),
)
}
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt`
around lines 29 - 37, AddContentRoute is not wiring its parameters into the
screen: pass the incoming paddingValues into AddContentScreen and replace the
empty onBackClick lambda with the provided navigateToCollectionCreate callback
so the back navigation works; update the AddContentRoute function signature
usage to call AddContentScreen(paddingValues = paddingValues, onBackClick =
navigateToCollectionCreate) so both paddingValues and navigateToCollectionCreate
are actually used.

Comment on lines +39 to 45
data class CollectionContentUiModel(
val contentId: Long,
val imageUrl: String,
val title: String,
val director: String,
val createdYear: String,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

중복된 data class 정의 제거 필요

CollectionContentUiModel이 이 파일에서 중복 정의되어 있습니다. 이미 com.flint.presentation.collectioncreate.model.CollectionContentUiModel에 동일한 클래스가 존재합니다. 이로 인해 import 충돌 또는 shadowing 문제가 발생할 수 있습니다.

🔧 제안: 중복 정의 제거 및 import 추가
+import com.flint.presentation.collectioncreate.model.CollectionContentUiModel
 import com.flint.presentation.collectioncreate.component.CollectionCreateContentSelect

-data class CollectionContentUiModel(
-    val contentId: Long,
-    val imageUrl: String,
-    val title: String,
-    val director: String,
-    val createdYear: String,
-)
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt`
around lines 39 - 45, Remove the duplicate local data class definition of
CollectionContentUiModel in AddContentScreen.kt and update the file to import
and use the single canonical model
com.flint.presentation.collectioncreate.model.CollectionContentUiModel instead;
delete the local data class block (the data class named CollectionContentUiModel
with contentId, imageUrl, title, director, createdYear) and add the appropriate
import, then ensure any usages in AddContentScreen (references to
CollectionContentUiModel) remain unchanged and compile against the imported
model.

Comment on lines +39 to +41
is UiState.Loading -> {
FlintLoadingIndicator()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Loading 상태에서 paddingValues가 적용되지 않습니다.

FlintLoadingIndicatorpaddingValues가 적용되지 않아, 로딩 중에 하단 네비게이션 바와 겹칠 수 있습니다.

🛠️ 수정 제안
         is UiState.Loading -> {
-            FlintLoadingIndicator()
+            Box(modifier = Modifier.padding(paddingValues)) {
+                FlintLoadingIndicator()
+            }
         }

Box 임포트 추가 필요:

import androidx.compose.foundation.layout.Box
🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt` around
lines 39 - 41, In the UiState.Loading branch the FlintLoadingIndicator is
rendered without applying the provided paddingValues, causing overlap with the
bottom navigation; wrap FlintLoadingIndicator in a composable that applies
padding (e.g., Box(modifier = Modifier.padding(paddingValues))) or pass padding
to the indicator itself, and add the required import for
androidx.compose.foundation.layout.Box; update the UiState.Loading case to use
Box and apply paddingValues so the loading UI respects system/inset paddings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔖 API feat - API 연동 Refactor ♻️ 기능 변경 없이 코드 구조를 개선하여 가독성, 유지보수성, 성능을 향상

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] API 설계 구조 변경

3 participants