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 date;
|
||||
final String? time;
|
||||
final String? endDate;
|
||||
final String? endTime;
|
||||
final String? description;
|
||||
final int? categoryId;
|
||||
final String? categoryName;
|
||||
final String? categoryColor;
|
||||
final String? memberNames;
|
||||
final String? sourceUrl;
|
||||
final List<String> members;
|
||||
final String? sourceName;
|
||||
final String? sourceUrl;
|
||||
|
||||
Schedule({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.date,
|
||||
this.time,
|
||||
this.endDate,
|
||||
this.endTime,
|
||||
this.description,
|
||||
this.categoryId,
|
||||
this.categoryName,
|
||||
this.categoryColor,
|
||||
this.memberNames,
|
||||
this.sourceUrl,
|
||||
this.members = const [],
|
||||
this.sourceName,
|
||||
this.sourceUrl,
|
||||
});
|
||||
|
||||
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(
|
||||
id: json['id'] as int,
|
||||
id: json['id'] is String ? int.parse(json['id']) : json['id'] as int,
|
||||
title: json['title'] as String,
|
||||
date: json['date'] as String,
|
||||
time: json['time'] as String?,
|
||||
endDate: json['end_date'] as String?,
|
||||
endTime: json['end_time'] as String?,
|
||||
description: json['description'] as String?,
|
||||
categoryName: json['category_name'] as String?,
|
||||
categoryColor: json['category_color'] as String?,
|
||||
memberNames: json['member_names'] as String?,
|
||||
sourceUrl: json['source_url'] as String?,
|
||||
sourceName: json['source_name'] as String?,
|
||||
categoryId: category?['id'] as int?,
|
||||
categoryName: category?['name'] as String?,
|
||||
categoryColor: category?['color'] as String?,
|
||||
members: membersList,
|
||||
sourceName: source?['name'] as String?,
|
||||
sourceUrl: source?['url'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 멤버 리스트 반환
|
||||
List<String> get memberList {
|
||||
if (memberNames == null || memberNames!.isEmpty) return [];
|
||||
return memberNames!.split(',').map((n) => n.trim()).where((n) => n.isNotEmpty).toList();
|
||||
}
|
||||
List<String> get memberList => members;
|
||||
|
||||
/// 시간 포맷 (HH:mm)
|
||||
String? get formattedTime {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ Future<List<Schedule>> getSchedules(int year, int month) async {
|
|||
'year': year.toString(),
|
||||
'month': month.toString(),
|
||||
});
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((json) => Schedule.fromJson(json)).toList();
|
||||
final Map<String, dynamic> data = response.data;
|
||||
final List<dynamic> schedulesJson = data['schedules'] ?? [];
|
||||
return schedulesJson.map((json) => Schedule.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
/// 다가오는 일정 N개 조회 (오늘 이후) - 웹과 동일
|
||||
|
|
@ -28,8 +29,9 @@ Future<List<Schedule>> getUpcomingSchedules(int limit) async {
|
|||
'startDate': todayStr,
|
||||
'limit': limit.toString(),
|
||||
});
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((json) => Schedule.fromJson(json)).toList();
|
||||
final Map<String, dynamic> data = response.data;
|
||||
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,
|
||||
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),
|
||||
Row(
|
||||
children: [
|
||||
// gap-3(12px) 사이
|
||||
if (schedule.formattedTime != null) ...[
|
||||
// gap-1(4px)
|
||||
_buildIcon('clock', 12, Colors.grey[400]!),
|
||||
const SizedBox(width: 4),
|
||||
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)
|
||||
if (memberList.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -704,6 +716,7 @@ class _HomeViewState extends ConsumerState<HomeView> with TickerProviderStateMix
|
|||
const icons = {
|
||||
'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"/>',
|
||||
'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"/>',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{decodeHtmlEntities(schedule.title)}
|
||||
</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 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
|
|
@ -72,13 +72,14 @@ const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className =
|
|||
{categoryInfo.name}
|
||||
</span>
|
||||
)}
|
||||
{sourceName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Link2 size={12} />
|
||||
{sourceName}
|
||||
</span>
|
||||
)}
|
||||
</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 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue