feat(app): 일정 화면 새 API 대응 및 생일/데뷔 카드 추가
- Schedule 모델: dynamic id로 변경 (생일/기념일 문자열 ID 지원) - 생일/데뷔/기념일 특별 필드 추가 (isBirthday, isDebut 등) - BirthdayCard: 핑크-보라 그라데이션, 멤버 사진, 케이크 이모지 - DebutCard: 블루 그라데이션, DEBUT/N YEARS 아이콘, 별 장식 - 일정 목록에서 특별 일정 카드 자동 분기 렌더링 - 특별 일정 클릭 시 상세 라우팅 방지 - 달력 그리드 그림자 클리핑 수정 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6ef851b02
commit
6284d216bd
3 changed files with 301 additions and 12 deletions
|
|
@ -112,7 +112,8 @@ class ScheduleDetail {
|
|||
}
|
||||
|
||||
class Schedule {
|
||||
final int id;
|
||||
/// ID (일반 일정: int, 생일/기념일: String)
|
||||
final dynamic id;
|
||||
final String title;
|
||||
final String date;
|
||||
final String? time;
|
||||
|
|
@ -122,6 +123,12 @@ class Schedule {
|
|||
final List<String> members;
|
||||
final String? sourceName;
|
||||
final String? sourceUrl;
|
||||
// 특별 일정 필드
|
||||
final bool isBirthday;
|
||||
final bool isDebut;
|
||||
final bool isAnniversary;
|
||||
final String? memberImage;
|
||||
final int? anniversaryYear;
|
||||
|
||||
Schedule({
|
||||
required this.id,
|
||||
|
|
@ -134,6 +141,11 @@ class Schedule {
|
|||
this.members = const [],
|
||||
this.sourceName,
|
||||
this.sourceUrl,
|
||||
this.isBirthday = false,
|
||||
this.isDebut = false,
|
||||
this.isAnniversary = false,
|
||||
this.memberImage,
|
||||
this.anniversaryYear,
|
||||
});
|
||||
|
||||
factory Schedule.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -149,7 +161,7 @@ class Schedule {
|
|||
final source = json['source'] as Map<String, dynamic>?;
|
||||
|
||||
return Schedule(
|
||||
id: json['id'] is String ? int.parse(json['id']) : json['id'] as int,
|
||||
id: json['id'], // int 또는 String (생일/기념일)
|
||||
title: json['title'] as String,
|
||||
date: json['date'] as String,
|
||||
time: json['time'] as String?,
|
||||
|
|
@ -159,9 +171,20 @@ class Schedule {
|
|||
members: membersList,
|
||||
sourceName: source?['name'] as String?,
|
||||
sourceUrl: source?['url'] as String?,
|
||||
isBirthday: json['is_birthday'] == true,
|
||||
isDebut: json['is_debut'] == true,
|
||||
isAnniversary: json['is_anniversary'] == true,
|
||||
memberImage: json['member_image'] as String?,
|
||||
anniversaryYear: (json['anniversary_year'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 특별 일정 여부 (생일, 데뷔, 기념일)
|
||||
bool get isSpecial => isBirthday || isDebut || isAnniversary;
|
||||
|
||||
/// 일반 일정의 int ID (특별 일정은 null)
|
||||
int? get numericId => id is int ? id : null;
|
||||
|
||||
/// 멤버 리스트 반환
|
||||
List<String> get memberList => members;
|
||||
|
||||
|
|
|
|||
|
|
@ -779,7 +779,7 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
|||
bottom: index < searchState.results.length - 1 ? 12 : 0,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => context.push('/schedule/${schedule.id}'),
|
||||
onTap: schedule.isSpecial ? null : () => context.push('/schedule/${schedule.id}'),
|
||||
child: SearchScheduleCard(
|
||||
schedule: schedule,
|
||||
categoryColor: parseColor(schedule.categoryColor),
|
||||
|
|
@ -1370,7 +1370,8 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
|||
}) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
clipBehavior: Clip.none,
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
|
|
@ -1409,9 +1410,8 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
|||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withValues(alpha: 0.4),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 1,
|
||||
color: AppColors.primary.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
|
|
@ -1581,11 +1581,15 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
|||
itemCount: state.selectedDateSchedules.length,
|
||||
itemBuilder: (context, index) {
|
||||
final schedule = state.selectedDateSchedules[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: index < state.selectedDateSchedules.length - 1 ? 12 : 0,
|
||||
),
|
||||
child: GestureDetector(
|
||||
|
||||
// 특별 일정 카드 (생일/데뷔/기념일)
|
||||
Widget card;
|
||||
if (schedule.isBirthday) {
|
||||
card = BirthdayCard(schedule: schedule);
|
||||
} else if (schedule.isDebut || schedule.isAnniversary) {
|
||||
card = DebutCard(schedule: schedule);
|
||||
} else {
|
||||
card = GestureDetector(
|
||||
onTap: () => context.push('/schedule/${schedule.id}'),
|
||||
child: AnimatedScheduleCard(
|
||||
key: ValueKey('${schedule.id}_${state.selectedDate.toString()}'),
|
||||
|
|
@ -1593,7 +1597,14 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
|
|||
schedule: schedule,
|
||||
categoryColor: parseColor(schedule.categoryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: index < state.selectedDateSchedules.length - 1 ? 12 : 0,
|
||||
),
|
||||
child: card,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/// 일정 카드 위젯
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/constants.dart';
|
||||
import '../../../models/schedule.dart';
|
||||
|
|
@ -250,3 +251,257 @@ class ScheduleCard extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 생일 카드 위젯
|
||||
class BirthdayCard extends StatelessWidget {
|
||||
final Schedule schedule;
|
||||
|
||||
const BirthdayCard({super.key, required this.schedule});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFF472B6), Color(0xFFA855F7), Color(0xFF6366F1)],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFA855F7).withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 배경 장식
|
||||
Positioned(
|
||||
top: -12, right: -12,
|
||||
child: Container(
|
||||
width: 64, height: 64,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -16, left: -16,
|
||||
child: Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
bottom: 12, left: 32,
|
||||
child: Text('🎉', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
// 내용
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 멤버 사진
|
||||
if (schedule.memberImage != null)
|
||||
Container(
|
||||
width: 56, height: 56,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: schedule.memberImage!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(color: Colors.white),
|
||||
),
|
||||
),
|
||||
// 제목
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('🎂', style: TextStyle(fontSize: 24)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
decodeHtmlEntities(schedule.title),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데뷔/기념일 카드 위젯
|
||||
class DebutCard extends StatelessWidget {
|
||||
final Schedule schedule;
|
||||
|
||||
const DebutCard({super.key, required this.schedule});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF7a99c8), Color(0xFF98b0d8), Color(0xFFb8c8e8)],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF7a99c8).withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 배경 장식
|
||||
Positioned(
|
||||
top: -24, left: -24,
|
||||
child: Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -32, right: -32,
|
||||
child: Container(
|
||||
width: 96, height: 96,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 반짝이는 별
|
||||
Positioned(
|
||||
top: 8, right: 16,
|
||||
child: Text('✦', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.6))),
|
||||
),
|
||||
Positioned(
|
||||
top: 16, right: 48,
|
||||
child: Text('✦', style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: 0.4))),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 12, right: 24,
|
||||
child: Text('✦', style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.5))),
|
||||
),
|
||||
// 내용
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 아이콘 영역
|
||||
Container(
|
||||
width: 56, height: 56,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
child: Center(
|
||||
child: schedule.isDebut
|
||||
? const Text(
|
||||
'DEBUT',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${schedule.anniversaryYear}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'YEARS',
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 제목
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
schedule.isDebut ? '🍀' : '☘️',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
schedule.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue