feat(app): 홈 화면 일정 API를 새 응답 형식에 맞게 업데이트
- Schedule 모델: category/source 중첩 객체, members 배열 파싱
- 일정 서비스: { schedules: [] } 래핑된 응답 처리
- 홈/웹 일정 카드: sourceName을 별도 줄로 분리하여 표시
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9d18449d3a
commit
0ddde32bed
4 changed files with 56 additions and 38 deletions
|
|
@ -116,52 +116,54 @@ class Schedule {
|
||||||
final String title;
|
final String title;
|
||||||
final String date;
|
final String date;
|
||||||
final String? time;
|
final String? time;
|
||||||
final String? endDate;
|
final int? categoryId;
|
||||||
final String? endTime;
|
|
||||||
final String? description;
|
|
||||||
final String? categoryName;
|
final String? categoryName;
|
||||||
final String? categoryColor;
|
final String? categoryColor;
|
||||||
final String? memberNames;
|
final List<String> members;
|
||||||
final String? sourceUrl;
|
|
||||||
final String? sourceName;
|
final String? sourceName;
|
||||||
|
final String? sourceUrl;
|
||||||
|
|
||||||
Schedule({
|
Schedule({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.date,
|
required this.date,
|
||||||
this.time,
|
this.time,
|
||||||
this.endDate,
|
this.categoryId,
|
||||||
this.endTime,
|
|
||||||
this.description,
|
|
||||||
this.categoryName,
|
this.categoryName,
|
||||||
this.categoryColor,
|
this.categoryColor,
|
||||||
this.memberNames,
|
this.members = const [],
|
||||||
this.sourceUrl,
|
|
||||||
this.sourceName,
|
this.sourceName,
|
||||||
|
this.sourceUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Schedule.fromJson(Map<String, dynamic> json) {
|
factory Schedule.fromJson(Map<String, dynamic> json) {
|
||||||
|
// category 중첩 객체 파싱
|
||||||
|
final category = json['category'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
// members 배열 파싱
|
||||||
|
final membersList = (json['members'] as List<dynamic>?)
|
||||||
|
?.map((m) => m is String ? m : m.toString())
|
||||||
|
.toList() ?? [];
|
||||||
|
|
||||||
|
// source 중첩 객체 파싱
|
||||||
|
final source = json['source'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
return Schedule(
|
return Schedule(
|
||||||
id: json['id'] as int,
|
id: json['id'] is String ? int.parse(json['id']) : json['id'] as int,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
date: json['date'] as String,
|
date: json['date'] as String,
|
||||||
time: json['time'] as String?,
|
time: json['time'] as String?,
|
||||||
endDate: json['end_date'] as String?,
|
categoryId: category?['id'] as int?,
|
||||||
endTime: json['end_time'] as String?,
|
categoryName: category?['name'] as String?,
|
||||||
description: json['description'] as String?,
|
categoryColor: category?['color'] as String?,
|
||||||
categoryName: json['category_name'] as String?,
|
members: membersList,
|
||||||
categoryColor: json['category_color'] as String?,
|
sourceName: source?['name'] as String?,
|
||||||
memberNames: json['member_names'] as String?,
|
sourceUrl: source?['url'] as String?,
|
||||||
sourceUrl: json['source_url'] as String?,
|
|
||||||
sourceName: json['source_name'] as String?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 멤버 리스트 반환
|
/// 멤버 리스트 반환
|
||||||
List<String> get memberList {
|
List<String> get memberList => members;
|
||||||
if (memberNames == null || memberNames!.isEmpty) return [];
|
|
||||||
return memberNames!.split(',').map((n) => n.trim()).where((n) => n.isNotEmpty).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 시간 포맷 (HH:mm)
|
/// 시간 포맷 (HH:mm)
|
||||||
String? get formattedTime {
|
String? get formattedTime {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ Future<List<Schedule>> getSchedules(int year, int month) async {
|
||||||
'year': year.toString(),
|
'year': year.toString(),
|
||||||
'month': month.toString(),
|
'month': month.toString(),
|
||||||
});
|
});
|
||||||
final List<dynamic> data = response.data;
|
final Map<String, dynamic> data = response.data;
|
||||||
return data.map((json) => Schedule.fromJson(json)).toList();
|
final List<dynamic> schedulesJson = data['schedules'] ?? [];
|
||||||
|
return schedulesJson.map((json) => Schedule.fromJson(json)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 다가오는 일정 N개 조회 (오늘 이후) - 웹과 동일
|
/// 다가오는 일정 N개 조회 (오늘 이후) - 웹과 동일
|
||||||
|
|
@ -28,8 +29,9 @@ Future<List<Schedule>> getUpcomingSchedules(int limit) async {
|
||||||
'startDate': todayStr,
|
'startDate': todayStr,
|
||||||
'limit': limit.toString(),
|
'limit': limit.toString(),
|
||||||
});
|
});
|
||||||
final List<dynamic> data = response.data;
|
final Map<String, dynamic> data = response.data;
|
||||||
return data.map((json) => Schedule.fromJson(json)).toList();
|
final List<dynamic> schedulesJson = data['schedules'] ?? [];
|
||||||
|
return schedulesJson.map((json) => Schedule.fromJson(json)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 일정 검색 결과
|
/// 일정 검색 결과
|
||||||
|
|
|
||||||
|
|
@ -637,13 +637,11 @@ class _HomeViewState extends ConsumerState<HomeView> with TickerProviderStateMix
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
// mt-2(8px) text-xs(12px) text-gray-400
|
// mt-2(8px) text-xs(12px) text-gray-400 - 시간 + 카테고리
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// gap-3(12px) 사이
|
|
||||||
if (schedule.formattedTime != null) ...[
|
if (schedule.formattedTime != null) ...[
|
||||||
// gap-1(4px)
|
|
||||||
_buildIcon('clock', 12, Colors.grey[400]!),
|
_buildIcon('clock', 12, Colors.grey[400]!),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -662,6 +660,20 @@ class _HomeViewState extends ConsumerState<HomeView> with TickerProviderStateMix
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// 소스 (별도 줄)
|
||||||
|
if (schedule.sourceName != null && schedule.sourceName!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildIcon('link', 12, Colors.grey[400]!),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
schedule.sourceName!,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
// mt-2(8px)
|
// mt-2(8px)
|
||||||
if (memberList.isNotEmpty) ...[
|
if (memberList.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -704,6 +716,7 @@ class _HomeViewState extends ConsumerState<HomeView> with TickerProviderStateMix
|
||||||
const icons = {
|
const icons = {
|
||||||
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
||||||
'tag': '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>',
|
'tag': '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>',
|
||||||
|
'link': '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
||||||
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
|
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className =
|
||||||
<p className="font-semibold text-sm text-gray-800 line-clamp-2 leading-snug">
|
<p className="font-semibold text-sm text-gray-800 line-clamp-2 leading-snug">
|
||||||
{decodeHtmlEntities(schedule.title)}
|
{decodeHtmlEntities(schedule.title)}
|
||||||
</p>
|
</p>
|
||||||
{/* 시간 + 카테고리 + 소스 */}
|
{/* 시간 + 카테고리 */}
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs text-gray-400">
|
<div className="flex items-center gap-3 mt-2 text-xs text-gray-400">
|
||||||
{timeStr && (
|
{timeStr && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
|
|
@ -72,13 +72,14 @@ const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className =
|
||||||
{categoryInfo.name}
|
{categoryInfo.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{sourceName && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Link2 size={12} />
|
|
||||||
{sourceName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* 소스 */}
|
||||||
|
{sourceName && (
|
||||||
|
<div className="flex items-center gap-1 mt-1.5 text-xs text-gray-400">
|
||||||
|
<Link2 size={12} />
|
||||||
|
{sourceName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 멤버 */}
|
{/* 멤버 */}
|
||||||
{displayMembers.length > 0 && (
|
{displayMembers.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue