@su-record/vibe 0.4.6 โ 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vibe/rules/core/communication-guide.md +104 -0
- package/.vibe/rules/core/development-philosophy.md +53 -0
- package/.vibe/rules/core/quick-start.md +121 -0
- package/.vibe/rules/languages/dart-flutter.md +509 -0
- package/.vibe/rules/languages/go.md +396 -0
- package/.vibe/rules/languages/java-spring.md +586 -0
- package/.vibe/rules/languages/kotlin-android.md +491 -0
- package/.vibe/rules/languages/python-django.md +371 -0
- package/.vibe/rules/languages/python-fastapi.md +386 -0
- package/.vibe/rules/languages/rust.md +425 -0
- package/.vibe/rules/languages/swift-ios.md +516 -0
- package/.vibe/rules/languages/typescript-nextjs.md +441 -0
- package/.vibe/rules/languages/typescript-node.md +375 -0
- package/.vibe/rules/languages/typescript-react-native.md +446 -0
- package/.vibe/rules/languages/typescript-react.md +525 -0
- package/.vibe/rules/languages/typescript-vue.md +353 -0
- package/.vibe/rules/quality/bdd-contract-testing.md +388 -0
- package/.vibe/rules/quality/checklist.md +276 -0
- package/.vibe/rules/quality/testing-strategy.md +437 -0
- package/.vibe/rules/standards/anti-patterns.md +369 -0
- package/.vibe/rules/standards/code-structure.md +291 -0
- package/.vibe/rules/standards/complexity-metrics.md +312 -0
- package/.vibe/rules/standards/naming-conventions.md +198 -0
- package/.vibe/rules/tools/mcp-hi-ai-guide.md +665 -0
- package/.vibe/rules/tools/mcp-workflow.md +51 -0
- package/bin/vibe +46 -0
- package/package.json +2 -2
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# ๐ค Kotlin + Android ํ์ง ๊ท์น
|
|
2
|
+
|
|
3
|
+
## ํต์ฌ ์์น (core์์ ์์)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
โ
๋จ์ผ ์ฑ
์ (SRP)
|
|
7
|
+
โ
์ค๋ณต ์ ๊ฑฐ (DRY)
|
|
8
|
+
โ
์ฌ์ฌ์ฉ์ฑ
|
|
9
|
+
โ
๋ฎ์ ๋ณต์ก๋
|
|
10
|
+
โ
ํจ์ โค 30์ค
|
|
11
|
+
โ
์ค์ฒฉ โค 3๋จ๊ณ
|
|
12
|
+
โ
Cyclomatic complexity โค 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Kotlin/Android ํนํ ๊ท์น
|
|
16
|
+
|
|
17
|
+
### 1. Jetpack Compose UI
|
|
18
|
+
|
|
19
|
+
```kotlin
|
|
20
|
+
// โ
Composable ํจ์
|
|
21
|
+
@Composable
|
|
22
|
+
fun UserProfileScreen(
|
|
23
|
+
viewModel: UserProfileViewModel = hiltViewModel(),
|
|
24
|
+
onNavigateBack: () -> Unit
|
|
25
|
+
) {
|
|
26
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
27
|
+
|
|
28
|
+
UserProfileContent(
|
|
29
|
+
uiState = uiState,
|
|
30
|
+
onRefresh = viewModel::loadUser,
|
|
31
|
+
onNavigateBack = onNavigateBack
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// โ
Stateless Composable (์ฌ์ฌ์ฉ ๊ฐ๋ฅ)
|
|
36
|
+
@Composable
|
|
37
|
+
private fun UserProfileContent(
|
|
38
|
+
uiState: UserProfileUiState,
|
|
39
|
+
onRefresh: () -> Unit,
|
|
40
|
+
onNavigateBack: () -> Unit,
|
|
41
|
+
modifier: Modifier = Modifier
|
|
42
|
+
) {
|
|
43
|
+
Scaffold(
|
|
44
|
+
topBar = {
|
|
45
|
+
TopAppBar(
|
|
46
|
+
title = { Text("ํ๋กํ") },
|
|
47
|
+
navigationIcon = {
|
|
48
|
+
IconButton(onClick = onNavigateBack) {
|
|
49
|
+
Icon(Icons.Default.ArrowBack, contentDescription = "๋ค๋ก")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
) { paddingValues ->
|
|
55
|
+
when (uiState) {
|
|
56
|
+
is UserProfileUiState.Loading -> LoadingContent(modifier.padding(paddingValues))
|
|
57
|
+
is UserProfileUiState.Success -> UserContent(
|
|
58
|
+
user = uiState.user,
|
|
59
|
+
modifier = modifier.padding(paddingValues)
|
|
60
|
+
)
|
|
61
|
+
is UserProfileUiState.Error -> ErrorContent(
|
|
62
|
+
message = uiState.message,
|
|
63
|
+
onRetry = onRefresh,
|
|
64
|
+
modifier = modifier.padding(paddingValues)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// โ
์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ
|
|
71
|
+
@Composable
|
|
72
|
+
fun UserCard(
|
|
73
|
+
user: User,
|
|
74
|
+
onClick: () -> Unit,
|
|
75
|
+
modifier: Modifier = Modifier
|
|
76
|
+
) {
|
|
77
|
+
Card(
|
|
78
|
+
modifier = modifier
|
|
79
|
+
.fillMaxWidth()
|
|
80
|
+
.clickable(onClick = onClick),
|
|
81
|
+
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
82
|
+
) {
|
|
83
|
+
Row(
|
|
84
|
+
modifier = Modifier.padding(16.dp),
|
|
85
|
+
verticalAlignment = Alignment.CenterVertically
|
|
86
|
+
) {
|
|
87
|
+
AsyncImage(
|
|
88
|
+
model = user.profileImage,
|
|
89
|
+
contentDescription = "${user.name} ํ๋กํ",
|
|
90
|
+
modifier = Modifier
|
|
91
|
+
.size(48.dp)
|
|
92
|
+
.clip(CircleShape)
|
|
93
|
+
)
|
|
94
|
+
Spacer(modifier = Modifier.width(16.dp))
|
|
95
|
+
Column {
|
|
96
|
+
Text(
|
|
97
|
+
text = user.name,
|
|
98
|
+
style = MaterialTheme.typography.titleMedium
|
|
99
|
+
)
|
|
100
|
+
Text(
|
|
101
|
+
text = user.email,
|
|
102
|
+
style = MaterialTheme.typography.bodySmall,
|
|
103
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. ViewModel (MVVM)
|
|
112
|
+
|
|
113
|
+
```kotlin
|
|
114
|
+
// โ
UiState ์ ์ (Sealed Interface)
|
|
115
|
+
sealed interface UserListUiState {
|
|
116
|
+
data object Loading : UserListUiState
|
|
117
|
+
data class Success(
|
|
118
|
+
val users: List<User>,
|
|
119
|
+
val isRefreshing: Boolean = false
|
|
120
|
+
) : UserListUiState
|
|
121
|
+
data class Error(val message: String) : UserListUiState
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// โ
ViewModel with Hilt
|
|
125
|
+
@HiltViewModel
|
|
126
|
+
class UserListViewModel @Inject constructor(
|
|
127
|
+
private val getUsersUseCase: GetUsersUseCase,
|
|
128
|
+
private val savedStateHandle: SavedStateHandle
|
|
129
|
+
) : ViewModel() {
|
|
130
|
+
|
|
131
|
+
private val _uiState = MutableStateFlow<UserListUiState>(UserListUiState.Loading)
|
|
132
|
+
val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow()
|
|
133
|
+
|
|
134
|
+
private val searchQuery = savedStateHandle.getStateFlow("search", "")
|
|
135
|
+
|
|
136
|
+
val filteredUsers: StateFlow<List<User>> = combine(
|
|
137
|
+
_uiState,
|
|
138
|
+
searchQuery
|
|
139
|
+
) { state, query ->
|
|
140
|
+
when (state) {
|
|
141
|
+
is UserListUiState.Success -> {
|
|
142
|
+
if (query.isBlank()) state.users
|
|
143
|
+
else state.users.filter { it.name.contains(query, ignoreCase = true) }
|
|
144
|
+
}
|
|
145
|
+
else -> emptyList()
|
|
146
|
+
}
|
|
147
|
+
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
|
148
|
+
|
|
149
|
+
init {
|
|
150
|
+
loadUsers()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fun loadUsers() {
|
|
154
|
+
viewModelScope.launch {
|
|
155
|
+
_uiState.value = UserListUiState.Loading
|
|
156
|
+
|
|
157
|
+
getUsersUseCase()
|
|
158
|
+
.onSuccess { users ->
|
|
159
|
+
_uiState.value = UserListUiState.Success(users)
|
|
160
|
+
}
|
|
161
|
+
.onFailure { error ->
|
|
162
|
+
_uiState.value = UserListUiState.Error(
|
|
163
|
+
error.message ?: "์ฌ์ฉ์ ๋ชฉ๋ก์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค"
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fun updateSearchQuery(query: String) {
|
|
170
|
+
savedStateHandle["search"] = query
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fun refresh() {
|
|
174
|
+
viewModelScope.launch {
|
|
175
|
+
val currentState = _uiState.value
|
|
176
|
+
if (currentState is UserListUiState.Success) {
|
|
177
|
+
_uiState.value = currentState.copy(isRefreshing = true)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getUsersUseCase()
|
|
181
|
+
.onSuccess { users ->
|
|
182
|
+
_uiState.value = UserListUiState.Success(users, isRefreshing = false)
|
|
183
|
+
}
|
|
184
|
+
.onFailure { /* ์๋ฌ ์ฒ๋ฆฌ */ }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 3. UseCase (Clean Architecture)
|
|
191
|
+
|
|
192
|
+
```kotlin
|
|
193
|
+
// โ
UseCase ์ ์
|
|
194
|
+
class GetUsersUseCase @Inject constructor(
|
|
195
|
+
private val userRepository: UserRepository,
|
|
196
|
+
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
197
|
+
) {
|
|
198
|
+
suspend operator fun invoke(): Result<List<User>> = withContext(dispatcher) {
|
|
199
|
+
runCatching {
|
|
200
|
+
userRepository.getUsers()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class GetUserUseCase @Inject constructor(
|
|
206
|
+
private val userRepository: UserRepository
|
|
207
|
+
) {
|
|
208
|
+
suspend operator fun invoke(id: String): Result<User> = runCatching {
|
|
209
|
+
userRepository.getUser(id)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class CreateUserUseCase @Inject constructor(
|
|
214
|
+
private val userRepository: UserRepository,
|
|
215
|
+
private val validator: UserValidator
|
|
216
|
+
) {
|
|
217
|
+
suspend operator fun invoke(request: CreateUserRequest): Result<User> {
|
|
218
|
+
// ์ ํจ์ฑ ๊ฒ์ฌ
|
|
219
|
+
validator.validate(request).onFailure { return Result.failure(it) }
|
|
220
|
+
|
|
221
|
+
return runCatching {
|
|
222
|
+
userRepository.createUser(request)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 4. Repository ํจํด
|
|
229
|
+
|
|
230
|
+
```kotlin
|
|
231
|
+
// โ
Repository Interface
|
|
232
|
+
interface UserRepository {
|
|
233
|
+
suspend fun getUsers(): List<User>
|
|
234
|
+
suspend fun getUser(id: String): User
|
|
235
|
+
suspend fun createUser(request: CreateUserRequest): User
|
|
236
|
+
suspend fun updateUser(id: String, request: UpdateUserRequest): User
|
|
237
|
+
suspend fun deleteUser(id: String)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// โ
Repository ๊ตฌํ
|
|
241
|
+
class UserRepositoryImpl @Inject constructor(
|
|
242
|
+
private val apiService: UserApiService,
|
|
243
|
+
private val userDao: UserDao,
|
|
244
|
+
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
245
|
+
) : UserRepository {
|
|
246
|
+
|
|
247
|
+
override suspend fun getUsers(): List<User> = withContext(dispatcher) {
|
|
248
|
+
try {
|
|
249
|
+
// API์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
|
|
250
|
+
val response = apiService.getUsers()
|
|
251
|
+
val users = response.map { it.toDomain() }
|
|
252
|
+
|
|
253
|
+
// ๋ก์ปฌ ์บ์ ์
๋ฐ์ดํธ
|
|
254
|
+
userDao.insertAll(users.map { it.toEntity() })
|
|
255
|
+
|
|
256
|
+
users
|
|
257
|
+
} catch (e: Exception) {
|
|
258
|
+
// ์คํ๋ผ์ธ: ๋ก์ปฌ ๋ฐ์ดํฐ ๋ฐํ
|
|
259
|
+
userDao.getAll().map { it.toDomain() }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
override suspend fun getUser(id: String): User = withContext(dispatcher) {
|
|
264
|
+
val response = apiService.getUser(id)
|
|
265
|
+
response.toDomain()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 5. ์๋ฌ ์ฒ๋ฆฌ
|
|
271
|
+
|
|
272
|
+
```kotlin
|
|
273
|
+
// โ
์ปค์คํ
์์ธ
|
|
274
|
+
sealed class AppException(message: String) : Exception(message) {
|
|
275
|
+
class NetworkException(message: String = "๋คํธ์ํฌ ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์") : AppException(message)
|
|
276
|
+
class UnauthorizedException(message: String = "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค") : AppException(message)
|
|
277
|
+
class NotFoundException(
|
|
278
|
+
val resource: String,
|
|
279
|
+
val id: String
|
|
280
|
+
) : AppException("${resource}์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค (ID: $id)")
|
|
281
|
+
class ServerException(message: String) : AppException(message)
|
|
282
|
+
class ValidationException(message: String) : AppException(message)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// โ
Result ํ์ฅ ํจ์
|
|
286
|
+
inline fun <T> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
|
|
287
|
+
getOrNull()?.let(action)
|
|
288
|
+
return this
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
inline fun <T> Result<T>.onFailure(action: (Throwable) -> Unit): Result<T> {
|
|
292
|
+
exceptionOrNull()?.let(action)
|
|
293
|
+
return this
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// โ
API ์๋ต ์ฒ๋ฆฌ
|
|
297
|
+
suspend fun <T> safeApiCall(
|
|
298
|
+
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
|
299
|
+
apiCall: suspend () -> T
|
|
300
|
+
): Result<T> = withContext(dispatcher) {
|
|
301
|
+
runCatching {
|
|
302
|
+
apiCall()
|
|
303
|
+
}.recoverCatching { throwable ->
|
|
304
|
+
when (throwable) {
|
|
305
|
+
is HttpException -> {
|
|
306
|
+
when (throwable.code()) {
|
|
307
|
+
401 -> throw AppException.UnauthorizedException()
|
|
308
|
+
404 -> throw AppException.NotFoundException("๋ฆฌ์์ค", "unknown")
|
|
309
|
+
else -> throw AppException.ServerException("์๋ฒ ์ค๋ฅ: ${throwable.code()}")
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
is IOException -> throw AppException.NetworkException()
|
|
313
|
+
else -> throw throwable
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 6. Hilt ์์กด์ฑ ์ฃผ์
|
|
320
|
+
|
|
321
|
+
```kotlin
|
|
322
|
+
// โ
Module ์ ์
|
|
323
|
+
@Module
|
|
324
|
+
@InstallIn(SingletonComponent::class)
|
|
325
|
+
object NetworkModule {
|
|
326
|
+
|
|
327
|
+
@Provides
|
|
328
|
+
@Singleton
|
|
329
|
+
fun provideOkHttpClient(): OkHttpClient {
|
|
330
|
+
return OkHttpClient.Builder()
|
|
331
|
+
.addInterceptor(AuthInterceptor())
|
|
332
|
+
.addInterceptor(HttpLoggingInterceptor().apply {
|
|
333
|
+
level = HttpLoggingInterceptor.Level.BODY
|
|
334
|
+
})
|
|
335
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
336
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
337
|
+
.build()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@Provides
|
|
341
|
+
@Singleton
|
|
342
|
+
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
|
343
|
+
return Retrofit.Builder()
|
|
344
|
+
.baseUrl(BuildConfig.API_BASE_URL)
|
|
345
|
+
.client(okHttpClient)
|
|
346
|
+
.addConverterFactory(GsonConverterFactory.create())
|
|
347
|
+
.build()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@Provides
|
|
351
|
+
@Singleton
|
|
352
|
+
fun provideUserApiService(retrofit: Retrofit): UserApiService {
|
|
353
|
+
return retrofit.create(UserApiService::class.java)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@Module
|
|
358
|
+
@InstallIn(SingletonComponent::class)
|
|
359
|
+
abstract class RepositoryModule {
|
|
360
|
+
|
|
361
|
+
@Binds
|
|
362
|
+
@Singleton
|
|
363
|
+
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 7. ํ
์คํธ
|
|
368
|
+
|
|
369
|
+
```kotlin
|
|
370
|
+
// โ
ViewModel ํ
์คํธ
|
|
371
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
372
|
+
class UserListViewModelTest {
|
|
373
|
+
|
|
374
|
+
@get:Rule
|
|
375
|
+
val mainDispatcherRule = MainDispatcherRule()
|
|
376
|
+
|
|
377
|
+
private lateinit var viewModel: UserListViewModel
|
|
378
|
+
private lateinit var getUsersUseCase: GetUsersUseCase
|
|
379
|
+
private lateinit var fakeUserRepository: FakeUserRepository
|
|
380
|
+
|
|
381
|
+
@Before
|
|
382
|
+
fun setup() {
|
|
383
|
+
fakeUserRepository = FakeUserRepository()
|
|
384
|
+
getUsersUseCase = GetUsersUseCase(fakeUserRepository)
|
|
385
|
+
viewModel = UserListViewModel(getUsersUseCase, SavedStateHandle())
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@Test
|
|
389
|
+
fun `loadUsers ์ฑ๊ณต์ Success ์ํ๊ฐ ๋๋ค`() = runTest {
|
|
390
|
+
// Given
|
|
391
|
+
val expectedUsers = listOf(
|
|
392
|
+
User(id = "1", name = "ํ
์คํธ1", email = "test1@example.com"),
|
|
393
|
+
User(id = "2", name = "ํ
์คํธ2", email = "test2@example.com")
|
|
394
|
+
)
|
|
395
|
+
fakeUserRepository.setUsers(expectedUsers)
|
|
396
|
+
|
|
397
|
+
// When
|
|
398
|
+
viewModel.loadUsers()
|
|
399
|
+
|
|
400
|
+
// Then
|
|
401
|
+
val state = viewModel.uiState.value
|
|
402
|
+
assertThat(state).isInstanceOf(UserListUiState.Success::class.java)
|
|
403
|
+
assertThat((state as UserListUiState.Success).users).hasSize(2)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@Test
|
|
407
|
+
fun `loadUsers ์คํจ์ Error ์ํ๊ฐ ๋๋ค`() = runTest {
|
|
408
|
+
// Given
|
|
409
|
+
fakeUserRepository.setShouldReturnError(true)
|
|
410
|
+
|
|
411
|
+
// When
|
|
412
|
+
viewModel.loadUsers()
|
|
413
|
+
|
|
414
|
+
// Then
|
|
415
|
+
val state = viewModel.uiState.value
|
|
416
|
+
assertThat(state).isInstanceOf(UserListUiState.Error::class.java)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// โ
Fake Repository
|
|
421
|
+
class FakeUserRepository : UserRepository {
|
|
422
|
+
private var users = mutableListOf<User>()
|
|
423
|
+
private var shouldReturnError = false
|
|
424
|
+
|
|
425
|
+
fun setUsers(users: List<User>) {
|
|
426
|
+
this.users = users.toMutableList()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fun setShouldReturnError(value: Boolean) {
|
|
430
|
+
shouldReturnError = value
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
override suspend fun getUsers(): List<User> {
|
|
434
|
+
if (shouldReturnError) throw Exception("Test error")
|
|
435
|
+
return users
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ... ๋ค๋ฅธ ๋ฉ์๋
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## ํ์ผ ๊ตฌ์กฐ
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
app/
|
|
446
|
+
โโโ src/main/java/com/example/app/
|
|
447
|
+
โ โโโ di/ # Hilt ๋ชจ๋
|
|
448
|
+
โ โ โโโ NetworkModule.kt
|
|
449
|
+
โ โ โโโ RepositoryModule.kt
|
|
450
|
+
โ โโโ data/
|
|
451
|
+
โ โ โโโ api/ # API ์๋น์ค
|
|
452
|
+
โ โ โ โโโ UserApiService.kt
|
|
453
|
+
โ โ โโโ local/ # Room DAO
|
|
454
|
+
โ โ โ โโโ UserDao.kt
|
|
455
|
+
โ โ โโโ model/ # DTO
|
|
456
|
+
โ โ โ โโโ UserDto.kt
|
|
457
|
+
โ โ โโโ repository/ # Repository ๊ตฌํ
|
|
458
|
+
โ โ โโโ UserRepositoryImpl.kt
|
|
459
|
+
โ โโโ domain/
|
|
460
|
+
โ โ โโโ model/ # ๋๋ฉ์ธ ๋ชจ๋ธ
|
|
461
|
+
โ โ โ โโโ User.kt
|
|
462
|
+
โ โ โโโ repository/ # Repository ์ธํฐํ์ด์ค
|
|
463
|
+
โ โ โ โโโ UserRepository.kt
|
|
464
|
+
โ โ โโโ usecase/ # UseCase
|
|
465
|
+
โ โ โโโ GetUsersUseCase.kt
|
|
466
|
+
โ โโโ presentation/
|
|
467
|
+
โ โโโ ui/
|
|
468
|
+
โ โ โโโ components/ # ๊ณตํต Composable
|
|
469
|
+
โ โ โโโ theme/ # Material Theme
|
|
470
|
+
โ โโโ feature/
|
|
471
|
+
โ โโโ user/
|
|
472
|
+
โ โโโ UserListScreen.kt
|
|
473
|
+
โ โโโ UserListViewModel.kt
|
|
474
|
+
โ โโโ UserListUiState.kt
|
|
475
|
+
โโโ src/test/
|
|
476
|
+
โโโ java/com/example/app/
|
|
477
|
+
โโโ presentation/
|
|
478
|
+
โโโ feature/user/
|
|
479
|
+
โโโ UserListViewModelTest.kt
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## ์ฒดํฌ๋ฆฌ์คํธ
|
|
483
|
+
|
|
484
|
+
- [ ] Jetpack Compose ์ฌ์ฉ (XML ๋ ์ด์์ ์ง์)
|
|
485
|
+
- [ ] StateFlow๋ก UI ์ํ ๊ด๋ฆฌ
|
|
486
|
+
- [ ] Sealed Interface๋ก UiState ์ ์
|
|
487
|
+
- [ ] Hilt๋ก ์์กด์ฑ ์ฃผ์
|
|
488
|
+
- [ ] UseCase๋ก ๋น์ฆ๋์ค ๋ก์ง ๋ถ๋ฆฌ
|
|
489
|
+
- [ ] Repository ํจํด์ผ๋ก ๋ฐ์ดํฐ ๊ณ์ธต ์ถ์ํ
|
|
490
|
+
- [ ] Result/runCatching์ผ๋ก ์๋ฌ ์ฒ๋ฆฌ
|
|
491
|
+
- [ ] collectAsStateWithLifecycle() ์ฌ์ฉ
|