반응형
플러터로 앱만들기 회원가입 및 사진올리기 기능
- 회원가입 화면: 이메일과 비밀번호를 입력하고 '가입하기' 버튼을 누르면 메인 화면으로 이동합니다. (실제 서버에 저장되지는 않는 UI 데모입니다.)
- 메인 화면:
- 갤러리에서 사진을 선택하는 기능
- 선택된 사진에 대한 소개글을 작성하는 기능
- '올리기' 버튼을 누르면 사진과 소개글이 화면 하단 리스트에 추가되는 기능
아래 단계를 따라 하시면 바로 앱을 실행해볼 수 있습니다.
1단계: Flutter 프로젝트 생성 및 설정
- 프로젝트 생성: 터미널이나 명령 프롬프트에서 아래 명령어를 실행하여 새 프로젝트를 만듭니다.생성된 bash
flutter create photo_app cd photo_app - 필요한 패키지 추가: 사진을 갤러리에서 가져오기 위해 패키지가 필요합니다. 파일을 열고 섹션에 아래와 같이 추가해 주세요.image_pickerpubspec.yaml종속성:생성된 yaml
dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 image_picker: ^1.0.7 # 이 줄을 추가하세요 - 패키지 설치: 파일을 저장한 후, 터미널에서 아래 명령어를 실행합니다.생성된 bash
flutter pub get - 플랫폼별 권한 설정 (매우 중요!): 를 사용하려면 각 OS에 사진첩 접근 권한을 요청해야 합니다.image_picker
- iOS 설정: 파일을 열고 태그 안에 다음 코드를 추가하세요.ios/러너/Info.plist<딕셔너리>생성된 xml
<key>NSPhotoLibraryUsageDescription</key> <string>앱에서 사진을 선택하기 위해 앨범 접근 권한이 필요합니다.</string> <key>NSCameraUsageDescription</key> <string>앱에서 사진을 찍기 위해 카메라 접근 권한이 필요합니다.</string> <key>NSMicrophoneUsageDescription</key> <string>앱에서 비디오를 녹화하기 위해 마이크 접근 권한이 필요합니다.</string> - Android 설정: 안드로이드는 별도 설정이 필요하지 않지만, 앱이 API 33 이상을 타겟팅하는 경우 파일에 다음 권한을 추가하는 것이 좋습니다.android/app/src/main/AndroidManifest.xml생성된 xml
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
- iOS 설정: 파일을 열고 태그 안에 다음 코드를 추가하세요.ios/러너/Info.plist<딕셔너리>생성된 xml
2단계: 전체소스 코드 작성
생성된 다트
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
// 앱 시작점
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '간단 사진 소개 앱',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// 앱이 시작될 때 보여줄 첫 화면
home: const SignUpScreen(),
);
}
}
// 회원가입 화면 위젯
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
// 텍스트 필드를 제어하기 위한 컨트롤러
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('회원가입'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: '이메일',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16.0),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: '비밀번호',
border: OutlineInputBorder(),
),
obscureText: true, // 비밀번호를 가려줌
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: () {
// '가입하기' 버튼을 누르면 메인 화면으로 이동
// 실제 앱에서는 여기서 유효성 검사 및 서버 통신이 필요
Navigator.pushReplacement( // pushReplacement를 사용해 뒤로가기 시 회원가입 화면이 나오지 않게 함
context,
MaterialPageRoute(builder: (context) => const MainScreen()),
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16.0),
),
child: const Text('가입하고 시작하기', style: TextStyle(fontSize: 16)),
),
],
),
),
);
}
@override
void dispose() {
// 위젯이 화면에서 사라질 때 컨트롤러를 정리
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
}
// 게시물을 표현하는 데이터 클래스
class Post {
final File image;
final String description;
Post({required this.image, required this.description});
}
// 메인 화면 위젯
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
// 선택된 이미지를 저장할 변수 (nullable)
File? _image;
// 소개글을 제어하기 위한 컨트롤러
final TextEditingController _descriptionController = TextEditingController();
// 게시물 목록을 저장할 리스트
final List<Post> _posts = [];
// 이미지 피커 인스턴스
final ImagePicker _picker = ImagePicker();
// 갤러리에서 이미지를 가져오는 함수
Future<void> _pickImage() async {
try {
final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});
}
} catch (e) {
// 에러 처리
print("이미지 선택 중 오류 발생: $e");
}
}
// 게시물을 리스트에 추가하는 함수
void _addPost() {
if (_image != null && _descriptionController.text.isNotEmpty) {
setState(() {
_posts.insert(0, Post(image: _image!, description: _descriptionController.text)); // 새 게시물을 맨 앞에 추가
_image = null; // 선택된 이미지 초기화
_descriptionController.clear(); // 텍스트 필드 초기화
});
// 키보드 숨기기
FocusScope.of(context).unfocus();
} else {
// 사용자에게 사진이나 소개글이 필요하다고 알림
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('사진과 소개를 모두 입력해주세요!')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('사진 소개'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 사진 선택 및 소개 입력 영역
GestureDetector(
onTap: _pickImage,
child: Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(12),
),
child: _image == null
? const Center(child: Text('여기를 눌러 사진을 선택하세요', style: TextStyle(color: Colors.grey)))
: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(_image!, fit: BoxFit.cover),
),
),
),
const SizedBox(height: 16),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(
hintText: '사진에 대한 소개를 입력하세요...',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _addPost,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50), // 버튼 크기
),
child: const Text('소개 올리기'),
),
const Divider(height: 32, thickness: 1),
// 게시물 목록 영역
const Text('내 소개 목록', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Expanded(
child: _posts.isEmpty
? const Center(child: Text('아직 등록된 소개가 없습니다.'))
: ListView.builder(
itemCount: _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
// 게시물 이미지
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
post.image,
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
const SizedBox(width: 16),
// 게시물 설명
Expanded(
child: Text(
post.description,
style: const TextStyle(fontSize: 16),
),
),
],
),
),
);
},
),
),
],
),
),
);
}
@override
void dispose() {
_descriptionController.dispose();
super.dispose();
}
}
3단계: 앱 실행
모든 설정을 마쳤다면, 터미널에서 아래 명령어를 실행하여 앱을 빌드하고 실행합니다.
Generated bash
flutter run
코드에 대한 간단한 설명
- main.dart: 앱의 시작점입니다. 을 설정하고 첫 화면으로 을 지정합니다.MaterialAppSignUpScreen
- SignUpScreen: 간단한 회원가입 UI입니다. 2개와 1개로 구성되어 있습니다. 버튼을 누르면 를 통해 으로 이동하며, 이전 화면(회원가입)으로 돌아올 수 없게 만듭니다.TextFieldElevatedButtonNavigator.pushReplacementMainScreen
- MainScreen: 핵심 기능이 들어있는 화면입니다.
- _image: 선택된 이미지 파일을 저장하는 타입 변수입니다.File
- _posts: 객체(이미지+설명)를 담는 리스트입니다.Post
- _pickImage(): 패키지를 사용해 갤러리에서 이미지를 가져와 변수를 업데이트하는 비동기 함수입니다.image_picker_이미지
- _addPost(): 현재 선택된 와 의 텍스트를 이용해 객체를 만들어 리스트에 추가합니다. 그 후, 입력 필드를 초기화합니다._이미지_descriptionController올리기_게시물
- 빌드() 메서드: 화면 UI를 구성합니다.
- 열을 사용하여 위젯을 세로로 배치합니다.
- 제스처 감지기로 감싼 를 통해 사진 선택 영역을 만듭니다.컨테이너
- 확장와 를 사용하여 리스트에 있는 모든 게시물을 동적으로 화면에 그려줍니다. 이는 게시물이 많아져도 화면을 벗어나지 않고 스크롤이 가능하게 해줍니다.리스트뷰.빌더_게시물
참고: 이 코드는 로컬 기기 내에서만 동작합니다. 앱을 껐다 켜면 올렸던 사진과 글이 사라집니다. 데이터를 영구적으로 저장하려면 Firebase의 Firestore(데이터베이스)와 Storage(파일 저장) 같은 백엔드 서비스 연동이 필요합니다.
728x90
반응형
'핫이슈' 카테고리의 다른 글
| 새마을금고, 정말 괜찮을까? '시한폭탄'의 9가지 위험신호 (내돈은 안전할까?) (0) | 2025.07.11 |
|---|---|
| 전세의 종말, 월세시대의 서막? 대한민국 부동산, 당신이 반드시 알아야 할 3가지 진실 (0) | 2025.07.10 |
| 현금부자만 살아남나? 전세대출 막히자 벌어지는 전세시장에서는.. (0) | 2025.07.08 |
| 비싼 아파트 보단 아파텔, '가성비 천국'과 '매도지옥' 사이? 실거주 vs 투자 완벽 해부 (0) | 2025.07.08 |
| 배당투자의 시대: 왜 지금 자본소득이 노동소득보다 중요한가? (0) | 2025.07.07 |
댓글