Flutter로 에브리타임 시간표 만들기 (UI 및 데이터 구조)
>2023.09.12.
개요
시간표 데이터를 모두 만들었으니, 이 데이터를 이용해 실제로 플러터에서 시간표 기능을 구현하기로 했습니다.
먼저, 기본적인 시간표의 틀은 여기에 좋은 글이 있어서 참고해 만들었습니다.
UI 부분은 링크한 블로그에서 에브리타임과 비슷하게 만들 수 있으니 강의를 추가/삭제하는 기능 구현에 대해 설명드리겠습니다.
이 부분도 만들면서 많은 시행착오가 있었는데, 처음에는 깃헙에서 에브리타임을 클론 코딩한 리포지토리가 있어서 그걸 참고로 개발을 하고 있었습니다.
그런데 이게 테이블을 이용해서 만들었는데, 이렇게 만드니 문제가 한두 가지가 아니었습니다.
이 문제는 나중에 쓰기로 하고 이번에는 위의 블로그대로 UI를 구성한 후, 스택에 포지션 위젯을 쌓으며 강의를 추가하는 로직대로 기능을 구현하려고 합니다.
이전 글에서 만든 강의 정보 데이터는 아래 구현 방법으론 동작하지 않을 것입니다. 아마 데이터를 새로운 시간 규칙으로 다시 매핑해야 동작할 것입니다!
시간표 UI 구조
body: SafeArea(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: (kColumnLength / 2 * kBoxSize) + kFirstColumnHeight,
child: Row(
children: [
buildTimeColumn(),
...buildDayColumn(0),
...buildDayColumn(1),
...buildDayColumn(2),
...buildDayColumn(3),
...buildDayColumn(4),
],
),
),
],
),
),
),일단 UI 구성은 위와 같습니다.
컬럼 안에 로우로 시간 컬럼과 각 요일 컬럼을 추가합니다.
토·일요일은 사용하지 않을 예정이라 5개의 요일 컬럼만 추가했습니다.
시간 컬럼 구현 (buildTimeColumn)
Expanded buildTimeColumn() {
return Expanded(
child: Column(
children: [
SizedBox(
height: kFirstColumnHeight,
),
...List.generate(
kColumnLength.toInt(),
(index) {
if (index % 2 == 0) {
return const Divider(
color: Colors.grey,
height: 0,
);
}
return SizedBox(
height: kBoxSize,
child: Center(child: Text('${index ~/ 2 + 9}')),
);
},
),
],
),
);
}buildTimeColumn 은 이렇게 구현되어 있습니다.
시간표의 시작 시간은 현재 9시로 설정되어 있는데, 이걸 바꾸고 싶으면
Center(child: Text('${index ~/ 2 + 9}')), 이 부분에서 숫자 9를 8로 바꾸면 8시부터 시작합니다.요일 컬럼 구현 (buildDayColumn)
List<Widget> buildDayColumn(int index) {
String currentDay = week[index];
List<Widget> lecturesForTheDay = [];
for (var lecture in selectedLectures) {
for (int i = 0; i < lecture.day.length; i++) {
// 시간 계산: (시작 시간 / 60) * 박스 크기
double top = kFirstColumnHeight + (lecture.start[i] / 60.0) * kBoxSize;
// 높이 계산: (종료 시간 - 시작 시간) / 60 * 박스 크기
double height = ((lecture.end[i] - lecture.start[i]) / 60.0) * kBoxSize;
if (lecture.day[i] == currentDay) {
lecturesForTheDay.add(
Positioned(
top: top,
left: 0,
child: Stack(children: [
GestureDetector(
onTap: () {
setState(() {
selectedLectures.remove(lecture);
setTimetableLength();
});
},
child: Container(
width: MediaQuery.of(context).size.width / 5,
height: height,
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.all(Radius.circular(2)),
),
child: Text(
"${lecture.lname}\n${lecture.classroom[i]}",
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
),
]),
),
);
}
}
}
return [
const VerticalDivider(
color: Colors.grey,
width: 0,
),
Expanded(
flex: 4,
child: Stack(
children: [
Column(
children: [
SizedBox(
height: 20,
child: Text(
'${week[index]}',
),
),
...List.generate(
kColumnLength.toInt(),
(index) {
if (index % 2 == 0) {
return const Divider(
color: Colors.grey,
height: 0,
);
}
return SizedBox(
height: kBoxSize,
child: Container(),
);
},
),
],
),
...lecturesForTheDay, // 현재 요일에 해당하는 모든 강의를 Stack에 추가
],
),
),
];
}buildDayColumn 은 이렇게 구현되어 있습니다. 밑에 설명할 강의 추가 로직을 통해 강의를 추가하면 selectedLectures 리스트에 강의가 담기게 되고,
여기서 요일별로 인덱스가 다른 이 위젯에서 인덱스를 확인한 후 조건에 맞으면 lecturesForTheDay 리스트에 위젯을 추가합니다.
그리고 이 위젯을 스프레드하면 끝! 시간표에 강의가 표시되게 됩니다.
여기서 시간은 Positioned 위젯의 top, 길이(시간 범위)는 컨테이너의 height를 조정해서 표시합니다.
시간표 UI의 한 칸의 길이를 60으로 설정해놨고, 이제 데이터에서 start와 end 값을 받아와서 이용하면 됩니다.
그럼 end - start가 강의 시간의 길이가 되겠죠? 이걸 height로 설정하고, top은 start 값을 받아오면 됩니다.
kFirstColumnHeight 가 뭔지 궁금하실 텐데, 이건 요일을 나타내는 컬럼의 높이를 변수로 빼놓은 것입니다.
setTimetableLength 는 시간표에 들어있는 강의에 따라 시간표의 길이가 조정되는 함수인데,
따로 코드는 적지 않겠습니다. 로직은 꽤 간단한 편입니다.
강의 추가 버튼
showModalBottomSheet(
context: context,
builder: (context) {
ValueNotifier<String> searchTermNotifier = ValueNotifier<String>("");
return SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
onChanged: (value) => searchTermNotifier.value = value,
decoration: const InputDecoration(
labelText: '과목명',
border: OutlineInputBorder(),
),
),
),
],
),
),
Expanded(
child: FutureBuilder<List<LectureSlot>>(
future: loadAllTimeSlots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
}
List<LectureSlot> allSubjects = snapshot.data!;
return ValueListenableBuilder<String>(
valueListenable: searchTermNotifier,
builder: (context, value, child) {
List<LectureSlot> filteredSubjects = allSubjects
.where((subject) => subject.lname.contains(value))
.toList();
return ListView.builder(
itemCount: filteredSubjects.length,
itemBuilder: (context, index) {
return buildLectureWidget(filteredSubjects[index], context);
},
);
},
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
),
],
),
);
},
);위 코드는 강의 추가 버튼을 눌렀을 때 동작하는 코드인데, showModalBottomSheet 를 통해 모달로 된 뷰를 띄웁니다.
뷰 안에는 JSON을 파싱해 강의 정보 데이터를 가져오는 loadAllTimeSlots() 함수를 이용해 리스트로 강의들을 띄웁니다.
결과
시간표가 잘 표시되는 모습
이런 식으로 표시됩니다.
여기의 강의들을 클릭하면 시간표에 강의가 추가됩니다!
시간표 추가가 정상적으로 이루어지는 모습