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 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/constants/paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ abstract class Paths {
static const String myMentorIntroduce = '/my_mentor_introduce';
static const String timeSetting = '/time_setting';
static const String timeChecking = '/time_checking';

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 All @@ -25,6 +26,8 @@ void setupServiceLocator() {

getIt.registerLazySingleton<RefreshService>(() => RefreshService());

getIt.registerLazySingleton<S3Service>(() => S3Service());

/**
* 뷰모델 등록
*/
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);
}
47 changes: 47 additions & 0 deletions lib/data/service/s3_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:cogo/constants/apis.dart';
import 'package:cogo/data/di/api_client.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}');
}

// 업로드 성공
return response.data.toString();
} 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');
}
}
}
103 changes: 103 additions & 0 deletions lib/features/mypage/image_upload_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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);

//todo 이미지 업로드 api

// 업로드 성공
_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();
}
}
55 changes: 55 additions & 0 deletions lib/features/mypage/image_uplooad_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'image_upload_view_model.dart';

class ImageUploadScreen extends StatelessWidget {
@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 Column(
children: [
if (viewModel.selectedImage != null)
Image.file(
viewModel.selectedImage!,
height: 200,
width: 200,
fit: BoxFit.cover,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: viewModel.pickImageFromGallery,
child: Text("Pick Image from Gallery"),
),
SizedBox(height: 20),
ElevatedButton(
onPressed:
viewModel.isUploading ? null : viewModel.uploadImage,
child: viewModel.isUploading
? CircularProgressIndicator(color: Colors.white)
: Text("Upload to S3"),
),
if (viewModel.errorMessage != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
viewModel.errorMessage!,
style: TextStyle(color: Colors.red),
),
),
],
);
},
),
),
),
);
}
}
5 changes: 5 additions & 0 deletions lib/features/mypage/mypage_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ class MypageScreen extends StatelessWidget {
SvgPicture.asset('assets/image/img_image.svg'),
// 기본 로컬 이미지
],
BasicButton(
text: "프로필 사진 변경",
isClickable: true,
onPressed: () => {context.push(Paths.image)},
),
const SizedBox(height: 13),
Center(
child: TagList(tags: user!.tags),
Expand Down
10 changes: 9 additions & 1 deletion lib/route/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:cogo/features/home/mentor_detail/views/mentor_question1_screen.d
import 'package:cogo/features/home/mentor_detail/views/mentor_question2_screen.dart';
import 'package:cogo/features/home/profile/profile_detail_screen.dart';
import 'package:cogo/features/home/search/search_screen.dart';
import 'package:cogo/features/mypage/image_uplooad_screen.dart';
import 'package:cogo/features/mypage/mentor_introduce/my_mentor_introduce_screen.dart';
import 'package:cogo/features/mypage/mentor_time_checking/mentor_time_checking_screen.dart';
import 'package:cogo/features/mypage/mentor_time_setting/mentor_time_setting_screen.dart';
Expand Down Expand Up @@ -242,6 +243,13 @@ final AppRouter = GoRouter(
child: const MentorTimeCheckingScreen(),
),
),
GoRoute(
path: Paths.image,
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: ImageUploadScreen(),
),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNestedNavigation(
Expand Down Expand Up @@ -269,4 +277,4 @@ StatefulShellBranch _createBranch(String path, Widget child) {
),
],
);
}
}
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies:

freezed_annotation: ^2.0.3

image_picker: ^0.8.7+5


dev_dependencies:
flutter_test:
Expand Down