시크릿 갤러리 앱
Flutter로 SQLite 기반 비밀 앨범 갤러리 앱을 만들어보아요.
목차
1.
프로젝트 개요
2.
프로젝트 초기 설정
3.
사용 라이브러리
4.
데이터 설계 — DbService
5.
화면 구현
6.
주요 UI 컴포넌트
7.
트러블슈팅
8.
핵심 Flutter 개념 요약
1. 프로젝트 개요
앱 소개
•
이름: 시크릿 갤러리 (Secret Gallery)
•
목적: Flutter의 SQLite DB, 권한 처리, 파일 복사, 상태 관리, 페이지 라우팅을 익히기 위한 실습 프로젝트
•
특징: 앱 잠금(PIN) + 비밀 앨범 기능을 갖춘 개인용 갤러리
주요 기능
기능 | 설명 |
앱 잠금 | 시작 시 PIN 입력, 최초 실행 시 PIN 설정 |
앨범 관리 | 일반/비밀 앨범 생성, 이름 변경, 삭제, 순서 변경 |
비밀 앨범 | 개별 비밀번호로 보호, 진입 시 비밀번호 확인 |
사진 불러오기 | 기기 갤러리에서 인앱 피커로 다중 선택 후 내부 저장소 복사 |
사진 관리 | 그리드 뷰, 정렬, 다중 선택, 삭제, 순서 변경 |
사진 공유 | share_plus로 외부 앱(카카오톡 등)에 공유 |
사진 상세 | 전체 이미지 뷰 + 제목·메모 편집 |
설정 | 앱 비밀번호 변경 |
앨범 검색 | 앨범 이름으로 실시간 검색 |
프로젝트 구조
secret_gallery/
├── lib/
│ ├── main.dart # 앱 진입점 · 다크 테마 설정
│ ├── models/
│ │ ├── album.dart # Album 데이터 모델
│ │ └── photo.dart # Photo 데이터 모델
│ ├── services/
│ │ ├── auth_service.dart # 앱/앨범 비밀번호 인증
│ │ ├── db_service.dart # SQLite CRUD (앨범·사진)
│ │ ├── file_service.dart # 이미지 파일 복사·삭제
│ │ └── share_service.dart # 사진 공유
│ └── pages/
│ ├── lock_page.dart # 앱 잠금 화면 (PIN 입력/설정)
│ ├── album_list_page.dart # 앨범 목록 화면
│ ├── photo_list_page.dart # 앨범 내 사진 목록 화면
│ ├── gallery_picker_page.dart# 기기 갤러리 피커 화면
│ ├── photo_detail_page.dart # 사진 상세·편집 화면
│ └── settings_page.dart # 설정 화면
├── guide/ # 강의 노트 문서 폴더
└── pubspec.yaml
Plain Text
복사
2. 프로젝트 초기 설정
Flutter 프로젝트 생성
flutter create secret_gallery
cd secret_gallery
Bash
복사
패키지 추가
flutter pub add path_provider path sqflite image_picker shared_preferences share_plus permission_handler photo_manager flutter_reorderable_grid_view
Bash
복사
pubspec.yaml 결과
dependencies:
flutter:
sdk: flutter
path_provider: ^2.1.0 # 내부 저장소 경로
path: ^1.9.0 # 경로 문자열 조합
sqflite: ^2.3.3 # SQLite 데이터베이스
image_picker: ^1.1.2 # 갤러리/카메라 피커 (보조)
shared_preferences: ^2.3.0 # 앱 PIN 저장
share_plus: ^10.1.4 # 사진 공유
permission_handler: ^11.3.0 # 갤러리 권한 요청
photo_manager: ^3.0.0 # 인앱 갤러리 피커
flutter_reorderable_grid_view: ^5.6.0 # 드래그 순서 변경 그리드
YAML
복사
폴더 구조 생성
lib/
├── models/ ← Album, Photo 모델
├── services/ ← DB·인증·파일·공유 서비스
└── pages/ ← 각 화면
Plain Text
복사
main.dart — 앱 진입점
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '시크릿 갤러리',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(), // 전체 다크 테마
home: const LockPage(), // 앱 시작 = 잠금 화면
);
}
}
Dart
복사
ThemeData.dark(): 시스템 다크 팔레트를 기본 적용합니다. 홈 화면이 LockPage이므로 앱 진입 시 항상 PIN 검증을 거칩니다.
3. 사용 라이브러리
패키지 | 역할 |
path_provider | 내부 저장소 경로 획득 |
path | 경로 문자열 조합(join) |
sqflite | SQLite DB (앨범·사진 CRUD) |
shared_preferences | PIN 번호 영구 저장 |
permission_handler | 갤러리 접근 권한 요청 |
photo_manager | 기기 사진 목록 조회, 인앱 피커 |
share_plus | 외부 앱으로 사진 공유 |
flutter_reorderable_grid_view | 드래그로 그리드 순서 변경 |
4. 데이터 설계 — DbService
원칙: UI
서비스 레이어 분리 (단일 책임 원칙)
4-1. 데이터베이스 전략
고려 사항 | 결정 |
저장 방식 | SQLite (gallery.db) — 앨범·사진 관계 구조에 적합 |
파일 저장 위치 | getApplicationDocumentsDirectory()/images/ |
파일명 규칙 | {timestamp_ms}.png (밀리초 타임스탬프) |
정렬 | sort_order 컬럼으로 사용자 지정 순서 유지 |
4-2. 데이터 모델
Album
class Album {
final int? id;
final String name;
final String type; // 'normal' | 'secret'
final String? password; // 비밀 앨범일 때만 존재
final int sortOrder;
}
Dart
복사
Photo
class Photo {
final int? id;
final int albumId; // 소속 앨범 FK
final String path; // 내부 저장소 절대 경로
final String? title; // 사용자가 지정한 제목
final String? memo; // 사용자가 입력한 메모
final String createdAt; // ISO 8601 문자열
final int sortOrder;
}
Dart
복사
4-3. SQL 스키마
CREATE TABLE albums (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'normal' | 'secret'
password TEXT,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
album_id INTEGER NOT NULL,
path TEXT NOT NULL,
title TEXT,
memo TEXT,
created_at TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0
);
SQL
복사
버전 관리: onUpgrade 콜백에서 ALTER TABLE로 컬럼 추가 시 기존 데이터를 보존합니다.
5. 화면 구현
5-1. LockPage — 앱 잠금 화면
구현 순서
1. initState → hasAppPassword() 확인
- 없으면 _isSettingMode = true (PIN 설정 화면)
- 있으면 PIN 입력 화면
2. _onSubmit()
- 설정 모드: setAppPassword() → AlbumListPage 이동
- 입력 모드: checkAppPassword() → 맞으면 이동, 틀리면 오류 메시지
Plain Text
복사
5-2. AlbumListPage — 앨범 목록
구현 순서
1. _loadAlbums() → DB에서 앨범 목록 조회
2. _buildNormalGrid() → GridView.builder + 앨범 카드
3. 다중 선택 모드 → Set<int> + 드래그 선택(Listener + RenderBox hit test)
4. 정렬 팝업 → PopupMenuButton<_SortType>
5. 순서 변경 모드 → ReorderableBuilder + DB sort_order 업데이트
6. 앨범 생성 다이얼로그 → StatefulBuilder (비밀 앨범 토글 연동)
7. 비밀 앨범 진입 → 비밀번호 다이얼로그 → AuthService.checkAlbumPassword()
8. 검색 모드 → AppBar TextField → _searchQuery 필터링
Plain Text
복사
5-3. PhotoListPage — 사진 목록
구현 순서
1. _loadPhotos() → DB에서 앨범 내 사진 조회
2. 그리드 뷰 → GridView.builder + Hero 태그
3. _addPhoto() → GalleryPickerPage → FileService.saveImage() → DB 저장
4. 다중 선택·삭제·공유 → Set<int> + Share.shareXFiles()
5. 정렬 / 순서 변경 → AlbumListPage와 동일 패턴
Plain Text
복사
5-4. GalleryPickerPage — 인앱 갤러리 피커
구현 순서
1. permission_handler 권한 요청 (Android 버전별 분기)
2. photo_manager로 전체 사진 목록 로드
3. GridView.builder + 썸네일 (AssetEntityImage)
4. 탭으로 다중 선택 → Set<AssetEntity>
5. 확인 버튼 → Navigator.pop(context, _selected.toList())
Plain Text
복사
5-5. PhotoDetailPage — 사진 상세
구현 순서
1. Hero 애니메이션으로 이미지 전환
2. 제목·메모 TextField
3. _saveMetadata() → DB.updatePhotoMeta()
4. _sharePhoto() → ShareService.sharePhoto()
Plain Text
복사
5-6. SettingsPage — 설정
1. 앱 비밀번호 변경: 현재 PW 확인 → 새 PW 입력 → 확인 → setAppPassword()
Plain Text
복사
6. 주요 UI 컴포넌트
컴포넌트 | 사용 위치 | 역할 |
PopScope | PhotoListPage | 다중 선택 모드일 때 뒤로 가기 가로채기 |
ReorderableBuilder | AlbumListPage, PhotoListPage | 드래그 순서 변경 |
Hero | PhotoListPage → DetailPage | 사진 전환 애니메이션 |
AlertDialog + StatefulBuilder | AlbumListPage | 다이얼로그 내부 상태 관리 |
Listener + RenderBox | AlbumListPage, PhotoListPage | 드래그 다중 선택 |
7. 트러블슈팅
7-1. Android 갤러리 권한 버전별 분기
Android 버전 | 필요 권한 |
14+ | READ_MEDIA_VISUAL_USER_SELECTED (permission_handler: Permission.photos) |
13 | READ_MEDIA_IMAGES (Permission.mediaLibrary) |
12 이하 | READ_EXTERNAL_STORAGE (Permission.storage) |
해결책: GalleryPickerPage._checkPermission()에서 단계적으로 요청
final visual = await Permission.photos.request();
if (visual.isGranted || visual.isLimited) return true;
final imgStatus = await Permission.mediaLibrary.request();
if (imgStatus.isGranted) return true;
final storage = await Permission.storage.request();
return storage.isGranted;
Dart
복사
7-2. PopupMenuButton 다크 테마 색상
다크 테마에서 팝업 메뉴 배경이 기본 흰색으로 나타날 수 있습니다.
PopupMenuButton<_SortType>(
color: Colors.grey[850], // ← 명시적 배경색 설정
...
)
Dart
복사
7-3. ReorderableBuilder + 일반 GridView 전환
사용자 지정 정렬 모드일 때만 ReorderableBuilder를 사용하고, 나머지 정렬은 일반 GridView를 사용합니다.
body: _sortType == _SortType.custom
? _buildReorderableGrid()
: _buildNormalGrid(),
Dart
복사
8. 핵심 Flutter 개념 요약
개념 | 적용 위치 | 핵심 내용 |
StatefulWidget + setState | 모든 페이지 | UI 상태 변경 시 리빌드 |
async / await | DB·파일·권한 호출 | 비동기 I/O를 동기 스타일로 작성 |
Navigator.push / pushReplacement | 페이지 전환 | push는 스택 추가, pushReplacement는 현재 교체 |
PopScope | PhotoListPage | 뒤로 가기 이벤트 가로채기 (Flutter 3.22+) |
Hero | 사진 목록 → 상세 | 공유 태그 기반 전환 애니메이션 |
AlertDialog + StatefulBuilder | 다이얼로그 | 다이얼로그 내부 상태 독립 관리 |
Listener + RenderBox.localToGlobal | 드래그 선택 | 포인터 좌표로 위젯 히트 테스트 |
Batch (sqflite) | 순서 저장 | 여러 UPDATE를 하나의 트랜잭션으로 처리 |



















