Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feature] image upload #81

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}

include ":app"
23 changes: 15 additions & 8 deletions lib/common/widgets/profile_card.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:cogo/common/widgets/atoms/texts/texts.dart';
import 'package:cogo/common/widgets/tag_list.dart';
import 'package:flutter/material.dart';
import 'package:cogo/constants/constants.dart';
import 'package:flutter/material.dart';

class ProfileCard extends StatelessWidget {
final String picture;
Expand Down Expand Up @@ -58,13 +58,20 @@ class ProfileCard extends StatelessWidget {
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
child: Image.asset(
picture.isNotEmpty ? picture : 'assets/default_img.png',
width: double.infinity,
height: 150,
fit: BoxFit.cover,
),
),
child: Image.network(
picture.isNotEmpty ? picture : '', // 빈 문자열로 설정해 에러를 유도
width: double.infinity,
height: 150,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/default_img.png', // 로컬 기본 이미지
width: double.infinity,
height: 150,
fit: BoxFit.cover,
);
},
)),
Positioned(
top: 15,
left: 15,
Expand Down
2 changes: 2 additions & 0 deletions lib/constants/paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ abstract class Paths {

static const String report = '/report';
static const String reportDetail = '/report_detail';

static const String image = '/image';
}
3 changes: 3 additions & 0 deletions lib/data/di/get_it_locator.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:cogo/data/service/mentor_service.dart';
import 'package:cogo/data/service/refresh_service.dart';
import 'package:cogo/data/service/s3_service.dart';
import 'package:cogo/data/service/user_service.dart';
import 'package:cogo/features/auth/login/login_view_model.dart';
import 'package:cogo/features/auth/signup/club/club_selection_view_model.dart';
Expand Down Expand Up @@ -27,6 +28,8 @@ void setupServiceLocator() {


///뷰모델 등록
getIt.registerLazySingleton<S3Service>(() => S3Service());

getIt.registerFactory<LoginViewModel>(() => LoginViewModel());

getIt.registerFactory<PhoneNumberViewModel>(
Expand Down
14 changes: 14 additions & 0 deletions lib/data/dto/response/image_save_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'image_save_response.freezed.dart';
part 'image_save_response.g.dart';

@freezed
class ImageSaveResponse with _$ImageSaveResponse {
const factory ImageSaveResponse({
String? savedUrl,
}) = _ImageSaveResponse;

factory ImageSaveResponse.fromJson(Map<String, dynamic> json) =>
_$ImageSaveResponseFromJson(json);
}
51 changes: 51 additions & 0 deletions lib/data/service/s3_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:cogo/constants/apis.dart';
import 'package:cogo/data/di/api_client.dart';
import 'package:cogo/data/dto/response/base_response.dart';
import 'package:cogo/data/dto/response/image_save_response.dart';
import 'package:dio/dio.dart';

class S3Service {
final ApiClient _apiClient = ApiClient();
static const apiVersion = "api/v2/";

Future<String?> uploadImage(String imagePath) async {
try {
const url = '$apiVersion${Apis.s3}/v2';

// FormData 생성
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(imagePath,
filename: imagePath.split('/').last)
});

// 요청 전송
final response = await _apiClient.dio.post(
url,
data: formData,
options: Options(
headers: {
'accept': '*/*',
'Content-Type': 'multipart/form-data',
},
),
);

// 상태 코드 확인
if (response.statusCode != 201) {
throw Exception('업로드 실패: 상태 코드 = ${response.statusCode}');
}

final baseResponse = BaseResponse<ImageSaveResponse>.fromJson(
response.data,
(contentJson) => ImageSaveResponse.fromJson(contentJson),
);
return baseResponse.content.savedUrl;
} on DioException catch (e) {
// Dio 특화된 에러 처리
throw Exception('업로드 중 오류 발생: ${e.response?.data ?? e.message}');
} catch (e) {
// 기타 예외 처리
throw Exception('업로드 중 오류 발생: ${e.toString()}');
}
}
}
30 changes: 30 additions & 0 deletions lib/data/service/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,34 @@ class UserService {
throw Exception('An unexpected error occurred: $e');
}
}

///PUT /api/v2/users/picture 이미지 저장하기
Future<bool> saveImage(String imageUrl) async {
try {
final response =
await _apiClient.dio.put(apiVersion + Apis.saveImage, data: imageUrl);

//todo 여기 response가 존재하나 필요가 없어서 안받음
// {
// "statusCode": "201",
// "message": "CREATED",
// "content": {
// "username": "113343694546635833713",
// "name": "222",
// "email": "objet917@gmail.com",
// "role": "ROLE_MENTOR",
// "phoneNum": "123-1231-2312",
// "picture": "\"https://cogo-bucket.s3.ap-northeast-2.amazonaws.com/v2/113343694546635833713\""
// }
// }
if (response.statusCode != 201) {
throw Exception('Failed to send verification code ${response.data}');
}
return true;
} on DioException catch (e) {
throw Exception('Error: ${e.response?.data ?? e.message}');
} catch (e) {
throw Exception('An unexpected error occurred: $e');
}
}
}
30 changes: 20 additions & 10 deletions lib/features/home/profile/profile_detail_screen.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:developer';

import 'package:cogo/common/widgets/atoms/texts/styles.dart';
import 'package:cogo/common/widgets/components/basic_button.dart';
import 'package:cogo/constants/colors.dart';
Expand Down Expand Up @@ -85,20 +87,28 @@ class ProfileDetailScreen extends StatelessWidget {
}

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
child: Image.asset(
profile.imageUrl.isNotEmpty
? profile.imageUrl
: 'assets/default_img.png',
width: double.infinity,
height: 150,
fit: BoxFit.cover,
),
),
child: Image.network(
profile.imageUrl.isNotEmpty
? profile.imageUrl
: '', // 빈 문자열로 설정해 에러를 유도
width: double.infinity,
height: 150,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
log("===이미지 에러===");
return Image.asset(
'assets/default_img.png', // 로컬 기본 이미지
width: double.infinity,
height: 150,
fit: BoxFit.cover,
);
},
)),
const SizedBox(height: 30),
Center(
child: Row(
Expand Down
101 changes: 101 additions & 0 deletions lib/features/mypage/image_upload_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'dart:io';

import 'package:cogo/data/service/s3_service.dart';
import 'package:cogo/data/service/user_service.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:image_picker/image_picker.dart';

class ImageUploadViewModel extends ChangeNotifier {
final S3Service s3service = GetIt.instance<S3Service>();
final UserService userService = GetIt.instance<UserService>();

File? _selectedImage;
bool _isUploading = false;
bool _isUpload = false;
String? _uploadResult;
String? _errorMessage;

// 생성자
ImageUploadViewModel();

// Getters
File? get selectedImage => _selectedImage;
bool get isUploading => _isUploading;

bool get isUpload => _isUpload;
String? get uploadResult => _uploadResult;
String? get errorMessage => _errorMessage;

// 갤러리에서 이미지 선택
Future<void> pickImageFromGallery() async {
try {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);

if (pickedFile != null) {
_selectedImage = File(pickedFile.path);
_errorMessage = null;
notifyListeners();
}
} catch (e) {
_errorMessage = '이미지 선택 중 오류 발생: ${e.toString()}';
notifyListeners();
}
}

// 카메라로 이미지 촬영
Future<void> takeImageFromCamera() async {
try {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.camera);

if (pickedFile != null) {
_selectedImage = File(pickedFile.path);
_errorMessage = null;
notifyListeners();
}
} catch (e) {
_errorMessage = '카메라 촬영 중 오류 발생: ${e.toString()}';
notifyListeners();
}
}

// 이미지 업로드
Future<void> uploadImage() async {
// 이미지가 선택되지 않은 경우
if (_selectedImage == null) {
_errorMessage = '먼저 이미지를 선택해주세요.';
notifyListeners();
return;
}

try {
// 업로드 시작
_isUploading = true;
_errorMessage = null;
notifyListeners();

// 서비스를 통해 이미지 업로드
_uploadResult = await s3service.uploadImage(_selectedImage!.path);

// 업로드 성공
_isUpload = await userService.saveImage(_uploadResult!);
_isUploading = false;
notifyListeners();
} catch (e) {
// 에러 처리
_errorMessage = e.toString();
_isUploading = false;
notifyListeners();
}
}

// 선택된 이미지 초기화
void clearSelectedImage() {
_selectedImage = null;
_uploadResult = null;
_errorMessage = null;
notifyListeners();
}
}
72 changes: 72 additions & 0 deletions lib/features/mypage/image_uplooad_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'image_upload_view_model.dart';

class ImageUploadScreen extends StatelessWidget {
const ImageUploadScreen({super.key});

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ImageUploadViewModel(),
child: Scaffold(
appBar: AppBar(title: Text("Image Uploader")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Consumer<ImageUploadViewModel>(
builder: (context, viewModel, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (viewModel.selectedImage != null)
Image.file(
viewModel.selectedImage!,
height: 200,
width: 200,
fit: BoxFit.cover,
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: viewModel.pickImageFromGallery,
child: const Text("갤러리")),
ElevatedButton(
onPressed: viewModel.takeImageFromCamera,
child: const Text("카메라")),
],
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async => {
log("사진을 바꿔요"),
viewModel.uploadImage(),
if (viewModel.isUpload) {Navigator.of(context).pop()}
},
child: viewModel.isUploading
? const CircularProgressIndicator(color: Colors.white)
: const Text("저장하기"),
),
if (viewModel.errorMessage != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
viewModel.errorMessage!,
style: TextStyle(color: Colors.red),
),
),
],
),
);
},
),
),
),
);
}
}
Loading