Skip to content

Commit 42687fb

Browse files
authored
Merge pull request #162 from dpplanner/feat/159-예약-색깔-커스텀-기능-추가
Feat/159 예약 색깔 커스텀 기능 추가
2 parents 018e3be + 8fb10b8 commit 42687fb

File tree

7 files changed

+333
-8
lines changed

7 files changed

+333
-8
lines changed

dplanner/lib/const/style.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,39 @@ class AppColor extends Color {
1717
static const lockColor = Color(0xFFCDCDCD);
1818
static const markColor = Color(0xFFFF443D);
1919
static const hyperLink = Colors.blueAccent;
20+
21+
static const List<Color> reservationColors = [
22+
Color(0xFFA294DB),
23+
Color(0xFFFF443D),
24+
Color(0xFFFFAE3D),
25+
Color(0xFF46D878),
26+
Color(0xFF3DDFFF),
27+
Color(0xFF3DA2FF),
28+
Color(0xFF6168AA),
29+
Color(0xFF80785E),
30+
Color(0xFFD9458C),
31+
Color(0xFFD9908D),
32+
Color(0xFF961E1E),
33+
];
34+
35+
static String getColorHex(Color color) {
36+
// 색상의 ARGB 값을 16진수 문자열로 변환
37+
final hexColor = color.value.toRadixString(16).padLeft(8, '0').toUpperCase();
38+
39+
// '0xFF'를 제외한 6자리 색상 코드만 추출
40+
return hexColor.substring(2);
41+
}
42+
43+
static Color ofHex(String hexCode) {
44+
// 입력 색상 코드가 6자리인지 확인
45+
if (hexCode.length != 6) {
46+
throw ArgumentError('색상 코드는 6자리여야 합니다.');
47+
}
48+
49+
// '0xFF'를 붙여서 ARGB 형식으로 변환
50+
final colorString = 'FF$hexCode';
51+
52+
// 16진수 문자열을 정수로 변환하고 Color 객체를 생성
53+
return reservationColors.firstWhere((color) => color == Color(int.parse(colorString, radix: 16)));
54+
}
2055
}

dplanner/lib/models/reservation_model.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class ReservationModel {
44
String clubMemberName,
55
resourceName,
66
title,
7+
color,
78
usage,
89
status,
910
startDateTime,
@@ -22,6 +23,7 @@ class ReservationModel {
2223
required this.resourceId,
2324
required this.resourceName,
2425
required this.title,
26+
required this.color,
2527
required this.usage,
2628
required this.sharing,
2729
required this.status,
@@ -42,6 +44,7 @@ class ReservationModel {
4244
resourceId = json['resourceId'],
4345
resourceName = json['resourceName'],
4446
title = json['title'],
47+
color = json['color'],
4548
usage = json['usage'],
4649
sharing = json['sharing'],
4750
status = json['status'],
@@ -65,6 +68,7 @@ class ReservationModel {
6568
resourceId: -1,
6669
resourceName: "",
6770
title: "",
71+
color: "",
6872
usage: "",
6973
sharing: false,
7074
status: "",

dplanner/lib/pages/club_timetable_page.dart

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:dplanner/models/reservation_model.dart';
88
import 'package:dplanner/pages/loading_page.dart';
99
import 'package:dplanner/services/lock_api_service.dart';
1010
import 'package:dplanner/const/style.dart';
11+
import 'package:dplanner/widgets/color_unit_widget.dart';
1112
import 'package:flutter/cupertino.dart';
1213
import 'package:flutter/material.dart';
1314
import 'package:calendar_view/calendar_view.dart';
@@ -27,6 +28,7 @@ import '../services/reservation_api_service.dart';
2728
import '../services/resource_api_service.dart';
2829
import '../widgets/banner_ad_widget.dart';
2930
import '../widgets/bottom_bar.dart';
31+
import '../widgets/color_scroll_widget.dart';
3032
import '../widgets/full_screen_image.dart';
3133
import '../widgets/nextpage_button.dart';
3234
import '../widgets/outline_textform.dart';
@@ -158,14 +160,18 @@ class _ClubTimetablePageState extends State<ClubTimetablePage> {
158160
? (i.clubMemberId == MemberController.to.clubMember().id
159161
? AppColor.subColor2
160162
: AppColor.subColor4)
161-
: (i.clubMemberId == MemberController.to.clubMember().id ||
162-
i.invitees.any((element) =>
163-
element["clubMemberId"] ==
164-
MemberController.to.clubMember().id &&
165-
element["clubMemberName"] ==
166-
MemberController.to.clubMember().name))
167-
? AppColor.subColor1
168-
: AppColor.subColor3));
163+
: AppColor.ofHex(i.color) // 승인된 예약만 설정한대로
164+
165+
// 나중에 쓸수도 있어서 남겨둠(나와 관련된 예약 하이라이트 같은 기능)
166+
// : (i.clubMemberId == MemberController.to.clubMember().id ||
167+
// i.invitees.any((element) =>
168+
// element["clubMemberId"] ==
169+
// MemberController.to.clubMember().id &&
170+
// element["clubMemberName"] ==
171+
// MemberController.to.clubMember().name))
172+
// ? AppColor.subColor1
173+
// : AppColor.subColor3
174+
));
169175
}
170176

171177
for (var i in locks) {
@@ -914,6 +920,10 @@ class _ClubTimetablePageState extends State<ClubTimetablePage> {
914920
startOfWeek = standardDay.subtract(Duration(days: weekday - 1));
915921
String dateOfLock = DateFormat('yyyy년 MM월').format(startOfWeek);
916922

923+
Color selectedColor = reservation == null
924+
? AppColor.reservationColors[0]
925+
: AppColor.ofHex(reservation.color);
926+
917927
if (types == 3) {
918928
reservationTime = DateTime.parse(reservation!.startDateTime);
919929
focusedDay = reservationTime;
@@ -1443,6 +1453,33 @@ class _ClubTimetablePageState extends State<ClubTimetablePage> {
14431453
)),
14441454
],
14451455
),
1456+
Padding(
1457+
padding: const EdgeInsets.only(top: 32.0),
1458+
child : Row(
1459+
mainAxisAlignment:MainAxisAlignment.spaceBetween,
1460+
children: [
1461+
const Text(
1462+
"예약 색상",
1463+
style: TextStyle(
1464+
fontWeight: FontWeight.w700,
1465+
fontSize: 16),
1466+
),
1467+
types == 3
1468+
? ColorUnitWidget(
1469+
color: selectedColor,
1470+
showBorder: true,
1471+
borderWidth: 5.0)
1472+
: ColorScrollWidget(
1473+
defaultColor: selectedColor,
1474+
availableColors: AppColor.reservationColors,
1475+
onColorChanged: (color) {
1476+
setState(() {
1477+
selectedColor = color;
1478+
});
1479+
})
1480+
]
1481+
)
1482+
),
14461483
Visibility(
14471484
visible: types == 3,
14481485
replacement: Column(
@@ -3219,6 +3256,7 @@ class _ClubTimetablePageState extends State<ClubTimetablePage> {
32193256
reservationOwnerId: owner['clubMemberId'],
32203257
resourceId: selectedValue!.id,
32213258
title: title.text,
3259+
color: AppColor.getColorHex(selectedColor),
32223260
usage: usage.text,
32233261
sharing: (open == Open.yes) ? true : false,
32243262
startDateTime: startDateTime,
@@ -3229,6 +3267,7 @@ class _ClubTimetablePageState extends State<ClubTimetablePage> {
32293267
reservationId: reservation!.reservationId,
32303268
resourceId: selectedValue!.id,
32313269
title: title.text,
3270+
color: AppColor.getColorHex(selectedColor),
32323271
usage: usage.text,
32333272
sharing: (open == Open.yes) ? true : false,
32343273
startDateTime: startDateTime,

dplanner/lib/services/reservation_api_service.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class ReservationApiService {
4545
{required int reservationOwnerId,
4646
required int resourceId,
4747
required String title,
48+
required String color,
4849
required String usage,
4950
required bool sharing,
5051
required String startDateTime,
@@ -65,6 +66,7 @@ class ReservationApiService {
6566
"reservationOwnerId": reservationOwnerId,
6667
"resourceId": resourceId,
6768
"title": title,
69+
"color": color,
6870
"usage": usage,
6971
"sharing": sharing,
7072
"startDateTime": startDateTime,
@@ -260,6 +262,7 @@ class ReservationApiService {
260262
{required int reservationId,
261263
required int resourceId,
262264
required String title,
265+
required String color,
263266
required String usage,
264267
required bool sharing,
265268
required String startDateTime,
@@ -278,6 +281,7 @@ class ReservationApiService {
278281
body: jsonEncode({
279282
"resourceId": resourceId,
280283
"title": title,
284+
"color": color,
281285
"usage": usage,
282286
"sharing": sharing,
283287
"startDateTime": startDateTime,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import 'package:dplanner/widgets/color_unit_widget.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/services.dart';
4+
5+
import '../const/style.dart';
6+
7+
class ColorScrollWidget extends StatefulWidget {
8+
final Color defaultColor;
9+
final List<Color> availableColors;
10+
final ValueChanged<Color> onColorChanged; // 색상이 변경될 때 호출될 콜백
11+
12+
const ColorScrollWidget({
13+
super.key,
14+
required this.defaultColor,
15+
required this.availableColors,
16+
required this.onColorChanged, // 초기 색상 설정은 필요 없으므로 제거
17+
});
18+
19+
@override
20+
State<ColorScrollWidget> createState() => _ColorScrollWidgetState();
21+
}
22+
23+
class _ColorScrollWidgetState extends State<ColorScrollWidget> {
24+
static const double circleSize = ColorUnitWidget.circleSize;
25+
static const double circlePadding = 16.0;
26+
static const double indicatorWidth = 180.0; // 5개의 원이 보여질 정도의 가로 길이
27+
static const double scrollBarHeight = 4.0; // 스크롤바 높이
28+
static const Duration animationDuration = Duration(milliseconds: 300); // 스크롤 애니메이션 지속 시간
29+
30+
List<Color> colors = []; // 실제로 사용할 색상 리스트
31+
32+
final ScrollController _scrollController = ScrollController();
33+
int selectedIndex = 2; // 초기 선택된 색상 인덱스
34+
35+
@override
36+
void initState() {
37+
super.initState();
38+
WidgetsBinding.instance.addPostFrameCallback((_) {
39+
setState(() {
40+
colors.addAll([
41+
Colors.transparent,
42+
Colors.transparent,
43+
...widget.availableColors,
44+
Colors.transparent,
45+
Colors.transparent]);
46+
});
47+
selectedIndex = _getColorIndex(widget.defaultColor); // 기본 색상 인덱스 설정
48+
_centerScrollOnSelected();
49+
});
50+
}
51+
52+
@override
53+
void dispose() {
54+
_scrollController.dispose();
55+
super.dispose();
56+
}
57+
58+
int _getColorIndex(Color color) {
59+
// 기본 색상에 대한 인덱스를 반환합니다.
60+
int index = colors.indexOf(color);
61+
if (index == -1) {
62+
// 색상이 colors 목록에 없으면 기본 색상 인덱스를 설정하지 않습니다.
63+
return 2; // 기본 색상 인덱스
64+
}
65+
return index;
66+
}
67+
68+
void _centerScrollOnSelected() {
69+
// 선택된 색상이 화면 중앙에 오도록 스크롤 위치를 설정합니다.
70+
double targetOffset = selectedIndex * (circleSize + circlePadding) - (indicatorWidth / 2) + circleSize / 2;
71+
_scrollController.jumpTo(targetOffset);
72+
}
73+
74+
void _onScroll() {
75+
// 스크롤 위치를 기반으로 인디케이터의 중심에 가장 가까운 색상 인덱스를 계산합니다.
76+
double offset = _scrollController.offset + (indicatorWidth / 2);
77+
int newIndex = (offset / (circleSize + circlePadding)).round();
78+
79+
// 색상이 인디케이터의 중심에 가장 가깝도록 선택합니다.
80+
double centerOffset = (circleSize + circlePadding) * newIndex;
81+
double indicatorCenter = _scrollController.offset + (indicatorWidth / 2);
82+
double colorCenter = centerOffset + circleSize / 2;
83+
84+
if ((indicatorCenter - colorCenter).abs() < (circleSize + circlePadding) / 2) {
85+
// 색상 인덱스를 실제 색상 범위로 제한합니다.
86+
if (newIndex < 2) newIndex = 2; // 첫 번째 색상 (두 번째 인덱스)으로 제한
87+
if (newIndex >= colors.length - 2) newIndex = colors.length - 3; // 마지막 색상 (마지막에서 두 번째 인덱스)으로 제한
88+
89+
if (newIndex != selectedIndex && newIndex >= 0 && newIndex < colors.length) {
90+
setState(() {
91+
selectedIndex = newIndex;
92+
HapticFeedback.selectionClick();
93+
widget.onColorChanged(selectedColor);
94+
});
95+
}
96+
}
97+
}
98+
99+
void _onColorTap(int index) async {
100+
// 사용자가 특정 색상을 탭했을 때 해당 색상이 중앙에 오도록 부드럽게 스크롤합니다.
101+
int adjustedIndex = index;
102+
if (index < 2) adjustedIndex = 2; // 첫 번째 색상 (두 번째 인덱스)으로 제한
103+
if (index >= colors.length - 2) adjustedIndex = colors.length - 3; // 마지막 색상 (마지막에서 두 번째 인덱스)으로 제한
104+
105+
setState(() {
106+
selectedIndex = adjustedIndex;
107+
});
108+
109+
double targetOffset = adjustedIndex * (circleSize + circlePadding) - (indicatorWidth / 2) + circleSize / 2;
110+
111+
await _scrollController.animateTo(
112+
targetOffset,
113+
duration: animationDuration,
114+
curve: Curves.easeInOut,
115+
);
116+
117+
HapticFeedback.selectionClick();
118+
widget.onColorChanged(selectedColor);
119+
}
120+
121+
Color get selectedColor {
122+
if (selectedIndex == 0) {
123+
return AppColor.reservationColors.first; // 첫 번째 더미 색상 선택 시 첫 번째 실제 색상 반환
124+
} else if (selectedIndex == colors.length - 1) {
125+
return AppColor.reservationColors.last; // 마지막 더미 색상 선택 시 마지막 실제 색상 반환
126+
} else {
127+
return colors[selectedIndex]; // 그 외에는 실제 선택된 색상 반환
128+
}
129+
}
130+
131+
@override
132+
Widget build(BuildContext context) {
133+
return Column(
134+
mainAxisSize: MainAxisSize.min,
135+
children: [
136+
Stack(
137+
alignment: Alignment.center,
138+
children: [
139+
SizedBox(
140+
width: indicatorWidth,
141+
height: circleSize + scrollBarHeight,
142+
child: NotificationListener<ScrollNotification>(
143+
onNotification: (scrollNotification) {
144+
if (scrollNotification is ScrollUpdateNotification) {
145+
_onScroll();
146+
}
147+
return true;
148+
},
149+
child: ListView.builder(
150+
controller: _scrollController,
151+
scrollDirection: Axis.horizontal,
152+
itemCount: colors.length,
153+
itemBuilder: (context, index) {
154+
return GestureDetector(
155+
onTap: () => _onColorTap(index),
156+
child: Padding(
157+
padding: const EdgeInsets.symmetric(horizontal: circlePadding / 2),
158+
child: ColorUnitWidget(
159+
color: colors[index],
160+
showBorder: index >= 2 && index < 2 + widget.availableColors.length,
161+
borderWidth: selectedIndex == index ? 0 : 5,
162+
)
163+
),
164+
);
165+
},
166+
),
167+
),
168+
),
169+
],
170+
),
171+
],
172+
);
173+
}
174+
}

0 commit comments

Comments
 (0)