Flutter 첨부파일

이동언·2025년 3월 5일

1. 기초설정

1-1. yml 파일추가

image_picker라는 라이브러리 추가하기.

1-2. android 설정

android/app/main/manifest.xml 파일 내부에 두줄추가

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

1-3. ios 설정

ios/Runner/info.plist 파일 내부에 해당 문장추가

    <key>NSCameraUsageDescription</key>
    <string>We need your permission to use the camera</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>We need your permission to access the photo library</string>


2. fileUploadUtil

direct로 fileupload를 사용해도 되지만 본인은 docker의 niginx 안에 넣기 때문에 post하는 url이 다르기도하고, 재사용성을 위해 util로 빼서 만듬.

import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class FileUploadUtil {
  static Future<void> uploadFile({
    required BuildContext context, // build 할때 상태
    required List<File> images, // 두개이상의 이미지를 선택하기 위해 List를 선택
    required String uri, // post를 할 uri
}) async {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse(uri)
    );

    for (var image in images) { // 파일전송할때, body내부 form-data를 이용해서 key-value로 전송을 하도록 구조했었음
      var imageFile = await http.MultipartFile.fromPath(
        'files', // key값
        image.path, // value값은 이미지경로
      );
      request.files.add(imageFile);
    }

    var response = await request.send();

    if (response.statusCode == 200) { // 파일 업로드가 정상적인지 확인 가능함.
      var responseData = await http.Response.fromStream(response);
      var fileUrl = responseData.body;
      print('File upload Successful');

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Upload successful: $fileUrl')),
      );
    } else {
      print('File upload failed');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('File upload failed')),
      );
    }
  }
}

3. util 사용

본인은 소셜로그인이후 개인정보추가입력창에서 증명사진 및 서류등록에 사용을 했음.

import 'dart:io';
import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:gooinpro_parttimer/services/api/loginapi/login_api.dart';
import 'package:gooinpro_parttimer/utils/file_upload_util.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../models/login/login_register_model.dart';
import '../../models/login/login_response_model.dart';
import '../../providers/user_provider.dart';

class RegisterPage extends StatefulWidget {
  
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  late LoginRegister registerData;
  late TextEditingController _birthController;
  List<File> _imagesProfile = []; // 증명사진 넣는 file
  List<File> _imagesDocument = []; // 추가 서류 넣는 file
  final picker = ImagePicker(); // imagepicker 정의
  final String baseUrl = dotenv.env['API_UPLOAD_LOCAL_HOST'] ?? 'No API host found';

  
  void initState() {
    super.initState();
    _birthController = TextEditingController();
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    var userProvider = context.read<UserProvider>();

    registerData = LoginRegister(
      pemail: userProvider.pemail ?? '',
      pname: userProvider.pname ?? '',
      pbirth: DateTime.now(),
      pgender: true,
      proadAddress: '',
      pdetailAddress: '',
    );

    _birthController.text = formatDate(registerData.pbirth);
  }

  
  void dispose() {
    _birthController.dispose();
    super.dispose();
  }

  String formatDate(DateTime date) {
    return DateFormat('yyyy-MM-dd').format(date);
  }

  Future<void> _pickProfileImage() async { // 증명사진 넣는 메소드를 따로 빼서 에뮬레이터의 사진첩에서 선택하는것
    final pickedFile = await picker.pickMultiImage();
    if (pickedFile != null) {
      setState(() {
        _imagesProfile = pickedFile.map((pickedFile) => File(pickedFile.path)).toList(); // 선택한 이미지의 위치 파악
      });
    }
  }

  Future<void> _pickDocumentImage() async { // 추가서류 넣는 메소드를 따로 빼고
    final pickedFile = await picker.pickMultiImage();
    if (pickedFile != null) {
      setState(() {
        _imagesDocument = pickedFile.map((pickedFile) => File(pickedFile.path)).toList();
      });
    }
  }


  Future<void> uploadFiles(BuildContext context, List<File> profileImages, List<File> documentImages) async { // 만들어놨던 util의 메소드를 사용
    if (_imagesProfile == null){
      return;
    }
    // FileUploadUtil.uploadFile(context: context, images: _imagesProfile!, uri: '$baseUrl/upload/api/partTimer/document'); 안드로이드 용
    // FileUploadUtil.uploadFile(context: context, images: _imagesDocument!, uri: '$baseUrl/upload/api/partTimer/profile'); 안드로이드 용
     FileUploadUtil.uploadFile(context: context, images: _imagesProfile!, uri: 'http://localhost:8085/upload/api/partTimer/document');
     FileUploadUtil.uploadFile(context: context, images: _imagesDocument!, uri: 'http://localhost:8085/upload/api/partTimer/profile');
  }

  void _onClickSend() async {
    login_api api = login_api();
    print("이메일: ${registerData.pemail}");
    LoginResponse response = await api.registerUser(registerData);
    print(response.accessToken);
    uploadFiles(context, _imagesProfile, _imagesDocument); // 클릭시 모든 메소드 실행되도록
    context.go('/jobposting');
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('회원가입')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Consumer<UserProvider>(
          builder: (context, userNotifier, child) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 이메일
                Row(
                  children: [
                    Text('이메일: ', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        userNotifier.pemail ?? '',
                        style: TextStyle(fontSize: 18),
                        overflow: TextOverflow.ellipsis, // 텍스트 넘칠 때 처리
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 16),

                // 이름
                Row(
                  children: [
                    Text('이름: ', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        userNotifier.pname ?? '',
                        style: TextStyle(fontSize: 18),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 16),

                // 생년월일
                Row(
                  children: [
                    Text('생년월일: ', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    Expanded(
                      child: TextFormField(
                        controller: _birthController,
                        readOnly: true,
                        decoration: InputDecoration(
                          border: OutlineInputBorder(),
                          suffixIcon: Icon(Icons.calendar_today),
                        ),
                        onTap: () async {
                          DateTime? pickedDate = await showDatePicker(
                            context: context,
                            initialDate: registerData.pbirth,
                            firstDate: DateTime(1900),
                            lastDate: DateTime.now(),
                          );
                          if (pickedDate != null) {
                            setState(() {
                              registerData.pbirth = pickedDate;
                              _birthController.text = formatDate(pickedDate);
                            });
                          }
                        },
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 16),

                // 성별 선택
                Row(
                  children: [
                    Text('성별: ', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    DropdownButton<bool>(
                      value: registerData.pgender,
                      onChanged: (value) {
                        setState(() {
                          registerData.pgender = value!;
                        });
                      },
                      items: [
                        DropdownMenuItem(value: true, child: Text('남')),
                        DropdownMenuItem(value: false, child: Text('여')),
                      ],
                    ),
                  ],
                ),
                SizedBox(height: 16),

                // 주소입력
                Row(
                  children: [
                    Text('주소입력: ', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    Expanded(
                      child: TextField(
                        decoration: InputDecoration(
                          border: OutlineInputBorder(),
                          hintText: '주소를 입력하세요',
                        ),
                        onChanged: (value) {
                          setState(() {
                            registerData.proadAddress = value;
                          });
                        },
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 16),

                // 증명사진
                Row(
                  children: [
                    Text('증명사진', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    _imagesProfile.isEmpty
                        ? Text('no image')
                        : Container(
                      width: 60, // 고정된 크기로 설정
                      height: 60,
                      decoration: BoxDecoration( // 선택한 이미지 미리보기식으로 하나 나옴
                        image: DecorationImage(
                          image: FileImage(_imagesProfile[0]),
                          fit: BoxFit.cover,
                        ),
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    SizedBox(width: 16),
                    ElevatedButton(
                        onPressed: _pickProfileImage,
                        child: Text('사진 선택')),
                    SizedBox(width: 8),
                  ],
                ),
                SizedBox(height: 16),

                // 증명사진
                Row(
                  children: [
                    Text('보건증', style: TextStyle(fontSize: 18)),
                    SizedBox(width: 8),
                    _imagesDocument.isEmpty
                        ? Text('no image')
                        : Container(
                      width: 60, // 고정된 크기로 설정
                      height: 60,
                      decoration: BoxDecoration(
                        image: DecorationImage(
                          image: FileImage(_imagesDocument[0]),
                          fit: BoxFit.cover,
                        ),
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    SizedBox(width: 16),
                    ElevatedButton(
                        onPressed: _pickDocumentImage,
                        child: Text('사진 선택')),
                    SizedBox(width: 8),
                  ],
                ),
                SizedBox(height: 16),

                // 확인 버튼
                Center(
                  child: ElevatedButton(
                    onPressed: _onClickSend,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0),
                      child: Text('확인', style: TextStyle(fontSize: 18)),

                    ),
                  ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

0개의 댓글