Search
Duplicate

시크릿 갤러리 앱

시크릿 갤러리 앱

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
복사
: 버전을 직접 지정하지 않고 flutter pub add 패키지명을 사용하면 pub.dev에서 최신 버전을 자동으로 선택합니다.

폴더 구조 생성

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 컴포넌트

자세한 내용은 주요UI.md 참고
컴포넌트
사용 위치
역할
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를 하나의 트랜잭션으로 처리