From d78f0902187a3c4c6ee8f7a232493a7cfa0456d6 Mon Sep 17 00:00:00 2001 From: Kyu hyunSung Date: Thu, 29 May 2025 18:08:21 +0900 Subject: [PATCH 1/4] [CHORE/#25] Category delete, add API Fixed --- .../java/com/example/data/api/CategoryApi.kt | 28 +-- .../java/com/example/data/di/NetworkModule.kt | 63 ++++++- .../java/com/example/data/dto/CategoryDto.kt | 4 +- .../com/example/data/mapper/CategoryMapper.kt | 18 +- .../data/repository/MenuRepositoryImpl.kt | 120 +++++++++--- .../usecase/menu/DeleteCategoryUseCase.kt | 17 ++ .../menu/screen/CategoryManagementScreen.kt | 16 +- .../example/menu/viewmodel/MenuViewModel.kt | 58 +++--- .../com/example/order/screen/OrderScreen.kt | 171 +++++++++++------- 9 files changed, 338 insertions(+), 157 deletions(-) create mode 100644 domain/src/main/java/com/example/domain/usecase/menu/DeleteCategoryUseCase.kt diff --git a/data/src/main/java/com/example/data/api/CategoryApi.kt b/data/src/main/java/com/example/data/api/CategoryApi.kt index f26c9be..15ef415 100644 --- a/data/src/main/java/com/example/data/api/CategoryApi.kt +++ b/data/src/main/java/com/example/data/api/CategoryApi.kt @@ -1,4 +1,4 @@ -// data/api/CategoryApi.kt +// data/api/CategoryApi.kt - @Header 파라미터 방식 package com.example.data.api import com.example.data.dto.CategoryCreateRequest @@ -8,40 +8,24 @@ import retrofit2.http.* /** * 카테고리 관련 API 엔드포인트 정의 - * 카테고리 CRUD 작업을 처리하는 Retrofit 인터페이스 */ interface CategoryApi { - /** - * 모든 카테고리 목록 조회 - * - * @return 카테고리 목록을 담은 Response - * API: GET /api/categories - */ @GET("api/categories") suspend fun getCategories(): Response> /** - * 새로운 카테고리 생성 - * - * @param request 생성할 카테고리 정보 (이름, ID) - * @return 생성된 카테고리 정보를 담은 Response - * API: POST /api/categories + * 새로운 카테고리 생성 - 헤더를 파라미터로 전달 */ @POST("api/categories") suspend fun createCategory( + @Header("Content-Type") contentType: String = "application/json", + @Header("Accept") accept: String = "application/json", @Body request: CategoryCreateRequest ): Response - /** - * 특정 카테고리 삭제 - * - * @param categoryId 삭제할 카테고리의 ID - * @return 삭제 결과를 담은 Response (성공 시 빈 응답) - * API: DELETE /api/categories/{id} - */ @DELETE("api/categories/{id}") suspend fun deleteCategory( - @Path("id") categoryId: Long // Int에서 Long으로 변경 + @Path("id") categoryId: Long ): Response -} +} \ No newline at end of file diff --git a/data/src/main/java/com/example/data/di/NetworkModule.kt b/data/src/main/java/com/example/data/di/NetworkModule.kt index cf78024..3444c1f 100644 --- a/data/src/main/java/com/example/data/di/NetworkModule.kt +++ b/data/src/main/java/com/example/data/di/NetworkModule.kt @@ -13,6 +13,7 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory import java.util.concurrent.TimeUnit +import javax.inject.Named import javax.inject.Singleton /** @@ -59,16 +60,42 @@ object NetworkModule { * - 로깅 인터셉터 추가 * - 타임아웃 설정 (연결, 읽기, 쓰기 각각 30초) */ +// NetworkModule.kt - 인터셉터 순서 변경 및 강화 @Provides @Singleton fun provideOkHttpClient( loggingInterceptor: HttpLoggingInterceptor ): OkHttpClient { return OkHttpClient.Builder() - .addInterceptor(loggingInterceptor) // HTTP 로깅 - .connectTimeout(30, TimeUnit.SECONDS) // 연결 타임아웃 - .readTimeout(30, TimeUnit.SECONDS) // 읽기 타임아웃 - .writeTimeout(30, TimeUnit.SECONDS) // 쓰기 타임아웃 + // 로깅 인터셉터를 먼저 추가 + .addInterceptor(loggingInterceptor) + // 헤더 인터셉터를 나중에 추가 (더 높은 우선순위) + .addInterceptor { chain -> + val original = chain.request() + + // 강제로 로그 출력 + android.util.Log.d("HeaderInterceptor", "🔧 헤더 인터셉터 실행됨!") + android.util.Log.d("HeaderInterceptor", "원본 URL: ${original.url}") + android.util.Log.d("HeaderInterceptor", "원본 헤더: ${original.headers}") + + val newRequest = original.newBuilder() + .removeHeader("Content-Type") // 기존 헤더 제거 + .removeHeader("Accept") + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Barrion-Android") + .build() + + android.util.Log.d("HeaderInterceptor", "수정된 헤더: ${newRequest.headers}") + + val response = chain.proceed(newRequest) + android.util.Log.d("HeaderInterceptor", "응답 코드: ${response.code}") + + response + } + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) .build() } @@ -107,7 +134,33 @@ object NetworkModule { */ @Provides @Singleton - fun provideCategoryApi(retrofit: Retrofit): CategoryApi { + fun provideCategoryApi(@Named("CategoryRetrofit") retrofit: Retrofit): CategoryApi { return retrofit.create(CategoryApi::class.java) } + // NetworkModule.kt에 추가 + @Provides + @Singleton + @Named("CategoryRetrofit") + fun provideCategoryRetrofit(json: Json): Retrofit { + val client = OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build() + + android.util.Log.d("CategoryRetrofit", "헤더 추가됨: ${request.headers}") + chain.proceed(request) + } + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + } } \ No newline at end of file diff --git a/data/src/main/java/com/example/data/dto/CategoryDto.kt b/data/src/main/java/com/example/data/dto/CategoryDto.kt index 07d13e1..9f252fa 100644 --- a/data/src/main/java/com/example/data/dto/CategoryDto.kt +++ b/data/src/main/java/com/example/data/dto/CategoryDto.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable @Serializable data class CategoryDto( @SerialName("categoryId") - val categoryId: Long, // Int에서 Long으로 변경 + val categoryId: Long, @SerialName("categoryName") val categoryName: String ) @@ -15,7 +15,7 @@ data class CategoryDto( @Serializable data class CategoryCreateRequest( @SerialName("categoryId") - val categoryId: Long, // Int에서 Long으로 변경 + val categoryId: Long, // 필수 필드로 변경 @SerialName("categoryName") val categoryName: String ) diff --git a/data/src/main/java/com/example/data/mapper/CategoryMapper.kt b/data/src/main/java/com/example/data/mapper/CategoryMapper.kt index 09a3303..d1316bc 100644 --- a/data/src/main/java/com/example/data/mapper/CategoryMapper.kt +++ b/data/src/main/java/com/example/data/mapper/CategoryMapper.kt @@ -22,12 +22,15 @@ import com.example.domain.model.Category * - order: API에 order 필드가 없으므로 categoryId를 Int로 변환하여 사용 * - 나머지 필드들은 기본값 사용 */ +/** + * 카테고리 관련 데이터 변환을 담당하는 매퍼 함수들 + */ + fun CategoryDto.toDomain(): Category { return Category( - id = categoryId, // Long 그대로 사용 + id = categoryId, name = categoryName, - order = categoryId.toInt(), // Long을 Int로 변환 (order 필드용) - // API에 없는 필드들은 기본값 설정 + order = categoryId.toInt(), isDefault = false, menuCount = 0, createdAt = System.currentTimeMillis() @@ -44,9 +47,16 @@ fun CategoryDto.toDomain(): Category { * - id → categoryId: Domain의 id를 서버의 categoryId로 매핑 * - name → categoryName: Domain의 name을 서버의 categoryName으로 매핑 */ + + +/** + * Domain 모델을 서버 생성 요청 DTO로 변환 + * ⚠️ categoryId는 서버에서 자동 생성하므로 포함하지 않음 + */ +// 2. CategoryMapper.kt 수정 fun Category.toCreateRequest(): CategoryCreateRequest { return CategoryCreateRequest( - categoryId = id, // Long 그대로 사용 + categoryId = id, // ID 포함해서 전송 categoryName = name ) } diff --git a/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt b/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt index 6ad4b95..30f163b 100644 --- a/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt +++ b/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt @@ -3,6 +3,8 @@ package com.example.data.repository import android.util.Log import com.example.data.api.CategoryApi import com.example.data.api.MenuApi +import com.example.data.dto.CategoryCreateRequest +import com.example.data.dto.CategoryDto import com.example.data.mapper.toDomain import com.example.data.mapper.toCategoryDomainList import com.example.data.mapper.toMenuDomainList @@ -15,7 +17,16 @@ import javax.inject.Inject import javax.inject.Singleton import com.example.data.dto.MenuPageResponse import com.example.data.dto.MenuDto +import kotlinx.serialization.json.Json import retrofit2.Response +// 필요한 import도 추가 +import okhttp3.OkHttpClient +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +// 필요한 import 추가 +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import java.util.concurrent.TimeUnit /** * MenuRepository 구현체 * - 실제 API와 연동하여 메뉴/카테고리 데이터 관리 @@ -79,60 +90,117 @@ class MenuRepositoryImpl @Inject constructor( */ override suspend fun addCategory(name: String): Result { return try { - Log.d(TAG, "📁 카테고리 추가 시작: $name") + Log.d(TAG, "📁 카테고리 추가 (ID 포함): $name") + + // 먼저 기존 카테고리들을 조회해서 다음 ID 계산 + val existingCategories = getCategories().getOrElse { emptyList() } + val nextId = if (existingCategories.isNotEmpty()) { + existingCategories.maxOf { it.id } + 1 + } else { + 1L + } - // 임시 ID 생성 (서버에서 실제 ID 할당) val tempCategory = Category( - id = 0, // 서버에서 할당받을 예정 + id = nextId, // 계산된 다음 ID 사용 name = name, - order = 999, // 임시 순서 + order = 999, isDefault = false, menuCount = 0 ) val request = tempCategory.toCreateRequest() - Log.d(TAG, "📁 요청 데이터: $request") + Log.d(TAG, "📁 요청 데이터 (ID 포함): $request") + + val client = OkHttpClient.Builder() + .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .build() + + val json = """{"categoryId":${nextId},"categoryName":"$name"}""" + Log.d(TAG, "📁 전송할 JSON: $json") + + val requestBody = json.toRequestBody("application/json; charset=utf-8".toMediaType()) + + val httpRequest = okhttp3.Request.Builder() + .url("http://13.209.99.95:8080/api/categories") + .post(requestBody) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Barrion-Android-Manual") + .build() - val response = categoryApi.createCategory(request) - Log.d(TAG, "📁 서버 응답 코드: ${response.code()}") - Log.d(TAG, "📁 서버 응답 메시지: ${response.message()}") + val response = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + client.newCall(httpRequest).execute() + } + + Log.d(TAG, "📁 응답 코드: ${response.code}") + Log.d(TAG, "📁 응답 성공 여부: ${response.isSuccessful}") if (response.isSuccessful) { - val createdCategory = response.body()?.toDomain() - ?: throw Exception("서버 응답이 비어있습니다") - Log.d(TAG, "✅ 카테고리 생성 성공: $createdCategory") - Result.success(createdCategory) - } else { - // 에러 바디도 확인 - val errorBody = response.errorBody()?.string() - Log.e(TAG, "❌ 카테고리 생성 실패") - Log.e(TAG, "❌ 응답 코드: ${response.code()}") - Log.e(TAG, "❌ 응답 메시지: ${response.message()}") - Log.e(TAG, "❌ 에러 바디: $errorBody") + val responseBody = response.body?.string() ?: "" + Log.d(TAG, "✅ 카테고리 생성 성공: $responseBody") + + val jsonParser = Json { ignoreUnknownKeys = true } + val categoryDto = jsonParser.decodeFromString(responseBody) + val category = categoryDto.toDomain() - Result.failure(Exception("카테고리 생성 실패: ${response.code()} - ${response.message()}")) + Result.success(category) + } else { + val errorBody = response.body?.string() ?: "" + Log.e(TAG, "❌ 카테고리 생성 실패: ${response.code} - $errorBody") + Result.failure(Exception("카테고리 생성 실패: ${response.code} - $errorBody")) } + } catch (e: Exception) { - Log.e(TAG, "💥 카테고리 생성 중 예외 발생: ${e.message}", e) - Result.failure(Exception("카테고리 생성 중 네트워크 오류: ${e.message}")) + Log.e(TAG, "💥 카테고리 생성 중 예외: ${e.message}", e) + Result.failure(e) } } - /** * 카테고리 삭제 * 기존: 성공만 반환 → 변경: 실제 API 호출 */ + + // 3. MenuRepositoryImpl.kt의 deleteCategory 수정 override suspend fun deleteCategory(categoryId: Long): Result { return try { - // 실제 API 호출 - val response = categoryApi.deleteCategory(categoryId) + Log.d(TAG, "🗑️ 카테고리 삭제: $categoryId") + + val client = OkHttpClient.Builder() + .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .build() + + val request = okhttp3.Request.Builder() + .url("http://13.209.99.95:8080/api/categories/$categoryId") + .delete() + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Barrion-Android-Manual") + .build() + + Log.d(TAG, "🗑️ 삭제 요청 URL: ${request.url}") + Log.d(TAG, "🗑️ 삭제 요청 메서드: ${request.method}") + + val response = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + client.newCall(request).execute() + } + + Log.d(TAG, "🗑️ 삭제 응답 코드: ${response.code}") + Log.d(TAG, "🗑️ 삭제 응답 성공 여부: ${response.isSuccessful}") if (response.isSuccessful) { + Log.d(TAG, "✅ 카테고리 삭제 성공") Result.success(Unit) } else { - Result.failure(Exception("카테고리 삭제 실패: ${response.code()} - ${response.message()}")) + val errorBody = response.body?.string() ?: "" + Log.e(TAG, "❌ 카테고리 삭제 실패: ${response.code} - $errorBody") + Result.failure(Exception("카테고리 삭제 실패: ${response.code} - $errorBody")) } + } catch (e: Exception) { + Log.e(TAG, "💥 카테고리 삭제 중 예외: ${e.message}", e) Result.failure(Exception("카테고리 삭제 중 네트워크 오류: ${e.message}")) } } diff --git a/domain/src/main/java/com/example/domain/usecase/menu/DeleteCategoryUseCase.kt b/domain/src/main/java/com/example/domain/usecase/menu/DeleteCategoryUseCase.kt new file mode 100644 index 0000000..9cdcbc9 --- /dev/null +++ b/domain/src/main/java/com/example/domain/usecase/menu/DeleteCategoryUseCase.kt @@ -0,0 +1,17 @@ +package com.example.domain.usecase.menu + +import com.example.domain.repository.MenuRepository +import javax.inject.Inject + +class DeleteCategoryUseCase @Inject constructor( + private val menuRepository: MenuRepository +) { + suspend fun execute(categoryId: Long): Result { + // 기본 카테고리 삭제 방지 + if (categoryId <= 0) { + return Result.failure(IllegalArgumentException("유효하지 않은 카테고리 ID입니다")) + } + + return menuRepository.deleteCategory(categoryId) + } +} \ No newline at end of file diff --git a/feature/menu/src/main/java/com/example/menu/screen/CategoryManagementScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/CategoryManagementScreen.kt index 1406ed7..fb0795f 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/CategoryManagementScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/CategoryManagementScreen.kt @@ -35,13 +35,14 @@ fun CategoryManagementScreen( ) { // State 구독 val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } // 다이얼로그 상태들 var showAddCategoryDialog by remember { mutableStateOf(false) } var showDeleteCategoryDialog by remember { mutableStateOf(false) } var categoryToDelete by remember { mutableStateOf(null) } - // Effect 처리 + // Effect 처리 - 에러/토스트 케이스 추가 LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { @@ -52,6 +53,18 @@ fun CategoryManagementScreen( showDeleteCategoryDialog = false categoryToDelete = null } + is MenuEffect.ShowError -> { + snackbarHostState.showSnackbar( + message = effect.error, + duration = SnackbarDuration.Long + ) + } + is MenuEffect.ShowToast -> { + snackbarHostState.showSnackbar( + message = effect.message, + duration = SnackbarDuration.Short + ) + } else -> {} } } @@ -59,6 +72,7 @@ fun CategoryManagementScreen( Scaffold( containerColor = MaterialTheme.barrionColors.white, + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { CenterAlignedTopAppBar( title = { diff --git a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt index 381196d..f1af405 100644 --- a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt +++ b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.example.domain.usecase.menu.AddCategoryUseCase import com.example.domain.usecase.menu.GetMenusUseCase import com.example.domain.usecase.menu.AddMenuUseCase +import com.example.domain.usecase.menu.DeleteCategoryUseCase import com.example.domain.usecase.menu.DeleteMenuUseCase import com.example.domain.usecase.menu.UpdateMenuUseCase import com.example.menu.type.MenuIntent @@ -23,19 +24,14 @@ import javax.inject.Inject /** * 메뉴 화면의 ViewModel - MVI 패턴 구현 */ -//class MenuViewModel( -// private val getMenusUseCase: GetMenusUseCase, -// private val addMenuUseCase: AddMenuUseCase, -// private val deleteMenuUseCase: DeleteMenuUseCase -//) : ViewModel() { - -@HiltViewModel //Hilt 추가 +@HiltViewModel class MenuViewModel @Inject constructor( private val getMenusUseCase: GetMenusUseCase, private val addMenuUseCase: AddMenuUseCase, private val deleteMenuUseCase: DeleteMenuUseCase, - private val addCategoryUseCase: AddCategoryUseCase, // 추가 - private val updateMenuUseCase: UpdateMenuUseCase // 추가 + private val addCategoryUseCase: AddCategoryUseCase, + private val updateMenuUseCase: UpdateMenuUseCase, + private val deleteCategoryUseCase: DeleteCategoryUseCase // 쉼표 추가 ) : ViewModel() { private val _state = MutableStateFlow(MenuState()) @@ -49,30 +45,29 @@ class MenuViewModel @Inject constructor( } fun handleIntent(intent: MenuIntent) { - println("Intent 받음: $intent") // 디버그 로그 추가 + println("Intent 받음: $intent") when (intent) { is MenuIntent.LoadMenus -> loadMenus() is MenuIntent.RefreshData -> refreshData() is MenuIntent.NavigateToCategoryManagement -> { - println("카테고리 관리로 이동 Intent 처리") // 디버그 로그 추가 + println("카테고리 관리로 이동 Intent 처리") navigateToCategoryManagement() } is MenuIntent.NavigateToAddMenu -> navigateToAddMenu() is MenuIntent.NavigateToCategoryDetail -> navigateToCategoryDetail(intent.categoryId) is MenuIntent.AddMenu -> addMenu(intent) - is MenuIntent.UpdateMenu -> updateMenu(intent.menu) // 추가 + is MenuIntent.UpdateMenu -> updateMenu(intent.menu) is MenuIntent.DeleteMenu -> deleteMenu(intent.menuId) - is MenuIntent.AddCategory -> addCategory(intent.name) // 추가 - is MenuIntent.DeleteCategory -> deleteCategory(intent.categoryId) // 추가 + is MenuIntent.AddCategory -> addCategory(intent.name) + is MenuIntent.DeleteCategory -> deleteCategory(intent.categoryId) is MenuIntent.ClearError -> clearError() else -> { /* TODO: 나머지 구현 */ } } } -// 새로운 함수들 추가 + /** * 카테고리 추가 */ - // addCategory 함수 수정 private fun addCategory(name: String) { viewModelScope.launch { println("📁 ViewModel - 카테고리 추가 시작: $name") @@ -98,12 +93,21 @@ class MenuViewModel @Inject constructor( */ private fun deleteCategory(categoryId: Long) { viewModelScope.launch { - // TODO: DeleteCategoryUseCase 구현 후 사용 - // 임시로 성공 처리 - _effect.emit(MenuEffect.CategoryDeletedSuccessfully) - _effect.emit(MenuEffect.ShowToast("카테고리가 삭제되었습니다")) - // 데이터 다시 로드 - loadMenus() + println("🗑️ ViewModel - 카테고리 삭제 시작: $categoryId") + + deleteCategoryUseCase.execute(categoryId) + .onSuccess { + println("✅ ViewModel - 카테고리 삭제 성공") + _effect.emit(MenuEffect.CategoryDeletedSuccessfully) + _effect.emit(MenuEffect.ShowToast("카테고리가 삭제되었습니다")) + loadMenus() // 데이터 새로고침 + } + .onFailure { exception -> + println("❌ ViewModel - 카테고리 삭제 실패: ${exception.message}") + _effect.emit(MenuEffect.ShowError( + exception.message ?: "카테고리 삭제 중 오류가 발생했습니다" + )) + } } } @@ -164,18 +168,17 @@ class MenuViewModel @Inject constructor( ) } } -// updateMenu 함수 추가 + /** * 메뉴 수정 */ - // updateMenu 함수 수정 private fun updateMenu(menu: com.example.domain.model.Menu) { viewModelScope.launch { updateMenuUseCase.execute(menu) .onSuccess { updatedMenu -> _effect.emit(MenuEffect.MenuUpdatedSuccessfully) _effect.emit(MenuEffect.ShowToast("메뉴가 수정되었습니다")) - loadMenus() // 데이터 새로고침 + loadMenus() } .onFailure { exception -> _effect.emit(MenuEffect.ShowError( @@ -184,6 +187,7 @@ class MenuViewModel @Inject constructor( } } } + private fun deleteMenu(menuId: Long) { viewModelScope.launch { deleteMenuUseCase.execute(menuId) @@ -201,7 +205,7 @@ class MenuViewModel @Inject constructor( private fun navigateToCategoryManagement() { viewModelScope.launch { - println("NavigateToCategoryManagement Effect 발생") // 디버그 로그 추가 + println("NavigateToCategoryManagement Effect 발생") _effect.emit(MenuEffect.NavigateToCategoryManagement) } } @@ -224,4 +228,4 @@ class MenuViewModel @Inject constructor( private fun clearError() { _state.value = _state.value.copy(error = null) } -} +} \ No newline at end of file diff --git a/feature/order/src/main/java/com/example/order/screen/OrderScreen.kt b/feature/order/src/main/java/com/example/order/screen/OrderScreen.kt index 1d90a75..31603e3 100644 --- a/feature/order/src/main/java/com/example/order/screen/OrderScreen.kt +++ b/feature/order/src/main/java/com/example/order/screen/OrderScreen.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -20,6 +22,7 @@ import com.example.ui.theme.Spacing import com.example.ui.theme.CornerRadius import com.example.ui.components.buttons.BarrionNavigationButtons +@OptIn(ExperimentalMaterial3Api::class) @Composable fun OrderScreen( viewModel: OrderViewModel, @@ -28,96 +31,124 @@ fun OrderScreen( val state by viewModel.state.collectAsStateWithLifecycle() var showDeleteDialog by rememberSaveable { mutableStateOf(false) } var orderToDelete by rememberSaveable { mutableIntStateOf(0) } + val snackbarHostState = remember { SnackbarHostState() } // Effect 처리 LaunchedEffect(viewModel.effect) { viewModel.effect.collect { effect -> when (effect) { is OrderEffect.ShowError -> { - // 에러 스낵바 또는 토스트 처리 + snackbarHostState.showSnackbar(effect.message) } is OrderEffect.ShowDeleteSuccess -> { - // 성공 메시지 처리 + snackbarHostState.showSnackbar("${effect.orderNumber}번 주문이 삭제되었습니다") + } + is OrderEffect.NavigateToOrderDetail -> { + // 주문 상세 화면으로 네비게이션 } - else -> Unit } } } - Column( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.barrionColors.white) - .padding(Spacing.Medium) - ) { - // 상단 헤더 - 주문 관리만 가운데 표시 - Text( - text = "주문 관리", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.barrionColors.grayBlack, - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally) - .padding(bottom = Spacing.Large) - ) - - // 정산 현황 카드 - if (state.summary != null) { - OrderSummaryCard( - summary = state.summary!!, - modifier = Modifier.fillMaxWidth() + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "주문 관리", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + }, + actions = { + IconButton( + onClick = { viewModel.handleIntent(OrderIntent.RefreshData) } + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "새로고침" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.barrionColors.white, + titleContentColor = MaterialTheme.barrionColors.grayBlack, + actionIconContentColor = MaterialTheme.barrionColors.grayBlack + ) ) - - Spacer(modifier = Modifier.height(Spacing.Large)) - } - - // 주문 내역 제목 - 왼쪽 배치로 변경 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + containerColor = MaterialTheme.barrionColors.white + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(Spacing.Medium) ) { - // 왼쪽: 주문 내역 제목 - Text( - text = "주문 내역", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.barrionColors.grayBlack, - fontWeight = FontWeight.Bold - ) + // 정산 현황 카드 + if (state.summary != null) { + OrderSummaryCard( + summary = state.summary!!, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(Spacing.Large)) + } - // 오른쪽: 총 건수 - Text( - text = "총 ${state.orders.size}건", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.barrionColors.grayMedium - ) - } + // 주문 내역 제목 - 왼쪽 배치로 변경 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // 왼쪽: 주문 내역 제목 + Text( + text = "주문 내역", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.barrionColors.grayBlack, + fontWeight = FontWeight.Bold + ) + + // 오른쪽: 총 건수 + Text( + text = "총 ${state.orders.size}건", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.barrionColors.grayMedium + ) + } - Spacer(modifier = Modifier.height(Spacing.Medium)) + Spacer(modifier = Modifier.height(Spacing.Medium)) - // 주문 목록 - when { - state.isLoading -> { - OrderLoadingState() - } + // 주문 목록 + when { + state.isLoading -> { + OrderLoadingState() + } - state.isEmpty -> { - OrderEmptyState() - } + state.isEmpty -> { + OrderEmptyState() + } - else -> { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(Spacing.Medium), - modifier = Modifier.weight(1f) - ) { - items(state.orders) { order -> - OrderListItem( - order = order, - onDeleteClick = { orderId -> - orderToDelete = orderId - showDeleteDialog = true - } - ) + else -> { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(Spacing.Medium), + modifier = Modifier.weight(1f) + ) { + items(state.orders) { order -> + OrderListItem( + order = order, + onDeleteClick = { orderId -> + orderToDelete = orderId + showDeleteDialog = true + } + ) + } } } } From f896c60d8b33afa05b466830f6377c38952aa71b Mon Sep 17 00:00:00 2001 From: Kyu hyunSung Date: Thu, 29 May 2025 21:00:22 +0900 Subject: [PATCH 2/4] [CHORE/#25] Image dialgoue --- .../example/menu/component/ImageUploadArea.kt | 116 ++++++++++++++---- .../com/example/menu/screen/AddMenuScreen.kt | 42 +++++-- .../com/example/menu/screen/EditMenuScreen.kt | 56 ++++++++- .../com/example/menu/screen/MenuMviScreen.kt | 38 +++++- .../example/menu/viewmodel/MenuViewModel.kt | 13 ++ 5 files changed, 221 insertions(+), 44 deletions(-) diff --git a/feature/menu/src/main/java/com/example/menu/component/ImageUploadArea.kt b/feature/menu/src/main/java/com/example/menu/component/ImageUploadArea.kt index 5b72a29..a3d9fe5 100644 --- a/feature/menu/src/main/java/com/example/menu/component/ImageUploadArea.kt +++ b/feature/menu/src/main/java/com/example/menu/component/ImageUploadArea.kt @@ -76,14 +76,22 @@ fun ImageUploadArea( ) { uri: Uri? -> uri?.let { imageUri -> try { + println("🖼️ 갤러리 이미지 선택됨: $imageUri") val inputStream = context.contentResolver.openInputStream(imageUri) - val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream) + val bitmap = BitmapFactory.decodeStream(inputStream) inputStream?.close() - // Bitmap을 Base64로 변환 - val base64String = bitmapToBase64(bitmap) - onImageSelected("data:image/jpeg;base64,$base64String") + if (bitmap != null) { + // Bitmap을 Base64로 변환 + val base64String = bitmapToBase64(bitmap) + val dataUri = "data:image/jpeg;base64,$base64String" + onImageSelected(dataUri) + println("🖼️ 갤러리 이미지 Base64 변환 완료: ${base64String.take(50)}...") + } else { + println("❌ 갤러리 이미지 Bitmap 변환 실패") + } } catch (e: Exception) { + println("❌ 갤러리 이미지 처리 오류: ${e.message}") e.printStackTrace() } } @@ -95,15 +103,25 @@ fun ImageUploadArea( ) { success: Boolean -> if (success) { try { - val bitmap = android.graphics.BitmapFactory.decodeFile(photoFile.absolutePath) - val base64String = bitmapToBase64(bitmap) - onImageSelected("data:image/jpeg;base64,$base64String") + println("🖼️ 카메라 촬영 완료") + val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath) + if (bitmap != null) { + val base64String = bitmapToBase64(bitmap) + val dataUri = "data:image/jpeg;base64,$base64String" + onImageSelected(dataUri) + println("🖼️ 카메라 이미지 Base64 변환 완료: ${base64String.take(50)}...") + } else { + println("❌ 카메라 이미지 Bitmap 변환 실패") + } // 임시 파일 삭제 photoFile.delete() } catch (e: Exception) { + println("❌ 카메라 이미지 처리 오류: ${e.message}") e.printStackTrace() } + } else { + println("❌ 카메라 촬영 실패") } } @@ -118,6 +136,8 @@ fun ImageUploadArea( permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: false } + println("🔒 권한 결과 - 카메라: $cameraGranted, 저장소: $storageGranted") + if (cameraGranted || storageGranted) { showImagePicker = true } else { @@ -140,6 +160,7 @@ fun ImageUploadArea( shape = RoundedCornerShape(12.dp) ) .clickable { + println("🖼️ 이미지 영역 클릭됨") // 권한 체크 val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) val storagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -148,6 +169,8 @@ fun ImageUploadArea( ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) } + println("🔒 현재 권한 - 카메라: ${cameraPermission == PackageManager.PERMISSION_GRANTED}, 저장소: ${storagePermission == PackageManager.PERMISSION_GRANTED}") + if (cameraPermission == PackageManager.PERMISSION_GRANTED || storagePermission == PackageManager.PERMISSION_GRANTED) { showImagePicker = true @@ -164,15 +187,14 @@ fun ImageUploadArea( permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE) } } + println("🔒 권한 요청: ${permissions.joinToString()}") permissionLauncher.launch(permissions.toTypedArray()) } }, contentAlignment = Alignment.Center ) { -// AsyncImage 부분을 다음과 같이 수정: - if (imageUrl.isEmpty()) { - // 이미지가 없을 때 (기존 코드 그대로) + // 이미지가 없을 때 Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center @@ -194,39 +216,76 @@ fun ImageUploadArea( ) } } else { - // 이미지가 있을 때 - Base64 처리 개선 + // 이미지가 있을 때 - Base64와 URL 모두 처리 if (imageUrl.startsWith("data:image")) { // Base64 이미지인 경우 - val base64Data = imageUrl.substringAfter("base64,") - val imageBytes = Base64.decode(base64Data, Base64.DEFAULT) - val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + var bitmap by remember { mutableStateOf(null) } + var hasError by remember { mutableStateOf(false) } + + LaunchedEffect(imageUrl) { + try { + println("🖼️ Base64 이미지 표시 시도") + val base64Data = imageUrl.substringAfter("base64,") + val imageBytes = Base64.decode(base64Data, Base64.DEFAULT) + val decodedBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + if (decodedBitmap != null) { + println("🖼️ Base64 이미지 표시 성공") + bitmap = decodedBitmap + hasError = false + } else { + println("❌ Base64 Bitmap 생성 실패") + hasError = true + } + } catch (e: Exception) { + println("❌ Base64 디코딩 오류: ${e.message}") + hasError = true + } + } if (bitmap != null) { Image( - bitmap = bitmap.asImageBitmap(), + bitmap = bitmap!!.asImageBitmap(), contentDescription = "선택된 이미지", modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(12.dp)), contentScale = ContentScale.Crop ) - } else { - // Bitmap 생성 실패 시 placeholder - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + } else if (hasError) { + // 에러 상태 표시 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Text("이미지 로드 실패", color = MaterialTheme.colorScheme.error) + Icon( + imageVector = Icons.Default.CameraAlt, + contentDescription = "이미지 오류", + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error + ) + Text( + text = "이미지 로드 실패", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) } + } else { + // 로딩 상태 + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + color = MaterialTheme.colorScheme.primary + ) } } else { - // 일반 URL인 경우 (기존 코드) + // 일반 URL인 경우 + println("🖼️ URL 이미지 표시: $imageUrl") AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(imageUrl) .crossfade(true) .build(), - contentDescription = "선택된 이미지", + contentDescription = "메뉴 이미지", modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(12.dp)), @@ -234,7 +293,7 @@ fun ImageUploadArea( ) } - // 오버레이 (기존 코드 그대로) + // 이미지 변경 오버레이 Box( modifier = Modifier .fillMaxSize() @@ -260,11 +319,18 @@ fun ImageUploadArea( // 이미지 선택 다이얼로그 ImagePickerDialog( isVisible = showImagePicker, - onDismiss = { showImagePicker = false }, + onDismiss = { + showImagePicker = false + println("🖼️ 이미지 선택 다이얼로그 닫힘") + }, onCameraSelected = { + println("🖼️ 카메라 선택됨") + showImagePicker = false cameraLauncher.launch(photoUri) }, onGallerySelected = { + println("🖼️ 갤러리 선택됨") + showImagePicker = false galleryLauncher.launch("image/*") } ) diff --git a/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt index eeed833..eda8287 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt @@ -31,12 +31,12 @@ import com.example.menu.viewmodel.MenuViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddMenuScreen( - selectedCategoryId: Long? = null, // 미리 선택된 카테고리 (카테고리 상세에서 온 경우) + selectedCategoryId: Long? = null, viewModel: MenuViewModel, onNavigateBack: () -> Unit = {} ) { - // State 구독 val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } // 폼 상태들 var menuName by remember { mutableStateOf("") } @@ -44,22 +44,39 @@ fun AddMenuScreen( var description by remember { mutableStateOf("") } var selectedCategory by remember { mutableStateOf(selectedCategoryId ?: 0L) } var imageUrl by remember { mutableStateOf("") } - var selectedBase64Image by remember { mutableStateOf(null) } // 추가 + var selectedBase64Image by remember { mutableStateOf(null) } // 에러 상태들 var nameError by remember { mutableStateOf("") } var priceError by remember { mutableStateOf("") } var categoryError by remember { mutableStateOf("") } - // Effect 처리 + // Effect 처리 - 성공/에러 메시지 표시 후 화면 닫기 LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { is MenuEffect.MenuAddedSuccessfully -> { - onNavigateBack() // 성공 시 뒤로가기 + // 성공 메시지 표시 + snackbarHostState.showSnackbar( + message = "메뉴가 추가되었습니다", + duration = SnackbarDuration.Short + ) + // 즉시 화면 닫기 (delay 제거) + onNavigateBack() } is MenuEffect.ShowError -> { - // TODO: 토스트 메시지 또는 스낵바 표시 + // 에러 메시지 표시 + snackbarHostState.showSnackbar( + message = effect.error, + duration = SnackbarDuration.Long + ) + } + is MenuEffect.ShowToast -> { + // 토스트 메시지 표시 + snackbarHostState.showSnackbar( + message = effect.message, + duration = SnackbarDuration.Short + ) } else -> {} } @@ -67,6 +84,7 @@ fun AddMenuScreen( } Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( title = { Text("메뉴 추가") }, @@ -114,7 +132,7 @@ fun AddMenuScreen( categoryId = selectedCategory, description = description.trim(), imageUrl = imageUrl, - base64Image = selectedBase64Image // 추가 + base64Image = selectedBase64Image ) ) } @@ -153,8 +171,8 @@ fun AddMenuScreen( categories = state.categories, imageUrl = imageUrl, onImageChange = { imageUrl = it }, - selectedBase64Image = selectedBase64Image, // 추가 - onBase64ImageChange = { base64 -> // 추가 + selectedBase64Image = selectedBase64Image, + onBase64ImageChange = { base64 -> println("🖼️ ImageUpload - 이미지 선택됨: ${base64?.take(50) ?: "null"}...") selectedBase64Image = base64 }, @@ -182,8 +200,8 @@ private fun AddMenuContent( categories: List, imageUrl: String, onImageChange: (String) -> Unit, - selectedBase64Image: String?, // 추가 - onBase64ImageChange: (String?) -> Unit, // 추가 + selectedBase64Image: String?, + onBase64ImageChange: (String?) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -253,7 +271,7 @@ private fun AddMenuContent( // 설명 OutlinedTextField( value = description, - onValueChange = onDescriptionChange, // 수정: onDescriptionChange → onValueChange + onValueChange = onDescriptionChange, label = { Text("설명") }, placeholder = { Text("메뉴 설명 입력") }, modifier = Modifier diff --git a/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt index d2443c4..37ab070 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt @@ -35,6 +35,7 @@ fun EditMenuScreen( ) { // State 구독 val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } // 수정할 메뉴 찾기 val menuToEdit = remember(state.menusByCategory, menuId) { @@ -47,21 +48,39 @@ fun EditMenuScreen( var description by remember(menuToEdit) { mutableStateOf(menuToEdit?.description ?: "") } var selectedCategory by remember(menuToEdit) { mutableStateOf(menuToEdit?.categoryId ?: 0L) } var imageUrl by remember(menuToEdit) { mutableStateOf(menuToEdit?.imageUrl ?: "") } + var selectedBase64Image by remember { mutableStateOf(null) } // Base64 이미지 추가 // 에러 상태들 var nameError by remember { mutableStateOf("") } var priceError by remember { mutableStateOf("") } var categoryError by remember { mutableStateOf("") } - // Effect 처리 + // Effect 처리 - 성공/에러 메시지 표시 LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { is MenuEffect.MenuUpdatedSuccessfully -> { - onNavigateBack() // 성공 시 뒤로가기 + // 성공 메시지 표시 + snackbarHostState.showSnackbar( + message = "메뉴가 수정되었습니다", + duration = SnackbarDuration.Short + ) + // 즉시 화면 닫기 + onNavigateBack() } is MenuEffect.ShowError -> { - // TODO: 토스트 메시지 또는 스낵바 표시 + // 에러 메시지 표시 + snackbarHostState.showSnackbar( + message = effect.error, + duration = SnackbarDuration.Long + ) + } + is MenuEffect.ShowToast -> { + // 토스트 메시지 표시 + snackbarHostState.showSnackbar( + message = effect.message, + duration = SnackbarDuration.Short + ) } else -> {} } @@ -90,6 +109,7 @@ fun EditMenuScreen( } Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( title = { Text("메뉴 수정") }, @@ -133,8 +153,13 @@ fun EditMenuScreen( price = priceValue!!, categoryId = selectedCategory, description = description.trim(), - imageUrl = imageUrl + imageUrl = if (selectedBase64Image != null) "" else imageUrl // Base64가 있으면 URL 클리어 ) + + // Base64 이미지가 있으면 포함해서 수정 요청 + // 실제로는 UpdateMenuWithImage Intent가 필요할 수 있지만 + // 현재는 UpdateMenu Intent 사용 + println("🖼️ 수정 - Base64 이미지: ${selectedBase64Image?.take(50) ?: "없음"}") viewModel.handleIntent(MenuIntent.UpdateMenu(updatedMenu)) } } @@ -172,6 +197,11 @@ fun EditMenuScreen( categories = state.categories, imageUrl = imageUrl, onImageChange = { imageUrl = it }, + selectedBase64Image = selectedBase64Image, + onBase64ImageChange = { base64 -> + println("🖼️ 수정 - 이미지 선택됨: ${base64?.take(50) ?: "null"}") + selectedBase64Image = base64 + }, modifier = Modifier.padding(paddingValues) ) } @@ -196,6 +226,8 @@ private fun EditMenuContent( categories: List, imageUrl: String, onImageChange: (String) -> Unit, + selectedBase64Image: String?, // 추가 + onBase64ImageChange: (String?) -> Unit, // 추가 modifier: Modifier = Modifier ) { Column( @@ -223,10 +255,22 @@ private fun EditMenuContent( } } - // 이미지 업로드 영역 + // 이미지 업로드 영역 - Base64 처리 추가 ImageUploadArea( imageUrl = imageUrl, - onImageSelected = onImageChange, + onImageSelected = { base64OrUrl -> + println("🖼️ 수정 - ImageUploadArea 이미지 받음: ${base64OrUrl.take(50)}...") + + if (base64OrUrl.startsWith("data:image")) { + // Base64 이미지인 경우 + onBase64ImageChange(base64OrUrl) + println("🖼️ 수정 - Base64 데이터 저장됨") + } else { + // 일반 URL인 경우 + onImageChange(base64OrUrl) + println("🖼️ 수정 - URL 저장됨: $base64OrUrl") + } + }, modifier = Modifier.fillMaxWidth() ) diff --git a/feature/menu/src/main/java/com/example/menu/screen/MenuMviScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/MenuMviScreen.kt index ec60280..44106e9 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/MenuMviScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/MenuMviScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -40,24 +41,49 @@ fun MenuMviScreen( var showDeleteMenuDialog by remember { mutableStateOf(false) } var menuToDelete by remember { mutableStateOf(null) } - // Effect 처리 + // Effect 처리 - 데이터 업데이트 감지 추가 LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { + // 네비게이션 Effect is MenuEffect.NavigateToCategoryManagement -> onNavigateToCategoryManagement() is MenuEffect.NavigateToAddMenu -> onNavigateToAddMenu() is MenuEffect.NavigateToCategoryDetail -> { onNavigateToCategoryDetail(effect.categoryId, effect.categoryName) } + + // 메뉴 관련 Effect - 데이터 새로고침 트리거 is MenuEffect.MenuDeletedSuccessfully -> { showDeleteMenuDialog = false menuToDelete = null + // 자동으로 loadMenus()가 ViewModel에서 호출됨 + } + is MenuEffect.MenuAddedSuccessfully -> { + // 메뉴 추가 시 자동 새로고침 (ViewModel에서 처리) + } + is MenuEffect.MenuUpdatedSuccessfully -> { + // 메뉴 수정 시 자동 새로고침 (ViewModel에서 처리) + } + + // 카테고리 관련 Effect - 데이터 새로고침 트리거 + is MenuEffect.CategoryAddedSuccessfully -> { + // 카테고리 추가 시 자동 새로고침 (ViewModel에서 처리) + } + is MenuEffect.CategoryDeletedSuccessfully -> { + // 카테고리 삭제 시 자동 새로고침 (ViewModel에서 처리) } + else -> {} } } } + // 화면이 다시 보여질 때 데이터 새로고침 + LaunchedEffect(Unit) { + // 화면 진입 시 데이터 새로고침 + viewModel.handleIntent(MenuIntent.RefreshData) + } + // UI 구성 - 헤더 위치 조정 Scaffold( modifier = Modifier.fillMaxSize(), @@ -83,6 +109,16 @@ fun MenuMviScreen( } }, actions = { + // 새로고침 버튼 추가 + IconButton( + onClick = { viewModel.handleIntent(MenuIntent.RefreshData) } + ) { + Icon( + Icons.Default.Refresh, + contentDescription = "새로고침", + tint = MaterialTheme.barrionColors.grayMedium + ) + } IconButton( onClick = { viewModel.handleIntent(MenuIntent.NavigateToAddMenu) } ) { diff --git a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt index f1af405..83d8972 100644 --- a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt +++ b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt @@ -153,6 +153,7 @@ class MenuViewModel @Inject constructor( } } + // MenuViewModel.kt의 addMenu 함수 수정 private fun addMenu(intent: MenuIntent.AddMenu) { viewModelScope.launch { println("📱 ViewModel - 메뉴 추가 요청") @@ -166,6 +167,18 @@ class MenuViewModel @Inject constructor( description = intent.description, base64Image = intent.base64Image ) + .onSuccess { menu -> + println("✅ ViewModel - 메뉴 추가 성공: ${menu.name}") + _effect.emit(MenuEffect.MenuAddedSuccessfully) + _effect.emit(MenuEffect.ShowToast("메뉴가 추가되었습니다")) + loadMenus() // 데이터 새로고침 + } + .onFailure { exception -> + println("❌ ViewModel - 메뉴 추가 실패: ${exception.message}") + _effect.emit(MenuEffect.ShowError( + exception.message ?: "메뉴 추가 중 오류가 발생했습니다" + )) + } } } From 67f2aadc87796732984e6660b94650587e6dee49 Mon Sep 17 00:00:00 2001 From: Kyu hyunSung Date: Thu, 29 May 2025 21:49:10 +0900 Subject: [PATCH 3/4] [CHORE/#25] menu fixed image --- .../data/repository/MenuRepositoryImpl.kt | 71 ++++++++++++++----- .../domain/repository/MenuRepository.kt | 1 + .../domain/usecase/menu/UpdateMenuUseCase.kt | 47 +++++++++++- .../com/example/menu/screen/EditMenuScreen.kt | 60 ++++++++-------- .../java/com/example/menu/type/MenuIntent.kt | 8 +++ .../example/menu/viewmodel/MenuViewModel.kt | 66 ++++++++++++++++- 6 files changed, 200 insertions(+), 53 deletions(-) diff --git a/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt b/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt index 30f163b..008740d 100644 --- a/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt +++ b/data/src/main/java/com/example/data/repository/MenuRepositoryImpl.kt @@ -27,6 +27,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import kotlinx.coroutines.withContext import kotlinx.coroutines.Dispatchers import java.util.concurrent.TimeUnit + /** * MenuRepository 구현체 * - 실제 API와 연동하여 메뉴/카테고리 데이터 관리 @@ -80,10 +81,6 @@ class MenuRepositoryImpl @Inject constructor( } } - /** - * 새 카테고리 추가 - * 기존: 임시 카테고리 생성 → 변경: 실제 API 호출 - */ /** * 새 카테고리 추가 * 기존: 임시 카테고리 생성 → 변경: 실제 API 호출 @@ -157,12 +154,11 @@ class MenuRepositoryImpl @Inject constructor( Result.failure(e) } } + /** * 카테고리 삭제 * 기존: 성공만 반환 → 변경: 실제 API 호출 */ - - // 3. MenuRepositoryImpl.kt의 deleteCategory 수정 override suspend fun deleteCategory(categoryId: Long): Result { return try { Log.d(TAG, "🗑️ 카테고리 삭제: $categoryId") @@ -270,7 +266,7 @@ class MenuRepositoryImpl @Inject constructor( * 새 메뉴 추가 * 기존: 임시 ID 할당 → 변경: 실제 API 호출 */ - override suspend fun addMenu(menu: Menu, base64Image: String?): Result { // = null 제거 + override suspend fun addMenu(menu: Menu, base64Image: String?): Result { return try { Log.d(TAG, "📱 메뉴 추가 시작: ${menu.name}") Log.d(TAG, "🖼️ Base64 이미지: ${base64Image?.take(50) ?: "없음"}...") @@ -297,24 +293,61 @@ class MenuRepositoryImpl @Inject constructor( } /** - * 메뉴 정보 수정 + * 메뉴 정보 수정 (이미지 없음) * 기존: 임시 리스트 수정 → 변경: 실제 API 호출 */ override suspend fun updateMenu(menu: Menu): Result { return try { - // base64Image 없이 메뉴 수정 (임시) + Log.d(TAG, "📱 메뉴 수정 (이미지 없음): ${menu.name}") + + // base64Image 없이 메뉴 수정 val request = menu.toUpdateRequest(base64Image = null) val response = menuApi.updateMenu(menu.id, request) if (response.isSuccessful) { val updatedMenu = response.body()?.toDomain() ?: throw Exception("서버 응답이 비어있습니다") + Log.d(TAG, "✅ 메뉴 수정 성공: ${updatedMenu.name}") + Result.success(updatedMenu) + } else { + val error = "메뉴 수정 실패: ${response.code()} - ${response.message()}" + Log.e(TAG, "❌ $error") + Result.failure(Exception(error)) + } + } catch (e: Exception) { + val error = "메뉴 수정 중 네트워크 오류: ${e.message}" + Log.e(TAG, "💥 $error", e) + Result.failure(Exception(error)) + } + } + + /** + * 메뉴 정보 수정 (이미지 포함) + * 새로 추가된 메서드 - Base64 이미지와 함께 메뉴 수정 + */ + override suspend fun updateMenuWithImage(menu: Menu, base64Image: String): Result { + return try { + Log.d(TAG, "📱 메뉴 수정 (이미지 포함): ${menu.name}") + Log.d(TAG, "🖼️ Base64 이미지: ${base64Image.take(50)}...") + + // base64Image와 함께 메뉴 수정 + val request = menu.toUpdateRequest(base64Image = base64Image) + val response = menuApi.updateMenu(menu.id, request) + + if (response.isSuccessful) { + val updatedMenu = response.body()?.toDomain() + ?: throw Exception("서버 응답이 비어있습니다") + Log.d(TAG, "✅ 이미지 포함 메뉴 수정 성공: ${updatedMenu.name}") Result.success(updatedMenu) } else { - Result.failure(Exception("메뉴 수정 실패: ${response.code()} - ${response.message()}")) + val error = "이미지 포함 메뉴 수정 실패: ${response.code()} - ${response.message()}" + Log.e(TAG, "❌ $error") + Result.failure(Exception(error)) } } catch (e: Exception) { - Result.failure(Exception("메뉴 수정 중 네트워크 오류: ${e.message}")) + val error = "이미지 포함 메뉴 수정 중 네트워크 오류: ${e.message}" + Log.e(TAG, "💥 $error", e) + Result.failure(Exception(error)) } } @@ -324,21 +357,23 @@ class MenuRepositoryImpl @Inject constructor( */ override suspend fun deleteMenu(menuId: Long): Result { return try { + Log.d(TAG, "🗑️ 메뉴 삭제: $menuId") + // 실제 API 호출 val response = menuApi.deleteMenu(menuId) if (response.isSuccessful) { + Log.d(TAG, "✅ 메뉴 삭제 성공") Result.success(Unit) } else { - Result.failure(Exception("메뉴 삭제 실패: ${response.code()} - ${response.message()}")) + val error = "메뉴 삭제 실패: ${response.code()} - ${response.message()}" + Log.e(TAG, "❌ $error") + Result.failure(Exception(error)) } } catch (e: Exception) { - Result.failure(Exception("메뉴 삭제 중 네트워크 오류: ${e.message}")) + val error = "메뉴 삭제 중 네트워크 오류: ${e.message}" + Log.e(TAG, "💥 $error", e) + Result.failure(Exception(error)) } } - - // TODO: 이미지 업로드 기능을 위한 추가 메서드들 - // 향후 Repository 인터페이스에 추가 필요: - // suspend fun addMenuWithImage(menu: Menu, base64Image: String): Result - // suspend fun updateMenuWithImage(menu: Menu, base64Image: String?): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/repository/MenuRepository.kt b/domain/src/main/java/com/example/domain/repository/MenuRepository.kt index d762337..34754a3 100644 --- a/domain/src/main/java/com/example/domain/repository/MenuRepository.kt +++ b/domain/src/main/java/com/example/domain/repository/MenuRepository.kt @@ -22,4 +22,5 @@ interface MenuRepository { suspend fun addMenu(menu: Menu, base64Image: String? = null): Result suspend fun updateMenu(menu: Menu): Result suspend fun deleteMenu(menuId: Long): Result + suspend fun updateMenuWithImage(menu: Menu, base64Image: String): Result // 추가 } \ No newline at end of file diff --git a/domain/src/main/java/com/example/domain/usecase/menu/UpdateMenuUseCase.kt b/domain/src/main/java/com/example/domain/usecase/menu/UpdateMenuUseCase.kt index 474fae7..3f95909 100644 --- a/domain/src/main/java/com/example/domain/usecase/menu/UpdateMenuUseCase.kt +++ b/domain/src/main/java/com/example/domain/usecase/menu/UpdateMenuUseCase.kt @@ -8,18 +8,60 @@ import javax.inject.Inject * 메뉴 수정 UseCase * - 메뉴 수정 시 필요한 비즈니스 로직 처리 * - 입력 데이터 검증 + * - 이미지 포함/미포함 수정 지원 */ class UpdateMenuUseCase @Inject constructor( private val menuRepository: MenuRepository ) { /** - * 메뉴를 수정 + * 메뉴를 수정 (이미지 없음) * @param menu 수정할 메뉴 객체 */ suspend fun execute(menu: Menu): Result { // 1. 입력 데이터 검증 + val validationResult = validateMenu(menu) + if (validationResult.isFailure) { + return validationResult + } + + // 2. 메뉴 수정 (이미지 없음) + return menuRepository.updateMenu(menu) + } + + /** + * 메뉴를 수정 (이미지 포함) + * @param menu 수정할 메뉴 객체 + * @param base64Image Base64로 인코딩된 이미지 데이터 + */ + suspend fun executeWithImage(menu: Menu, base64Image: String): Result { + + // 1. 입력 데이터 검증 + val validationResult = validateMenu(menu) + if (validationResult.isFailure) { + return validationResult + } + + // 2. Base64 이미지 검증 + if (base64Image.isBlank()) { + return Result.failure(IllegalArgumentException("이미지 데이터가 비어있습니다")) + } + + if (!base64Image.startsWith("data:image")) { + return Result.failure(IllegalArgumentException("올바른 이미지 형식이 아닙니다")) + } + + // 3. 메뉴 수정 (이미지 포함) + return menuRepository.updateMenuWithImage(menu, base64Image) + } + + /** + * 메뉴 데이터 검증 + * @param menu 검증할 메뉴 객체 + * @return 검증 결과 - 성공 시 원본 메뉴, 실패 시 에러 + */ + private fun validateMenu(menu: Menu): Result { if (menu.name.isBlank()) { return Result.failure(IllegalArgumentException("메뉴 이름은 필수입니다")) } @@ -36,7 +78,6 @@ class UpdateMenuUseCase @Inject constructor( return Result.failure(IllegalArgumentException("올바른 메뉴 ID가 아닙니다")) } - // 2. 메뉴 수정 - return menuRepository.updateMenu(menu) + return Result.success(menu) } } \ No newline at end of file diff --git a/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt index 37ab070..5017682 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/EditMenuScreen.kt @@ -25,6 +25,7 @@ import com.example.menu.viewmodel.MenuViewModel * - 기존 메뉴 정보로 폼 초기화 * - 메뉴 정보 수정 * - 입력 검증 + * - 이미지 수정 지원 */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -48,7 +49,7 @@ fun EditMenuScreen( var description by remember(menuToEdit) { mutableStateOf(menuToEdit?.description ?: "") } var selectedCategory by remember(menuToEdit) { mutableStateOf(menuToEdit?.categoryId ?: 0L) } var imageUrl by remember(menuToEdit) { mutableStateOf(menuToEdit?.imageUrl ?: "") } - var selectedBase64Image by remember { mutableStateOf(null) } // Base64 이미지 추가 + var selectedBase64Image by remember { mutableStateOf(null) } // 에러 상태들 var nameError by remember { mutableStateOf("") } @@ -153,14 +154,21 @@ fun EditMenuScreen( price = priceValue!!, categoryId = selectedCategory, description = description.trim(), - imageUrl = if (selectedBase64Image != null) "" else imageUrl // Base64가 있으면 URL 클리어 + imageUrl = if (selectedBase64Image != null) "" else imageUrl ) - // Base64 이미지가 있으면 포함해서 수정 요청 - // 실제로는 UpdateMenuWithImage Intent가 필요할 수 있지만 - // 현재는 UpdateMenu Intent 사용 - println("🖼️ 수정 - Base64 이미지: ${selectedBase64Image?.take(50) ?: "없음"}") - viewModel.handleIntent(MenuIntent.UpdateMenu(updatedMenu)) + // Base64 이미지 여부에 따라 다른 Intent 사용 + if (selectedBase64Image != null) { + // 이미지가 변경된 경우 - Base64 포함 수정 + println("🖼️ 수정 - 이미지 변경됨: ${selectedBase64Image!!.take(50)}...") + viewModel.handleIntent( + MenuIntent.UpdateMenuWithImage(updatedMenu, selectedBase64Image) + ) + } else { + // 이미지 변경 없는 경우 - 기존 수정 + println("🖼️ 수정 - 이미지 변경 없음") + viewModel.handleIntent(MenuIntent.UpdateMenu(updatedMenu)) + } } } ) { @@ -195,12 +203,20 @@ fun EditMenuScreen( }, categoryError = categoryError, categories = state.categories, - imageUrl = imageUrl, - onImageChange = { imageUrl = it }, - selectedBase64Image = selectedBase64Image, - onBase64ImageChange = { base64 -> - println("🖼️ 수정 - 이미지 선택됨: ${base64?.take(50) ?: "null"}") - selectedBase64Image = base64 + imageUrl = if (selectedBase64Image != null) selectedBase64Image!! else imageUrl, // Base64 우선 표시 + onImageChange = { newImageData -> + println("🖼️ 수정 - 이미지 데이터 받음: ${newImageData.take(50)}...") + + if (newImageData.startsWith("data:image")) { + // Base64 이미지인 경우 + selectedBase64Image = newImageData + println("🖼️ 수정 - Base64 저장됨") + } else { + // URL인 경우 + imageUrl = newImageData + selectedBase64Image = null // Base64 초기화 + println("🖼️ 수정 - URL 저장됨: $newImageData") + } }, modifier = Modifier.padding(paddingValues) ) @@ -226,8 +242,6 @@ private fun EditMenuContent( categories: List, imageUrl: String, onImageChange: (String) -> Unit, - selectedBase64Image: String?, // 추가 - onBase64ImageChange: (String?) -> Unit, // 추가 modifier: Modifier = Modifier ) { Column( @@ -255,22 +269,10 @@ private fun EditMenuContent( } } - // 이미지 업로드 영역 - Base64 처리 추가 + // 이미지 업로드 영역 ImageUploadArea( imageUrl = imageUrl, - onImageSelected = { base64OrUrl -> - println("🖼️ 수정 - ImageUploadArea 이미지 받음: ${base64OrUrl.take(50)}...") - - if (base64OrUrl.startsWith("data:image")) { - // Base64 이미지인 경우 - onBase64ImageChange(base64OrUrl) - println("🖼️ 수정 - Base64 데이터 저장됨") - } else { - // 일반 URL인 경우 - onImageChange(base64OrUrl) - println("🖼️ 수정 - URL 저장됨: $base64OrUrl") - } - }, + onImageSelected = onImageChange, modifier = Modifier.fillMaxWidth() ) diff --git a/feature/menu/src/main/java/com/example/menu/type/MenuIntent.kt b/feature/menu/src/main/java/com/example/menu/type/MenuIntent.kt index 9f2436e..e896c14 100644 --- a/feature/menu/src/main/java/com/example/menu/type/MenuIntent.kt +++ b/feature/menu/src/main/java/com/example/menu/type/MenuIntent.kt @@ -33,4 +33,12 @@ sealed interface MenuIntent { // UI 상태 변경 object ClearError : MenuIntent + + + // 메뉴 이미지 수정 + data class UpdateMenuWithImage( + val menu: com.example.domain.model.Menu, + val base64Image: String? + ) : MenuIntent + } \ No newline at end of file diff --git a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt index 83d8972..e0d1f97 100644 --- a/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt +++ b/feature/menu/src/main/java/com/example/menu/viewmodel/MenuViewModel.kt @@ -31,7 +31,7 @@ class MenuViewModel @Inject constructor( private val deleteMenuUseCase: DeleteMenuUseCase, private val addCategoryUseCase: AddCategoryUseCase, private val updateMenuUseCase: UpdateMenuUseCase, - private val deleteCategoryUseCase: DeleteCategoryUseCase // 쉼표 추가 + private val deleteCategoryUseCase: DeleteCategoryUseCase ) : ViewModel() { private val _state = MutableStateFlow(MenuState()) @@ -57,6 +57,7 @@ class MenuViewModel @Inject constructor( is MenuIntent.NavigateToCategoryDetail -> navigateToCategoryDetail(intent.categoryId) is MenuIntent.AddMenu -> addMenu(intent) is MenuIntent.UpdateMenu -> updateMenu(intent.menu) + is MenuIntent.UpdateMenuWithImage -> updateMenuWithImage(intent.menu, intent.base64Image) is MenuIntent.DeleteMenu -> deleteMenu(intent.menuId) is MenuIntent.AddCategory -> addCategory(intent.name) is MenuIntent.DeleteCategory -> deleteCategory(intent.categoryId) @@ -153,7 +154,9 @@ class MenuViewModel @Inject constructor( } } - // MenuViewModel.kt의 addMenu 함수 수정 + /** + * 메뉴 추가 + */ private fun addMenu(intent: MenuIntent.AddMenu) { viewModelScope.launch { println("📱 ViewModel - 메뉴 추가 요청") @@ -183,17 +186,22 @@ class MenuViewModel @Inject constructor( } /** - * 메뉴 수정 + * 메뉴 수정 (이미지 없음) */ private fun updateMenu(menu: com.example.domain.model.Menu) { viewModelScope.launch { + println("📱 ViewModel - 메뉴 수정 요청 (이미지 없음)") + println("📱 메뉴 ID: ${menu.id}") + updateMenuUseCase.execute(menu) .onSuccess { updatedMenu -> + println("✅ ViewModel - 메뉴 수정 성공: ${updatedMenu.name}") _effect.emit(MenuEffect.MenuUpdatedSuccessfully) _effect.emit(MenuEffect.ShowToast("메뉴가 수정되었습니다")) loadMenus() } .onFailure { exception -> + println("❌ ViewModel - 메뉴 수정 실패: ${exception.message}") _effect.emit(MenuEffect.ShowError( exception.message ?: "메뉴 수정 중 오류가 발생했습니다" )) @@ -201,14 +209,66 @@ class MenuViewModel @Inject constructor( } } + /** + * 메뉴 수정 (이미지 포함) + */ + private fun updateMenuWithImage(menu: com.example.domain.model.Menu, base64Image: String?) { + viewModelScope.launch { + println("📱 ViewModel - 이미지 포함 메뉴 수정 요청") + println("📱 메뉴 ID: ${menu.id}") + println("📱 base64 이미지: ${base64Image?.take(50) ?: "❌ NULL"}...") + + if (base64Image != null) { + // 실제 이미지 포함 수정 호출 + updateMenuUseCase.executeWithImage(menu, base64Image) + .onSuccess { updatedMenu -> + println("✅ ViewModel - 이미지 포함 메뉴 수정 성공: ${updatedMenu.name}") + _effect.emit(MenuEffect.MenuUpdatedSuccessfully) + _effect.emit(MenuEffect.ShowToast("메뉴가 수정되었습니다")) + loadMenus() + } + .onFailure { exception -> + println("❌ ViewModel - 이미지 포함 메뉴 수정 실패: ${exception.message}") + _effect.emit(MenuEffect.ShowError( + exception.message ?: "메뉴 수정 중 오류가 발생했습니다" + )) + } + } else { + // Base64가 null인 경우 기존 방식 사용 + println("⚠️ ViewModel - Base64 이미지가 null이므로 기존 수정 방식 사용") + updateMenuUseCase.execute(menu) + .onSuccess { updatedMenu -> + println("✅ ViewModel - 메뉴 수정 성공 (이미지 없음): ${updatedMenu.name}") + _effect.emit(MenuEffect.MenuUpdatedSuccessfully) + _effect.emit(MenuEffect.ShowToast("메뉴가 수정되었습니다")) + loadMenus() + } + .onFailure { exception -> + println("❌ ViewModel - 메뉴 수정 실패: ${exception.message}") + _effect.emit(MenuEffect.ShowError( + exception.message ?: "메뉴 수정 중 오류가 발생했습니다" + )) + } + } + } + } + + /** + * 메뉴 삭제 + */ private fun deleteMenu(menuId: Long) { viewModelScope.launch { + println("🗑️ ViewModel - 메뉴 삭제 시작: $menuId") + deleteMenuUseCase.execute(menuId) .onSuccess { + println("✅ ViewModel - 메뉴 삭제 성공") _effect.emit(MenuEffect.MenuDeletedSuccessfully) + _effect.emit(MenuEffect.ShowToast("메뉴가 삭제되었습니다")) loadMenus() } .onFailure { exception -> + println("❌ ViewModel - 메뉴 삭제 실패: ${exception.message}") _effect.emit(MenuEffect.ShowError( exception.message ?: "메뉴 삭제 중 오류가 발생했습니다" )) From 30dfec19da9a3920c7570c591ea419d48a446783 Mon Sep 17 00:00:00 2001 From: Kyu hyunSung Date: Thu, 29 May 2025 21:54:31 +0900 Subject: [PATCH 4/4] [CHORE/#25] Image Preview fixed --- .../com/example/menu/screen/AddMenuScreen.kt | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt b/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt index eda8287..7f33dd8 100644 --- a/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt +++ b/feature/menu/src/main/java/com/example/menu/screen/AddMenuScreen.kt @@ -169,12 +169,21 @@ fun AddMenuScreen( }, categoryError = categoryError, categories = state.categories, - imageUrl = imageUrl, - onImageChange = { imageUrl = it }, - selectedBase64Image = selectedBase64Image, - onBase64ImageChange = { base64 -> - println("🖼️ ImageUpload - 이미지 선택됨: ${base64?.take(50) ?: "null"}...") - selectedBase64Image = base64 + // ✅ Base64 이미지를 우선적으로 표시 + imageUrl = if (selectedBase64Image != null) selectedBase64Image!! else imageUrl, + onImageChange = { newImageData -> + println("🖼️ ImageUploadArea - 이미지 받음: ${newImageData.take(50)}...") + + if (newImageData.startsWith("data:image")) { + // Base64 이미지인 경우 + selectedBase64Image = newImageData + println("🖼️ Base64 데이터 저장됨") + } else { + // 일반 URL인 경우 + imageUrl = newImageData + selectedBase64Image = null // Base64 초기화 + println("🖼️ URL 저장됨: $newImageData") + } }, modifier = Modifier.padding(paddingValues) ) @@ -200,8 +209,6 @@ private fun AddMenuContent( categories: List, imageUrl: String, onImageChange: (String) -> Unit, - selectedBase64Image: String?, - onBase64ImageChange: (String?) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -211,22 +218,10 @@ private fun AddMenuContent( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - // 이미지 업로드 영역 + // 이미지 업로드 영역 - Base64 이미지도 표시 가능 ImageUploadArea( - imageUrl = imageUrl, - onImageSelected = { base64OrUrl -> - println("🖼️ ImageUploadArea - 이미지 받음: ${base64OrUrl.take(50)}...") - - if (base64OrUrl.startsWith("data:image")) { - // Base64 이미지인 경우 - onBase64ImageChange(base64OrUrl) // base64 데이터 저장 - println("🖼️ Base64 데이터 저장됨") - } else { - // 일반 URL인 경우 - onImageChange(base64OrUrl) // URL 저장 - println("🖼️ URL 저장됨: $base64OrUrl") - } - }, + imageUrl = imageUrl, // Base64 또는 URL + onImageSelected = onImageChange, modifier = Modifier.fillMaxWidth() )