
지난 시간에 구현한 캘린더 UI에 이어, 실제 일정을 표시하는 ScheduleCard, 오늘 날짜를 알려주는 TodayBanner, 그리고 일정을 입력받기 위한 BottomSheet와 CustomTextField를 구현하고 화면에 배치하는 방법
사용자로부터 시간이나 일정 내용을 입력받는 공통 위젯
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextFeild extends StatelessWidget {
final String label;
final bool isTime;
const CustomTextFeild({
required this.label,
required this.isTime,
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: PRIMARY_COLOR,
fontWeight: FontWeight.w600,
),
),
Expanded(
flex: isTime ? 0 : 1,
child: TextFormField(
cursorColor: Colors.grey,
maxLines: isTime ? 1 : null,
expands: !isTime,
keyboardType: isTime
? TextInputType.number
: TextInputType.multiline,
inputFormatters: isTime
? [FilteringTextInputFormatter.digitsOnly]
: [],
decoration: InputDecoration(
border: InputBorder.none,
filled: true,
fillColor: Colors.grey[300],
suffixText: isTime ? '시' : null,
),
),
),
],
);
}
}
| 코드 역할 | 비고 |
|---|---|
| final bool isTime | 시간 입력용(숫자)인지 내용 입력용(멀티라인)인지 구분 |
| maxLines: isTime ? 1 : null | 시간은 한 줄만, 내용은 줄 제한 없이(null) 입력 가능 |
| expands: !isTime | 내용 입력창일 때 부모의 남은 공간을 모두 채움 |
| inputFormatters | digitsOnly를 사용해 시간 입력 시 숫자만 허용 |
| suffixText: '시' | 시간 입력창 우측에 '시' 단위를 표시 |
등록된 일정의 시간과 내용을 시각적으로 보여주는 카드 위젯
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
class ScheduleCard extends StatelessWidget {
final int startTime;
final int endTime;
final String content;
const ScheduleCard({
required this.startTime,
required this.endTime,
required this.content,
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(width: 1.0, color: PRIMARY_COLOR),
borderRadius: BorderRadius.circular(8.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Time(startTime: startTime, endTime: endTime),
SizedBox(width: 16.0),
_Content(content: content),
SizedBox(width: 16.0),
],
),
),
),
);
}
}
class _Time extends StatelessWidget {
final int startTime;
final int endTime;
const _Time({required this.startTime, required this.endTime, Key? key}) : super(key: key);
Widget build(BuildContext context) {
final textStyle = TextStyle(fontWeight: FontWeight.w600, color: PRIMARY_COLOR, fontSize: 16);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${startTime.toString().padLeft(2, '0')}:00', style: textStyle),
Text('${endTime.toString().padLeft(2, '0')}:00', style: textStyle.copyWith(fontSize: 10.0)),
],
);
}
}
class _Content extends StatelessWidget {
final String content;
const _Content({required this.content, Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Expanded(child: Text(content));
}
}
| 코드 역할 | 비고 |
|---|---|
| IntrinsicHeight | 자식 위젯들(시간/내용) 중 가장 높은 위젯에 맞춰 높이를 동기화 |
| padLeft(2, '0') | 시간이 1자리(예: 9)일 때 앞을 0으로 채워 '09'로 표시 |
| _Time / _Content | 위젯을 분리하여 코드 가독성과 재사용성을 높임 (Private 위젯) |
상단 날짜 표시 배너와 하단 입력창 부분
// today_banner.dart 일부
return Container(
color: PRIMARY_COLOR,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${selectDate.year}년 ${selectDate.month}월 ${selectDate.day}일'),
Text('$count개'),
],
),
);
// schedule_bottom_sheet.dart 일부
showModalBottomSheet(
context: context,
isDismissible: true,
builder: (_) => ScheduleBottomSheet(),
);
| 코드 역할 | 비고 |
|---|---|
| MainAxisAlignment.spaceBetween | 날짜(좌)와 개수(우)를 양 끝으로 배치 |
| isDismissible: true | 배경 탭 시 바텀 시트가 닫히도록 설정 |
| MediaQuery... / 2 | 화면 높이의 절반만큼만 시트가 올라오도록 설정 |
위의 컴포넌트들을 모두 모아 하나의 화면으로 조립
class _HomeScreenState extends State<HomeScreen> {
DateTime selectedDate = DateTime.utc(
DateTime.now().year, DateTime.now().month, DateTime.now().day,
);
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: PRIMARY_COLOR,
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true,
builder: (_) => ScheduleBottomSheet(),
);
},
),
body: SafeArea(
child: Column(
children: [
MainCalender(selectedDate: selectedDate, onDaySelected: onDaySelected),
SizedBox(height: 8),
TodayBanner(selectDate: selectedDate, count: 0),
ScheduleCard(startTime: 12, endTime: 14, content: '프로그램 공부'),
],
),
),
);
}
}
이 파일이 하는 일: 캘린더, 배너, 일정 카드를 세로(Column)로 배치하고, FAB를 통해 일정을 추가할 수 있는 진입점을 제공합니다.
CustomTextField 내에서 Expanded를 사용할 때 부모 위젯의 제약 조건에 따라 에러가 날 수 있어 높이 설정에 유의해야 한다.HomeScreen의 리스트에 추가할지에 대한 로직 구현이 더 연습이 필요할 것 같다.
