오프라인 우선(Offline-First) 설계와 자체 Repository 패턴, 5단계 RBAC 권한 시스템을 적용한 올인원 라이프 관리 앱. 캘린더·가계부·기분 기록·타이머·다국어 알림까지 하나로 관리합니다.
645곡+ PDF 악보 뷰어와 InteractiveViewer 핀치줌/스크롤, CustomPainter 기반 필기 레이어(펜·형광펜·지우개)를 직접 구현한 악보 앱. 콘티·즐겨찾기·Firestore 동기화를 지원합니다.
예배 안내, 교회 소개·연혁, 새가족 안내, 교인 전용 로그인을 갖춘 교회 홈페이지. HTML/CSS/JS로 직접 기획부터 배포까지 구축했습니다.
TheMealDB 기반 요리명·재료 검색과 한↔영 자동 번역(검색어·결과·조리법), Hive 오프라인 캐싱을 적용한 글로벌 레시피 앱. Google 로그인 및 즐겨찾기 동기화를 지원합니다.
개발 기간: 2026.04 ~ 진행중 (개인 프로젝트, 1인 개발) 플랫폼: Android (Google Play Store 비공개 테스트) / iOS 패키지명: com.terablack.mytodo 현재 버전: 1.5.1+34
"나의 할 일" 은 할 일 관리를 중심으로 캘린더·가계부·기분 기록·타이머·세계시각까지 하나의 앱에서 관리하는 올인원 라이프 관리 Flutter 앱입니다.
기능이 늘어날수록 "누가 어떤 기능까지 쓸 수 있는가"를 명확히 관리할 필요가 있다고 판단해, 관리자·베타·프로·학생·일반 5단계로 나뉜 등급별 접근 권한(RBAC) 구조를 처음부터 설계에 포함시켰습니다.
| 분류 | 기술 |
|---|---|
| 언어 | Dart |
| 프레임워크 | Flutter |
| 로컬 저장 | SharedPreferences |
| 인증 | Firebase Auth, Google Sign-In |
| 데이터베이스 | Cloud Firestore |
| 로컬 알림 | flutter_local_notifications, timezone |
| 캘린더/그래프 | table_calendar, fl_chart |
| 위치/날씨 | geolocator, http |
| 공휴일 데이터 | holiday_jp(오프라인), 자체 한국 공휴일 서비스 |
| 앱 정보 | package_info_plus, device_info_plus |
| 배포 (Android) | Google Play Store (AAB, Release Signing) |
| 버전 관리 | Git |
| 개발 환경 | macOS, VS Code, Android Studio |
주간 요약(Weekly Summary) 화면으로 완료율 시각화
가계부 (Pro 이상)
수입/지출 기록 및 fl_chart 기반 그래프 통계
기분 기록 (Pro 이상)
날짜별 기분 로그 기록·조회
타이머 · 세계시각 (Pro 이상)
집중 타이머, 여러 도시 동시 시각 조회
클라우드 동기화 (Beta·Admin 전용)
Firebase Auth(Google 로그인) + Firestore로 기기 간 데이터 백업/복원
알림 · 날씨
로컬 푸시(일일 알림), 위치 기반 날씨, 한국/일본 공휴일 자동 반영
다국어 · 테마
한국어/영어 등 다국어 지원, 라이트/다크/시스템 테마
보호자 동의 (만 14세 미만)
parent_consent)| 등급 | 이용 가능 기능 |
|---|---|
| Normal | 할 일·그래프·기본 설정·언어 |
| Student | Normal과 동일 (만 14세 미만, Beta·Pro 부여 불가) |
| Pro | Normal + 가계부·기분·타이머·세계시각·동물 테마 |
| Beta | Pro + 클라우드 동기화 |
| Admin | 모든 기능 + 공지 작성·회원 등급 관리 |
lib/
├── main.dart # 앱 진입점, Firebase 초기화
├── config/api_keys.dart
├── models/
│ ├── todo.dart / ledger.dart / mood.dart
│ ├── user_profile.dart # UserRole enum — 등급별 기능 접근 getter
│ ├── announcement.dart
│ └── parent_consent.dart
├── data/ # 도메인별 Repository (Todo/Ledger/Mood/Profile/
│ ├── todo_repository.dart # Theme/Language/WorldClock/Notification/
│ ├── ledger_repository.dart # Announcement/ParentConsent/Auth)
│ ├── mood_repository.dart
│ ├── profile_repository.dart
│ ├── auth_repository.dart
│ └── ...
├── services/
│ ├── sync_service.dart # Firebase 동기화
│ ├── notification_service.dart / daily_notification_service.dart
│ ├── weather_service.dart
│ ├── korean_holiday_service.dart / japanese_holiday_service.dart
│ └── permission_service.dart
├── screens/ # 23개 화면 (할일/캘린더/가계부/기분/타이머/
│ # 세계시각/설정/로그인/공지/온보딩 등)
├── theme/app_theme.dart
└── widgets/
UserRole enum이 canSync / canUsePro / canUseBeta / canEditAnnouncements / canManageUsers 같은 getter로 등급-기능 매핑을 캡슐화 — 화면마다 권한 체크 로직을 흩어놓지 않고 role.canSync 한 줄로 판단개발 기간: 2025.12 ~ 진행중 (개인 프로젝트)
플랫폼: Android (Google Play Store 배포) / iOS
패키지명: com.terablack.churchpraise
현재 버전: 1.0.4
교회 예배 및 찬양팀을 위한 디지털 악보 뷰어 앱입니다.
기존에 종이 악보집이나 여러 앱을 번갈아 사용하던 불편함을 해소하고자,
새찬송가 645곡 + CCM 악보를 하나의 앱에서 통합 관리할 수 있도록 제작했습니다.
악보 열람, 유튜브 연동, 필기 기능, 콘티(세트리스트) 관리, 즐겨찾기, 클라우드 동기화까지
예배 준비에 필요한 기능을 모두 담았습니다.
| 분류 | 기술 |
|---|---|
| 언어 | Dart |
| 프레임워크 | Flutter 3.x |
| 상태 관리 | ValueNotifier, setState |
| 로컬 저장 | SharedPreferences |
| PDF 렌더링 | pdfx |
| 인증 | Firebase Auth, Google Sign-In v7 |
| 데이터베이스 | Cloud Firestore |
| 네트워크 | http, url_launcher |
| 배포 (Android) | Google Play Store (AAB, Release Signing) |
| 배포 (iOS) | TestFlight / App Store |
| 버전 관리 | Git |
| 개발 환경 | macOS, VS Code, Android Studio |
툴바 위치(상단/하단) 사용자 설정 저장
필기 기능
JSON 직렬화로 필기 데이터 로컬 영구 저장
새찬송가 (645곡)
유튜브 링크 연동 (곡별 유튜브 버튼)
CCM
박자 태그 필터
콘티(세트리스트) 관리
콘티 시작 시 악보 뷰어가 순서대로 연속 재생
즐겨찾기 (내 악보)
등록 날짜 기준 정렬 표시
Google 로그인 & 클라우드 동기화
마지막 백업 시각 표시
설정
lib/
├── main.dart # 앱 진입점, Firebase 초기화
├── models/
│ ├── song.dart # 악보 모델 (찬송가·CCM 공통)
│ └── conti.dart # 콘티 모델
├── data/
│ ├── hymn_data.dart # 새찬송가 645곡 제목·유튜브URL·박자 정적 데이터
│ └── ccm_data.dart # CCM 곡 데이터
├── services/
│ ├── favorites_service.dart # 즐겨찾기 CRUD (SharedPreferences)
│ ├── conti_service.dart # 콘티 CRUD (SharedPreferences)
│ ├── tag_service.dart # 박자 태그 관리 + 마이그레이션 (V1~V8)
│ ├── sync_service.dart # Google 로그인 + Firestore 백업/복원
│ └── app_settings.dart # 테마 설정
├── screens/
│ ├── home_screen.dart # 메인 화면 (새찬송가·CCM·콘티·내악보 탭)
│ ├── viewer_screen.dart # PDF 뷰어 + 필기 도구
│ ├── settings_screen.dart # 설정 화면
│ └── conti_editor_screen.dart
└── widgets/
├── filter_sheet.dart
└── tag_editor_sheet.dart
HymnData.timeSignatures 맵 항목 수 변화를 감지해 마이그레이션 버전 업 없이 자동 반영clientConfigurationError 및 Account reauth failed 오류 발생google-services.json 다운로드 후 재빌드로 해결InteractiveViewer의 boundaryMargin이 고정값으로 설정되어 있어, 실제 PDF 높이보다 작을 경우 스크롤 영역이 제한됨boundaryMargin을 동적으로 설정하여 해결final scaledH = _totalH * (MediaQuery.of(context).size.width / _pageW);
final verticalMargin = (scaledH > screenH) ? scaledH : screenH;
boundaryMargin: EdgeInsets.symmetric(horizontal: 80, vertical: verticalMargin),
TagService의 마이그레이션 버전을 올리지 않으면 기존 사용자에게 새 태그가 적용되지 않는 구조HymnData.timeSignatures의 항목 수를 감지해 자동 동기화하는 _syncTimeSignatures() 메서드를 도입// 항목 수가 바뀌면 자동 재동기화 → 마이그레이션 버전 불필요
if (lastCount != HymnData.timeSignatures.length) {
await _syncTimeSignatures(prefs, tags);
}
google_sign_in 패키지를 최신 버전(v7)으로 업그레이드 후 GoogleSignIn() 생성자 및 accessToken API가 제거되어 빌드 실패GoogleSignIn.instance.initialize())으로 API가 변경됨accessToken 대신 idToken만으로 Firebase 인증 처리하도록 수정// v7 방식
GoogleSignIn.instance.initialize(
clientId: '...',
serverClientId: '...',
);
final googleUser = await GoogleSignIn.instance.authenticate();
final credential = GoogleAuthProvider.credential(
idToken: googleUser.authentication.idToken,
);
개발 기간: 2026.08 (개인 프로젝트, 1인 개발) 플랫폼: Web (GitHub Pages) 저장소: github.com/herjy6372/gimcheonchurch 비고: 개인정보(성도 사진) 보호를 위해 실제 배포 사이트는 비공개 전환 예정
김천침례교회 홈페이지는 예배 안내·교회 소개·연혁·사진 앨범을 제공하는 정적 웹사이트이며, 여기에 Firebase 기반 교인 전용 회원 시스템(로그인/가입/마이페이지/관리자 승인)을 함께 구축한 프로젝트입니다.
순수 HTML/CSS/JS 정적 페이지 위에 회원 기능만 Firebase로 연결하는 하이브리드 구조로, 별도 서버 없이 GitHub Pages만으로 전체 사이트를 운영합니다.
| 분류 | 기술 |
|---|---|
| 마크업/스타일 | HTML5, CSS3(Navy+Gold 디자인), Vanilla JS |
| 인증 | Firebase Auth |
| 데이터베이스 | Cloud Firestore |
| 지도 | Google Maps 임베드 |
| 배포 | GitHub Pages (.nojekyll) |
| 이미지 최적화 | macOS sips (웹 최적화 리사이즈, ~1600px) |
| 버전 관리 | Git (osxkeychain 인증) |
gimcheonchurch/
├── index.html # 예배안내 + 오시는 길
├── intro.html # 소개 + 층별 안내
├── history.html # 연혁 타임라인
├── album.html # 연도별 탭 앨범
├── login.html / signup.html
├── member.html # 마이페이지 (프로필 수정 + 관리자 기능 링크)
├── admin.html # 회원 승인, 등급/공지 관리
├── style.css / script.js / auth-nav.js / firebase-init.js
└── images/<year>/<slug>/NN.jpg (연도·이벤트별 폴더 구조)
users{name,email,birth,gender,phone,approved,admin,role,createdAt}, notices, phoneNumbers(중복 검사용), settings/general{autoApprove}auth-nav.js가 로그인 상태에 따라 네비게이션의 "로그인 ↔ 전용공간" 표시만 동적으로 전환sips로 웹 최적화 리사이즈(~1600px) 후 연도·이벤트별 폴더 구조로 정리개발 기간: 2026.08 ~ 진행중 (개인 프로젝트, 1인 개발) 배포 링크: Google Play Store 비공개 테스트 중 앱 버전: 1.0.1 (2)
"나의 레시피" 는 전 세계 요리 레시피를 검색하고 즐겨찾기로 관리하는 개인 포트폴리오용 Flutter 앱입니다.
TheMealDB 공개 API를 활용해 요리명·재료명 검색은 물론, 카테고리와 지역(국가)별 필터로 레시피를 둘러볼 수 있고, 한글 검색어와 조리법은 자동으로 번역돼 영어 기반 API 데이터를 한국어 사용자도 무리 없이 볼 수 있게 설계했습니다. Google 로그인 시에는 Firebase/Firestore로 프로필과 즐겨찾기를 기기 간에 동기화합니다.
기획부터 설계, 개발, Google Play Console 비공개 테스트 등록(콘텐츠 등급, 데이터 보안, 스토어 등록정보 등 전 항목)까지 전 과정을 혼자 진행한 프로젝트입니다.
| 분류 | 기술 |
|---|---|
| 언어 | Dart |
| 프레임워크 | Flutter |
| 상태 관리 | Riverpod (StateProvider / FutureProvider) |
| 네트워킹 | Dio |
| 로컬 캐시 | Hive (TTL 기반 오프라인 캐시) |
| 인증 | Firebase Auth, Google Sign-In |
| 데이터베이스 | Cloud Firestore |
| 외부 API | TheMealDB(레시피), MyMemory Translation API(번역) |
| 앱 정보 | package_info_plus, device_info_plus |
| 배포 (Android) | Google Play Store (AAB, Release Signing) |
| 버전 관리 | Git |
| 개발 환경 | macOS, VS Code |
selectedCategoryProvider / selectedAreaProvider / searchQueryProvider 세 상태 중 하나를 고르면 나머지 둘을 자동으로 초기화하도록 설계해, 서로 충돌하는 검색 조건이 동시에 활성화되지 않도록 함searchResultsProvider가 카테고리 → 지역 → 텍스트검색 순으로 우선순위를 방어적으로 재확인하는 이중 안전장치 포함
Repository + TtlCache 기반 오프라인 우선 설계
MealDbApi(순수 API 클라이언트) → RecipeRepository(Hive TTL 캐시 결합) → Riverpod Provider → UI로 계층을 분리해, API 클라이언트는 캐싱을, Repository는 UI를 몰라도 되게 구성검색/카테고리/지역/상세/번역 결과를 각각 캐싱해 같은 조건 재검색 시 API 호출을 최소화
한↔영 자동 번역 파이프라인
무료 API의 건당 500자 제한에 맞춰 문장 단위로 청크를 나누고, 4개씩 배치 처리해 rate limit을 회피
Firebase 프로필 사진 저장 방식
Firebase Auth의 photoURL은 2048자 길이 제한이 있어 커스텀 사진을 담을 수 없어서, 클라이언트에서 256×256 JPEG로 리사이즈한 뒤 base64로 인코딩해 Firestore 문서에 별도 저장
계정 정보 UI 일원화
lib/
├── main.dart / app.dart # 앱 진입점, HomeShell(검색/즐겨찾기/마이페이지 탭)
├── core/
│ ├── cache/
│ │ ├── hive_boxes.dart # Hive box 정의
│ │ └── ttl_cache.dart # TTL 기반 캐시 래퍼
│ ├── network/
│ │ ├── dio_client.dart # Dio 인스턴스, 디버그 빌드 전용 로깅
│ │ └── error_message.dart # 에러 → 사용자 친화 메시지 변환
│ └── widgets/state_views.dart # 공용 로딩/에러/빈 상태 위젯
└── features/
├── auth/
│ ├── data/ # auth_repository.dart, user_profile_repository.dart
│ ├── domain/user_profile.dart
│ └── presentation/ # login_screen.dart, my_page_screen.dart
├── recipe_search/
│ ├── data/ # meal_db_api.dart, recipe_repository.dart, translation_service.dart
│ ├── domain/ # recipe.dart, category_labels.dart, area_labels.dart
│ └── presentation/ # recipe_search_screen.dart, recipe_search_providers.dart
├── recipe_detail/ # LocalizedRecipe로 번역 결과 래핑
├── favorites/ # Firestore users/{uid}/favorites 서브컬렉션
└── settings/ # 설정 화면, 계정 정보 다이얼로그 공용 로직
authStateProvider를 즐겨찾기·프로필 Provider가 함께 watch해, 로그인/로그아웃/사용자 전환 시 관련 화면이 자동으로 다시 구독됨users/{uid} 문서와 하위 favorites 서브컬렉션 모두 소유자만 읽기/쓰기 가능하도록 보안 규칙으로 제한users/{uid} 문서는 소유자 스코프(request.auth.uid == uid)는 되어 있었지만, 쓰기 필드에 대한 화이트리스트나 크기 제한이 전혀 없었습니다. 즉, 변조된 클라이언트가 인증만 되어 있으면 자기 문서에 임의 필드나 최대 1MB까지 데이터를 채워 넣을 수 있는 구조였습니다.
allow read, write: if request.auth.uid == uid 한 줄로만 되어 있던 규칙을, request.resource.data.keys().hasOnly([...])로 허용 필드(photoBase64, birthDate, phoneNumber)를 제한하고 각 필드의 타입·크기(사진 200KB, 전화번호 30자 등)를 검증하도록 재작성한 뒤 firebase deploy --only firestore:rules로 실제 프로젝트에 배포했습니다.
android/key.properties가 없는 환경에서 release 빌드를 하면, 빌드가 실패하는 대신 아무 경고 없이 debug 서명으로 조용히 폴백하도록 구성돼 있었습니다. 이 상태로 배포하면 공개적으로 알려진 debug 키로 서명된 release APK가 나갈 위험이 있었습니다.
build.gradle.kts의 signingConfig 분기를, key.properties가 없으면 -PallowDebugSigning 옵션을 명시하지 않는 한 GradleException으로 빌드 자체를 실패시키도록 변경해 "조용한 실패"를 "시끄러운 실패"로 바꿨습니다.
signInWithGoogle()은 사용자가 계정 선택 화면에서 취소해도 예외 없이 정상 반환되는데, 로그인 화면의 실행 로직은 이 케이스를 구분하지 못하고 무조건 Navigator.pop()을 호출해 로그인 화면을 닫아버렸습니다. 사용자 입장에서는 취소했는데 로그인된 것처럼 화면이 넘어가는 버그였습니다.
signInWithGoogle()이 Future<bool>을 반환하도록 바꿔 취소 시 false, 성공 시 true를 리턴하게 하고, 화면에서는 true일 때만 화면을 닫도록 수정했습니다.
검색 탭에 지역(국가)별 필터를 추가하면서 한식/양식/중식 같은 필터를 기대했지만, TheMealDB의 지역 목록 API(list.php?a=list)를 실제로 확인해보니 American, British, Chinese, French, Italian, Japanese 등 28개 지역만 존재하고 Korean(한식) 데이터 자체가 API에 없다는 것을 발견했습니다.
없는 데이터를 억지로 만들어 넣기보다, 실제 지원되는 28개 지역에 한글 라벨(area_labels.dart)만 매핑해 제공하고, 한식 데이터가 API 자체에 없다는 점을 사용자에게 명확히 안내하는 방향으로 결정했습니다.
signOut()이 Auth/Google 로그아웃만 하고 Firestore 오프라인 퍼시스턴스는 그대로 둬서, 이전 사용자의 프로필·즐겨찾기가 기기에 남아있던 문제 → terminate() + clearPersistence() 추가photoBase64 값이 손상돼 있으면 base64Decode가 예외를 던져 마이페이지 전체가 크래시되던 위험 → try/catch로 감싸 실패 시 기본 아바타로 대체Stream.empty()라 값을 하나도 안 내보내고 닫혀서 로딩 스피너가 멈추지 않던 버그 → Stream.value(const [])로 교체String.hashCode로 만들었는데, 이는 Dart 런타임 버전 간 안정성이 보장되지 않아 캐시가 조용히 무효화될 수 있는 구조 → 자체 구현한 안정적인 FNV-1a 해시로 교체"사용자의 니즈를 충족할 수 있는 앱 개발을 만들기 위해 항상 열린 마인드로 소통하는 AI-Assisted Flutter Developer, Heo Jinyoung이 되겠습니다."
연락처 : herjy6372@gmail.com, 010-7176-6372