πŸ“š Today I Learned 25회차 - Flutter 일정 관리 μ•± λ§Œλ“€κΈ° πŸ“…

4 λΆ„ μ†Œμš”

μ˜€λŠ˜μ€ Flutter둜 일정 관리 앱을 λ§Œλ“œλŠ” ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν–ˆμŠ΅λ‹ˆλ‹€! 달λ ₯ UI κ΅¬ν˜„λΆ€ν„° λ°μ΄ν„°λ² μ΄μŠ€ 연동, μ„œλ²„ ν†΅μ‹ κΉŒμ§€ λ‹¨κ³„λ³„λ‘œ ν•™μŠ΅ν•˜λ©° 싀무에 ν•„μš”ν•œ 쒅합적인 μ•± 개발 μ—­λŸ‰μ„ 읡히게 될 것 κ°™μ•„ κΈ°λŒ€κ°€ λ©λ‹ˆλ‹€. πŸŽ‰

🎯 ν•™μŠ΅ λͺ©ν‘œ

  • 일정 관리 μ•±μ˜ 전체 ꡬ쑰와 단계별 개발 κ³Όμ • μ΄ν•΄ν•˜κΈ°
  • TableCalendar ν”ŒλŸ¬κ·ΈμΈμ„ ν™œμš©ν•œ 달λ ₯ UI κ΅¬ν˜„
  • Drift와 SQLiteλ₯Ό μ΄μš©ν•œ 둜컬 λ°μ΄ν„°λ² μ΄μŠ€ ꡬ좕
  • ν‚€λ³΄λ“œ λ…ΈμΆœ μ‹œ UI λŒ€μ‘ 방법 읡히기
  • pubspec.yaml의 dependencies vs dev_dependencies 차이 이해

πŸ“š ν”„λ‘œμ νŠΈ κ°œμš”

πŸ“‹ λ―Έμ…˜

일정을 λ‚ μ§œμ™€ μ‹œκ°„λ³„λ‘œ μ €μž₯ν•˜κ³  μ‘°νšŒν•˜λŠ” μ•± κ΅¬ν˜„ν•˜κΈ°

⭐ μ£Όμš” κΈ°λŠ₯

κΈ°λŠ₯ μ„€λͺ…
일정 μΆ”κ°€ λ²„νŠΌμ„ 눌러 μƒˆλ‘œμš΄ 일정 μž…λ ₯ 폼 μ—΄κΈ°
일정 생성 폼에 정보λ₯Ό μž…λ ₯ν•˜μ—¬ μƒˆ 일정 λ§Œλ“€κΈ°
일정 쑰회 달λ ₯μ—μ„œ λ‚ μ§œλ₯Ό 선택해 ν•΄λ‹Ή λ‚ μ§œμ˜ 일정 확인
일정 카운트 μ„ νƒν•œ λ‚ μ§œμ— λ“±λ‘λœ 일정 개수 ν‘œμ‹œ
데이터 μ €μž₯ SQLite 둜컬 DB λ˜λŠ” μ„œλ²„μ— 일정 μ €μž₯

πŸ› οΈ 핡심 ꡬ성 μš”μ†Œ

πŸ“¦ 일정 관리 μ•± 기술 μŠ€νƒ
β”œβ”€ 🎨 UI 라이브러리
β”‚  β”œβ”€ TableCalendar (달λ ₯ μœ„μ ―)
β”‚  └─ TextFormField (μž…λ ₯ 폼)
β”‚
β”œβ”€ πŸ’Ύ 데이터 관리
β”‚  β”œβ”€ SQLite (둜컬 λ°μ΄ν„°λ² μ΄μŠ€)
β”‚  β”œβ”€ Drift (SQL ORM)
β”‚  └─ Code Generation (μžλ™ μ½”λ“œ 생성)
β”‚
└─ 🌐 λ„€νŠΈμ›Œν¬ 톡신
   β”œβ”€ REST API
   └─ Dio (HTTP ν΄λΌμ΄μ–ΈνŠΈ)

πŸš€ 단계별 개발 λ‘œλ“œλ§΅

ν”„λ‘œμ νŠΈλŠ” μ μ§„μ μœΌλ‘œ λ°œμ „ν•˜λŠ” ꡬ쑰둜 μ§„ν–‰λ©λ‹ˆλ‹€:

1단계: λ©”λͺ¨λ¦¬ μ €μž₯
   ↓
2단계: 둜컬 λ°μ΄ν„°λ² μ΄μŠ€ (SQLite + Drift)
   ↓
3단계: μ„œλ²„ λ°μ΄ν„°λ² μ΄μŠ€ 연동 (REST API)
   ↓
4단계: Firestore 연동
   ↓
5단계: JWT 인증 κ΅¬ν˜„
   ↓
6단계: μ†Œμ…œ 둜그인 & Firebase 인증
   ↓
7단계: Supabase 연동
   ↓
8단계: κ΄‘κ³  및 배포

πŸ’‘ ν•™μŠ΅ 포인트: 같은 앱을 μ—¬λŸ¬ λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν•˜λ©΄μ„œ 각 기술의 μž₯단점을 비ꡐ할 수 μžˆμŠ΅λ‹ˆλ‹€!

πŸ’» TableCalendar ν”ŒλŸ¬κ·ΈμΈ

πŸ“– κ°œμš”

TableCalendarλŠ” Flutterμ—μ„œ κ°€μž₯ 많이 μ‚¬μš©λ˜λŠ” 달λ ₯ ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ, λ‹€μ–‘ν•œ μ»€μŠ€ν„°λ§ˆμ΄μ§• μ˜΅μ…˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

⚑ μ£Όμš” κΈ°λŠ₯

κΈ°λŠ₯ μ„€λͺ…
νŠΉμ • λ‚ μ§œ 선택 단일 λ‚ μ§œ 선택 및 포컀슀
λ‚ μ§œ λ²”μœ„ 선택 μ‹œμž‘μΌκ³Ό μ’…λ£ŒμΌμ„ μ§€μ •ν•œ λ²”μœ„ 선택
λ‚ μ§œ μ§€μ •ν•˜κΈ° ν˜„μž¬ 화면에 ν‘œμ‹œλ  μ—°/μ›” μ„€μ •
이벀트/일정 ν‘œμ‹œ 각 λ‚ μ§œμ— λ§ˆμ»€λ‚˜ λ°°μ§€ ν‘œμ‹œ

πŸ“ κΈ°λ³Έ μ‚¬μš©λ²•

// TableCalendar κΈ°λ³Έ κ΅¬ν˜„
import 'package:table_calendar/table_calendar.dart';

TableCalendar(
  // 첫 번째 λ‚ μ§œ (달λ ₯의 μ‹œμž‘ λ²”μœ„)
  firstDay: DateTime.utc(2020, 1, 1),

  // λ§ˆμ§€λ§‰ λ‚ μ§œ (달λ ₯의 끝 λ²”μœ„)
  lastDay: DateTime.utc(2030, 12, 31),

  // 포컀슀된 λ‚ μ§œ (ν˜„μž¬ ν‘œμ‹œλ˜λŠ” μ›”)
  focusedDay: DateTime.now(),

  // μ„ νƒλœ λ‚ μ§œ
  selectedDayPredicate: (day) {
    return isSameDay(_selectedDay, day);
  },

  // λ‚ μ§œ 선택 μ‹œ 콜백
  onDaySelected: (selectedDay, focusedDay) {
    setState(() {
      _selectedDay = selectedDay;
      _focusedDay = focusedDay;
    });
  },
)

πŸ”§ pubspec.yaml μ„€μ •

πŸ“¦ Dependencies vs Dev Dependencies

ν”„λ‘œμ νŠΈ μ„€μ • 파일인 pubspec.yamlμ—μ„œ νŒ¨ν‚€μ§€λ₯Ό μΆ”κ°€ν•  λ•Œ 두 κ°€μ§€ μ„Ήμ…˜μ΄ μžˆμŠ΅λ‹ˆλ‹€:

ꡬ뢄 μš©λ„ νŒ¨ν‚€μ§• μ˜ˆμ‹œ
dependencies μ•± 싀행에 ν•„μš”ν•œ νŒ¨ν‚€μ§€ βœ… 포함됨 table_calendar, dio, drift
dev_dependencies 개발 μ‹œμ—λ§Œ ν•„μš”ν•œ 도ꡬ ❌ μ œμ™Έλ¨ build_runner, drift_dev, flutter_test

πŸ’‘ μ½”λ“œ 생성 (Code Generation)

Drift와 같은 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” μ½”λ“œ 생성 κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€:

dependencies:
  drift: ^2.0.0           # μ‹€μ œ μ•±μ—μ„œ μ‚¬μš©
  sqlite3_flutter_libs: ^0.5.0

dev_dependencies:
  drift_dev: ^2.0.0       # μ½”λ“œ μƒμ„±μš© (개발 λ‹¨κ³„μ—μ„œλ§Œ)
  build_runner: ^2.3.0    # μ½”λ“œ 생성 μ‹€ν–‰κΈ°

μ½”λ“œ μƒμ„±μ΄λž€?

  • Dart μ–Έμ–΄λ‘œ ν…Œμ΄λΈ” μ •μ˜λ₯Ό μž‘μ„±ν•˜λ©΄
  • SQL 쿼리가 μžλ™μœΌλ‘œ μƒμ„±λ˜λŠ” κΈ°λŠ₯
  • 직접 SQL을 μž‘μ„±ν•˜μ§€ μ•Šμ•„λ„ λ˜μ–΄ 생산성과 μ•ˆμ •μ„± ν–₯상
# μ½”λ“œ 생성 μ‹€ν–‰ λͺ…λ Ήμ–΄
flutter pub run build_runner build

# 변경사항 κ°μ§€ν•˜μ—¬ μžλ™ μž¬μƒμ„±
flutter pub run build_runner watch

⚠️ 주의: μ½”λ“œ 생성은 DB ꡬ쑰가 변경될 λ•Œλ§ˆλ‹€ μ‹€ν–‰ν•΄μ•Ό ν•˜μ§€λ§Œ, μ•± μ‹€ν–‰ μ‹œμ—λŠ” ν•„μš” μ—†μœΌλ―€λ‘œ dev_dependencies에 ν¬ν•¨ν•©λ‹ˆλ‹€.

🎨 UI 이슈 ν•΄κ²°

⌨️ ν‚€λ³΄λ“œ λ…ΈμΆœ μ‹œ μœ„μ ― κ°€λ¦Ό 문제

문제 상황:

  • μ‚¬μš©μžκ°€ TextFormFieldλ₯Ό νƒ­ν•˜λ©΄ ν‚€λ³΄λ“œκ°€ ν™”λ©΄ ν•˜λ‹¨μ—μ„œ μ˜¬λΌμ˜΅λ‹ˆλ‹€
  • ν‚€λ³΄λ“œκ°€ μž…λ ₯ ν•„λ“œλ‚˜ λ²„νŠΌμ„ κ°€λ €μ„œ μ‚¬μš©μ„±μ΄ λ–¨μ–΄μ§‘λ‹ˆλ‹€

ν•΄κ²° 방법: MediaQueryλ₯Ό μ‚¬μš©ν•˜μ—¬ ν‚€λ³΄λ“œ λ†’μ΄λ§ŒνΌ 화면을 μœ„λ‘œ μ΄λ™μ‹œν‚΅λ‹ˆλ‹€.

πŸ’» κ΅¬ν˜„ μ½”λ“œ

// ν‚€λ³΄λ“œ 높이λ₯Ό κ³ λ €ν•œ BottomSheet κ΅¬ν˜„
final bottomInset = MediaQuery.of(context).viewInsets.bottom;

return SafeArea(
  child: Container(
    // ν™”λ©΄ λ†’μ΄μ˜ 절반 + ν‚€λ³΄λ“œ 높이
    height: MediaQuery.of(context).size.height / 2 + bottomInset,
    color: Colors.white,

    child: Padding(
      // μ•„λž˜μͺ½ νŒ¨λ”©μ„ ν‚€λ³΄λ“œ λ†’μ΄λ§ŒνΌ μΆ”κ°€
      padding: EdgeInsets.only(
        left: 8,
        right: 8,
        top: 8,
        bottom: bottomInset,  // πŸ”‘ 핡심: ν‚€λ³΄λ“œ λ†’μ΄λ§ŒνΌ νŒ¨λ”© μΆ”κ°€
      ),
      child: Column(
        children: [
          // μž…λ ₯ 폼 μœ„μ ―λ“€
          TextFormField(
            decoration: InputDecoration(labelText: '일정 제λͺ©'),
          ),
          // ... 기타 μœ„μ ―λ“€
        ],
      ),
    ),
  ),
);

πŸ” μ½”λ“œ 상세 μ„€λͺ…

// 1️⃣ μ‹œμŠ€ν…œμ΄ μ°¨μ§€ν•˜λŠ” ν•˜λ‹¨ μ˜μ—­ 크기 κ°€μ Έμ˜€κΈ°
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
// πŸ“± 일반적으둜 이 κ°’ = ν‚€λ³΄λ“œ 높이

// 2️⃣ Container 높이 동적 μ‘°μ •
height: MediaQuery.of(context).size.height / 2 + bottomInset
// ν‚€λ³΄λ“œκ°€ λ‚˜νƒ€λ‚˜λ©΄ Containerκ°€ μžλ™μœΌλ‘œ 높아짐

// 3️⃣ ν•˜λ‹¨ νŒ¨λ”© μΆ”κ°€λ‘œ 컨텐츠λ₯Ό μœ„λ‘œ 이동
padding: EdgeInsets.only(bottom: bottomInset)
// ν‚€λ³΄λ“œκ°€ 올라온 만큼 컨텐츠도 μœ„λ‘œ 이동

πŸ“Š λ™μž‘ 원리

μƒνƒœ bottomInset Container 높이 κ²°κ³Ό
ν‚€λ³΄λ“œ μˆ¨κΉ€ 0 ν™”λ©΄ 절반 일반 BottomSheet
ν‚€λ³΄λ“œ ν‘œμ‹œ ~300px ν™”λ©΄ 절반 + 300px μž…λ ₯ ν•„λ“œκ°€ ν‚€λ³΄λ“œ μœ„λ‘œ 이동

🎯 BottomSheet ν‘œμ‹œν•˜κΈ°

// showModalBottomSheet둜 일정 μž…λ ₯ 폼 ν‘œμ‹œ
showModalBottomSheet(
  context: context,
  // ν‚€λ³΄λ“œκ°€ 올라올 λ•Œ BottomSheet도 같이 μ˜¬λΌκ°€λ„λ‘ μ„€μ •
  isScrollControlled: true,
  builder: (context) {
    return ScheduleBottomSheet(); // μœ„μ—μ„œ κ΅¬ν˜„ν•œ μœ„μ ―
  },
);

πŸ’‘ 팁: isScrollControlled: trueλ₯Ό μ„€μ •ν•˜λ©΄ BottomSheet 높이λ₯Ό 자유둭게 μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€!

πŸ“ 마무리

βœ… 였늘 배운 것

  • ν”„λ‘œμ νŠΈ ꡬ쑰: 일정 관리 μ•±μ˜ 전체 μ•„ν‚€ν…μ²˜μ™€ 8단계 λ°œμ „ κ³Όμ •
  • TableCalendar: Flutter 달λ ₯ ν”ŒλŸ¬κ·ΈμΈμ˜ κΈ°λ³Έ μ‚¬μš©λ²•κ³Ό μ£Όμš” κΈ°λŠ₯
  • νŒ¨ν‚€μ§€ 관리: dependencies와 dev_dependencies의 차이점
  • μ½”λ“œ 생성: Drift의 μžλ™ SQL 생성 원리와 build_runner μ‚¬μš©λ²•
  • UI λŒ€μ‘: MediaQuery.viewInsetsλ₯Ό ν™œμš©ν•œ ν‚€λ³΄λ“œ λ…ΈμΆœ 이슈 ν•΄κ²°

πŸš€ λ‹€μŒ κ³„νš

  1. 달λ ₯ UI μ™„μ„±: TableCalendar μ»€μŠ€ν„°λ§ˆμ΄μ§• 및 이벀트 마컀 ν‘œμ‹œ
  2. μž…λ ₯ 폼 κ΅¬ν˜„: 제λͺ©, λ‚΄μš©, μ‹œκ°„ 선택 κΈ°λŠ₯이 ν¬ν•¨λœ μ™„μ „ν•œ 일정 μž…λ ₯ ν™”λ©΄
  3. λ©”λͺ¨λ¦¬ μ €μž₯: μ•± λ‚΄ λ©”λͺ¨λ¦¬μ— 일정 데이터 μ €μž₯ 및 쑰회 κΈ°λŠ₯ κ΅¬ν˜„
  4. Drift ν•™μŠ΅: SQLite λ°μ΄ν„°λ² μ΄μŠ€ 섀계 및 Drift ORM μ‚¬μš©λ²• 읡히기

λŒ“κΈ€λ‚¨κΈ°κΈ°