플러터 게시판에서 멀티이미지 업데이트 구현하기: 심층 가이드

2024. 6. 20. 14:41Flutter/Flutter Programming

반응형

안녕하세요! 오늘은 플러터 게시판에서 멀티이미지 업데이트 기능을 구현하는 방법에 대해 자세히 알아보겠습니다.

이 글에서는 기존 이미지 삭제, 새로운 이미지 추가, 저장, 게시글 업데이트 과정까지 단계별로 안내하며, 코드 예시와 함께 구현 방법을 자세히 설명합니다.

 

1. 기존 이미지 삭제

1.1 삭제 버튼 클릭 이벤트

 

게시판 화면에서 각 이미지 옆에 삭제 버튼을 배치하고, 해당 버튼 클릭 시 이벤트 처리 함수를 호출하도록 설정합니다.

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('게시글 수정'),
    ),
    body: ListView.builder(
      itemCount: _imageList.length,
      itemBuilder: (context, index) {
        final imageUrl = _imageList[index];
        return Row(
          children: [
            Image.network(imageUrl, width: 100, height: 100),
            IconButton(
              onPressed: () => _deleteImage(imageUrl),
              icon: Icon(Icons.delete),
            ),
          ],
        );
      },
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _pickImage,
      child: Icon(Icons.add_a_photo),
    ),
  );
}
 

1.2 URL 파싱

클릭된 이미지의 URL을 파싱하여 스토리지에서 삭제할 파일 이름을 추출합니다.

URL은 일반적으로 다음과 같은 형식입니다.

https://firebasestorage.googleapis.com/v0/b/PROJECT_ID/o/posts/POST_ID/IMAGE_NAME.jpg

위 URL에서 IMAGE_NAME.jpg 부분만 추출하여 파일 이름으로 사용합니다.

Future<void> _deleteImage(String imageUrl) async {
  // URL 파싱
  final fileName = imageUrl.split('/').last;

  // 스토리지 삭제
  final storageRef = FirebaseStorage.instance.refFromURL(imageUrl);
  await storageRef.delete();

  // 리스트 업데이트
  setState(() {
    _imageList.remove(imageUrl);
  });
}
 

1.3 스토리지 삭제

 

firebase_storage 플러터 패키지를 사용하여 추출된 파일 이름을 기반으로 파이어베이스 스토리지에서 해당 이미지를 삭제합니다.

Future<void> _deleteImage(String imageUrl) async {
  // ...

  // 스토리지 삭제
  final storageRef = FirebaseStorage.instance.refFromURL(imageUrl);
  await storageRef.delete();

  // ...
}
 

1.4 리스트 업데이트

 

삭제된 이미지 URL을 포함하는 리스트 항목을 리스트에서 제거하고 화면을 업데이트합니다.

Future<void> _deleteImage(String imageUrl) async {
  // ...

  // 리스트 업데이트
  setState(() {
    _imageList.remove(imageUrl);
  });
}
 

2. 새로운 이미지 추가

2.1 이미지 피커 사용

 

image_picker 플러터 패키지를 사용하여 이미지 피커를 호출하고 사용자가 선택한 이미지들을 받습니다. 이미지 피커는 사용자에게 사진 앨범 또는 카메라 액세스 권한을 요청합니다.

Future<void> _pickImage() async {
  final pickedImage = await ImagePicker().pickMultipleImages(maxImages: 5);
  if (pickedImage != null) {
    for (final image in pickedImage) {
      final file = await image.readAsFile();
      if (file != null) {
        // 스토리지에 이미지 저장
        final storageRef = FirebaseStorage.instance.ref().child('posts/${widget.post.id}/${file.path.split('/').last}');
        final uploadTask = storageRef.putFile(file);
        final imageUrl = await uploadTask.then((snapshot) => snapshot.ref.getDownloadURL());

        // 리스트 업데이트
        setState(() {
          _imageList.add(imageUrl);
        });
      }
    }
  }
}
 

2.2 스토리지 저장

선택된 이미지들을 파이어베이스 스토리지에 저장합니다. 각 이미지는 게시글 ID와 고유한 이름으로 저장됩니다.

Future<void> _pickImage() async {
  // ...

  for (final image in pickedImage) {
    // ...

    // 스토리지에 이미지 저장
    final storageRef = FirebaseStorage.instance.ref().child('posts/${widget.post.id}/${file.path.split('/').last}');
    final uploadTask = storageRef.putFile(file);
    final imageUrl = await uploadTask.then((snapshot) => snapshot.ref.getDownloadURL());

    // ...
  }
}
 

2.3 URL 추출

 

저장된 이미지들의 파이어베이스 스토리지 URL을 추출합니다. 이 URL들은 나중에 게시글 정보 업데이트에 사용됩니다.

Future<void> _pickImage() async {
  // ...

  for (final image in pickedImage) {
    // ...

    // 스토리지에 이미지 저장
    final storageRef = FirebaseStorage.instance.ref().child('posts/${widget.post.id}/${file.path.split('/').last}');
    final uploadTask = storageRef.putFile(file);
    final imageUrl = await uploadTask.then((snapshot) => snapshot.ref.getDownloadURL());

    // URL 추출
    final imageUrlList = _imageList;
    imageUrlList.add(imageUrl);

    // 리스트 업데이트
    setState(() {
      _imageList = imageUrlList;
    });
  }
}
 

2.4 리스트 업데이트

 

추출된 이미지 URL들을 새로운 리스트 항목으로 추가하고 화면을 업데이트합니다.

Future<void> _pickImage() async {
  // ...

  for (final image in pickedImage) {
    // ...

    // URL 추출
    final imageUrlList = _imageList;
    imageUrlList.add(imageUrl);

    // 리스트 업데이트
    setState(() {
      _imageList = imageUrlList;
    });
  }
}
 

3. 저장 및 게시글 업데이트

3.1 저장 버튼 클릭 이벤트

 

게시판 화면에 저장 버튼을 배치하고, 해당 버튼 클릭 시 이벤트 처리 함수를 호출하도록 설정합니다.

Widget build(BuildContext context) {
  // ...

  floatingActionButton: FloatingActionButton(
    onPressed: _savePost,
    child: Icon(Icons.save),
  ),

  // ...
}
 

3.2 게시글 정보 업데이트

 

저장 버튼 클릭 이벤트 처리 함수에서 게시글 제목, 내용, 이미지 URL 리스트를 업데이트합니다.

Future<void> _savePost() async {
  if (_formKey.currentState!.validate()) {
    // 게시글 업데이트
    final updatedPost = Post(
      id: widget.post.id,
      title: _titleController.text,
      content: _contentController.text,
      imageUrlList: _imageList,
    );

    // 게시글 데이터베이스 업데이트
    // ...

    // 화면 종료
    Navigator.pop(context);
  }
}
 

3.3 게시글 데이터베이스 업데이트

 

사용한 데이터베이스에 따라 구체적인 업데이트 방법은 다르지만, 일반적으로 다음과 같은 과정을 거칩니다.

  1. 데이터베이스 연결: 사용할 데이터베이스에 연결합니다.
  2. 게시글 정보 업데이트: 업데이트된 게시글 정보를 데이터베이스에 저장합니다.
  3. 데이터베이스 커밋: 변경 사항을 데이터베이스에 반영합니다.

예시: Firebase Firestore 사용 시

Future<void> _savePost() async {
  // ...

  // 게시글 데이터베이스 업데이트
  final docRef = FirebaseFirestore.instance.collection('posts').doc(widget.post.id);
  await docRef.update({
    'title': _titleController.text,
    'content': _contentController.text,
    'imageUrlList': _imageList,
  });

  // ...
}
 

4. 화면 종료

 

게시글 업데이트가 완료되면 화면을 종료하여 이전 화면으로 돌아갑니다.

Future<void> _savePost() async {
  // ...

  // 화면 종료
  Navigator.pop(context);
}
 

5. 전체 코드 예시

5.1. 필요한 패키지 설치

먼저, 프로젝트에 다음과 같은 패키지를 설치해야 합니다.

flutter pub add image_picker
flutter pub add firebase_storage
flutter pub add cloud_firestore

 

5.2. 모델 정의

게시글 데이터를 저장할 모델 클래스를 정의합니다.

class Post {
  final String id; // 게시글 ID
  final String title; // 게시글 제목
  final String content; // 게시글 내용
  final List<String> imageUrlList; // 이미지 URL 리스트

  Post({required this.id, required this.title, required this.content, required this.imageUrlList});
}
 

5.3. UI 코드

게시글 수정 화면의 UI 코드를 작성합니다. 이 코드에서는 기존 이미지 삭제, 새로운 이미지 추가, 저장 버튼 기능을 구현합니다.

 

 

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class Post {
  final String id; // 게시글 ID
  final String title; // 게시글 제목
  final String content; // 게시글 내용
  final List<String> imageUrlList; // 이미지 URL 리스트

  Post({required this.id, required this.title, required this.content, required this.imageUrlList});
}

class PostPage extends StatefulWidget {
  final Post post;

  const PostPage({Key? key, required this.post}) : super(key: key);

  @override
  _PostPageState createState() => _PostPageState();
}

class _PostPageState extends State<PostPage> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController(text: widget.post.title);
  final _contentController = TextEditingController(text: widget.post.content);
  final _imageList = <String>[];

  @override
  void initState() {
    super.initState();
    _imageList.addAll(widget.post.imageUrlList);
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  Future<void> _deleteImage(String imageUrl) async {
    // 스토리지에서 이미지 삭제
    final storageRef = FirebaseStorage.instance.refFromURL(imageUrl);
    await storageRef.delete();

    // 리스트 업데이트
    setState(() {
      _imageList.remove(imageUrl);
    });
  }

  Future<void> _pickImage() async {
    final pickedImage = await ImagePicker().pickMultipleImages(maxImages: 5);
    if (pickedImage != null) {
      for (final image in pickedImage) {
        final file = await image.readAsFile();
        if (file != null) {
          // 스토리지에 이미지 저장
          final storageRef = FirebaseStorage.instance.ref().child('posts/<span class="math-inline">\{widget\.post\.id\}/</span>{file.path.split('/').last}');
          final uploadTask = storageRef.putFile(file);
          final imageUrl = await uploadTask.then((snapshot) => snapshot.ref.getDownloadURL());

          // 리스트 업데이트
          setState(() {
            _imageList.add(imageUrl);
          });
        }
      }
    }
  }

  Future<void> _savePost() async {
    if (_formKey.currentState!.validate()) {
      // 게시글 업데이트
      final updatedPost = Post(
        id: widget.post.id,
        title: _titleController.text,
        content: _contentController.text,
        imageUrlList: _imageList,
      );

      // 게시글 데이터베이스 업데이트
      final docRef = FirebaseFirestore.instance.collection('posts').doc(updatedPost.id);
      await docRef.update({
        'title': updatedPost.title,
        'content': updatedPost.content,
        'imageUrlList': updatedPost.imageUrlList,
      });

      // 화면 종료
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('게시글 수정'),
      ),
      body: Form(
        key: _formKey,
        child: ListView(
          children: [
            TextFormField(
              controller: _titleController,
              decoration: InputDecoration(labelText: '제목'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '제목을 입력해주세요.';
                }
                return null;
              },
            ),
            TextFormField(
             controller: _contentController,
             maxLines: 10,
             decoration: InputDecoration(labelText: '내용'),
             validator: (value) {
               if (value == null || value.isEmpty) {
                  return '내용을 입력해주세요.';
                }
                return null;
              },
             ),
            GridView.count(
              shrinkWrap: true,
              crossAxisCount: 3,
              children: [
                for (final imageUrl in _imageList)
                  Stack(
                    children: [
                      Image.network(imageUrl, height: 100, fit: BoxFit.cover),
                      Positioned(
                        top: 10,
                        right: 10,
                        child: IconButton(
                          onPressed: () => _deleteImage(imageUrl),
                          icon: Icon(Icons.delete),
                          color: Colors.red,
                        ),
                      ),
                    ],
                  ),
                InkWell(
                  onTap: _pickImage,
                  child: Container(
                    height: 100,
                    width: 100,
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: Center(
                      child: Icon(Icons.add_a_photo),
                    ),
                  ),
                ),
              ],
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _savePost,
              child: Text('저장'),
            ),
          ],
        ),
      ),
    );
  }
}
 
 

설명

  • GridView.count: 이미지를 그리드 형식으로 표시합니다.
    • shrinkWrap: true로 설정하면 GridView가 콘텐츠에 맞게 높이를 조정합니다.
    • crossAxisCount: 3으로 설정하면 이미지가 3열로 표시됩니다.
    • children: _imageList에 있는 각 이미지 URL에 대해 다음과 같은 위젯을 생성합니다.
      • Stack: 이미지 위에 삭제 버튼을 배치합니다.
        • Image.network: 이미지 URL을 사용하여 이미지를 표시합니다.
        • Positioned: 삭제 버튼을 이미지의 오른쪽 상단에 배치합니다.
          • IconButton: 삭제 버튼을 만듭니다.
            • onPressed: _deleteImage 함수를 호출하여 이미지를 삭제합니다.
            • icon: 삭제 아이콘을 표시합니다.
            • color: 아이콘 색상을 빨간색으로 설정합니다.
      • InkWell: 새로운 이미지를 추가하기 위한 영역을 만듭니다.
        • onTap: _pickImage 함수를 호출하여 이미지를 선택합니다.
        • child: Container 위젯을 사용하여 추가 영역을 표시합니다.
          • decoration: 추가 영역의 테두리와 모서리를 설정합니다.
          • child: Center 위젯을 사용하여 추가 아이콘을 가운데 배치합니다.
            • Icon: 추가 아이콘을 표시합니다.
  • ElevatedButton: 게시글을 저장하는 버튼을 만듭니다.
    • onPressed: _savePost 함수를 호출하여 게시글을 저장합니다.
    • child: 버튼에 "저장" 텍스트를 표시합니다.

이 코드를 사용하면 기존 이미지를 삭제하고 새로운 이미지를 추가할 수 있으며, 게시글 제목과 내용을 편집하여 게시글을 수정할 수 있습니다.

추가 기능

  • 게시글 작성 화면에서 멀티이미지 업데이트 기능을 구현하려면 위 코드를 수정하여 새로운 이미지 추가 기능을 게시글 작성 화면에 추가해야 합니다.
  • 이미지 편집 기능을 추가하려면 image_editor 플러터 패키지를 사용할 수 있습니다.
  • 댓글 기능을 추가하려면 cloud_firestore 플러터 패키지를 사용하여 댓글 데이터를 저장하고 관리할 수 있습니다.

 

수발가족을 위한 일기장 “나비일기장

 

https://play.google.com/store/apps/details?id=com.maccrey.navi_diary_release

 

구글플레이 앱 배포의 시작! 비공개테스트 20명의 테스터모집을 위한 앱 "테스터 쉐어"

 

https://play.google.com/store/apps/details?id=com.maccrey.tester_share_release

 

Tester Share [테스터쉐어] - Google Play 앱

Tester Share로 Google Play 앱 등록을 단순화하세요.

play.google.com

 

 

카카오톡 오픈 채팅방

https://open.kakao.com/o/gsS8Jbzg

 

 

반응형