From 8f711c17064bb397ab49bc02aeec38ceeb1ccb8a Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 4 Nov 2026 16:44:55 +0900 Subject: [PATCH] =?UTF-8?q?refactor(flutter):=20=EA=B3=B5=EA=B3=B5?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AC=EC=A7=84=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=EA=B3=BC=20=EC=B6=9C=EC=B2=98=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=9D=84=20=EA=B3=B5=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 장소 화면도 공공누리 사진과 출처 표기를 써야 해서, 행사 쪽에 있던 것을 shared 로 옮겼다. Type3 사진을 자르지 않는 규칙이 한 위젯(PublicImage)에만 있게 한다. event_models 는 출처 모델을 다시 내보내 기존 import 는 그대로 동작한다. Co-Authored-By: Claude Opus 5.5 --- .../event/data/models/event_models.dart | 39 ++-------- .../presentation/widgets/event_visuals.dart | 46 +++-------- flutter/lib/shared/models/source_models.dart | 36 +++++++++ flutter/lib/shared/widgets/public_image.dart | 76 +++++++++++++++++++ 4 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 flutter/lib/shared/models/source_models.dart create mode 100644 flutter/lib/shared/widgets/public_image.dart diff --git a/flutter/lib/features/event/data/models/event_models.dart b/flutter/lib/features/event/data/models/event_models.dart index f09b737..9f03e06 100644 --- a/flutter/lib/features/event/data/models/event_models.dart +++ b/flutter/lib/features/event/data/models/event_models.dart @@ -1,6 +1,11 @@ /// 축제·공연·행사·전시 — 백엔드 EventListItemDto / EventDetailDto 와 1:1 대응한다. library; +import 'package:wandergo/shared/models/source_models.dart'; + +// 기존 import 를 깨지 않도록 출처 모델을 함께 내보낸다 +export 'package:wandergo/shared/models/source_models.dart'; + /// 행사 분류 (백엔드 EVENT_KINDS) enum EventKind { festival, @@ -17,23 +22,6 @@ enum EventKind { } } -/// 사진 공공누리 유형 — Type3 은 변경 금지(자르지 않고 그대로 보인다) -enum ImageLicense { - type1('Type1'), - type3('Type3'); - - const ImageLicense(this.code); - - final String code; - - static ImageLicense? parse(Object? value) { - for (final license in values) { - if (license.code == value) return license; - } - return null; - } -} - /// 목록의 한 건 class EventItem { const EventItem({ @@ -97,23 +85,6 @@ class EventItem { } } -/// 정보 출처 표기 — 화면에 반드시 보인다 -class Attribution { - const Attribution({required this.provider, this.asOf}); - - final String provider; - - /// 원본 기준 시점 - final String? asOf; - - factory Attribution.fromJson(Map json) { - return Attribution( - provider: json['provider'] as String, - asOf: json['asOf'] as String?, - ); - } -} - /// 상세 — 목록 필드에 소개·시간·링크·출처를 더한다 class EventDetail { const EventDetail({ diff --git a/flutter/lib/features/event/presentation/widgets/event_visuals.dart b/flutter/lib/features/event/presentation/widgets/event_visuals.dart index 79505b6..fa81c3d 100644 --- a/flutter/lib/features/event/presentation/widgets/event_visuals.dart +++ b/flutter/lib/features/event/presentation/widgets/event_visuals.dart @@ -1,11 +1,10 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:wandergo/core/l10n/l10n.dart'; -import 'package:wandergo/core/theme/app_colors.dart'; import 'package:wandergo/core/theme/app_theme.dart'; import 'package:wandergo/core/utils/format.dart'; import 'package:wandergo/features/event/data/models/event_models.dart'; +import 'package:wandergo/shared/widgets/public_image.dart'; extension EventKindLabel on EventKind { String label(AppLocalizations l10n) => switch (this) { @@ -17,9 +16,7 @@ extension EventKindLabel on EventKind { } /// 사진이 없을 때 분류별 자리 표시 색·아이콘 (디자인 캔버스의 자리 표시 톤) -({Color background, Color foreground, IconData icon}) _placeholderOf( - EventKind? kind, -) => switch (kind) { +ImagePlaceholder _placeholderOf(EventKind? kind) => switch (kind) { EventKind.festival => ( background: const Color(0xFFFFE7B8), foreground: const Color(0xFF6E4A00), @@ -51,8 +48,7 @@ String eventMeta(EventItem item) { return parts.join(' · '); } -/// 행사 사진 — 공공누리 Type3 은 자르지 않고(변경 금지) 통째로 보인다. -/// [fit] 을 주지 않으면 Type3 은 contain, 나머지는 cover. +/// 행사 사진 — 사진이 없으면 행사 종류별 자리 표시를 보인다([PublicImage]). class EventImage extends StatelessWidget { const EventImage({ super.key, @@ -75,34 +71,14 @@ class EventImage extends StatelessWidget { @override Widget build(BuildContext context) { - final placeholder = _placeholderOf(kind); - final fallback = ColoredBox( - color: placeholder.background, - child: Center( - child: Icon(placeholder.icon, size: 28, color: placeholder.foreground), - ), - ); - final imageUrl = url; - return ClipRRect( - borderRadius: BorderRadius.circular(radius), - child: SizedBox( - width: width, - height: height, - child: imageUrl == null - ? fallback - : ColoredBox( - color: AppColors.placeholder, - child: CachedNetworkImage( - imageUrl: imageUrl, - fit: - fit ?? - (license == ImageLicense.type3 - ? BoxFit.contain - : BoxFit.cover), - errorWidget: (_, _, _) => fallback, - ), - ), - ), + return PublicImage( + url: url, + license: license, + placeholder: _placeholderOf(kind), + width: width, + height: height, + radius: radius, + fit: fit, ); } } diff --git a/flutter/lib/shared/models/source_models.dart b/flutter/lib/shared/models/source_models.dart new file mode 100644 index 0000000..2aa1264 --- /dev/null +++ b/flutter/lib/shared/models/source_models.dart @@ -0,0 +1,36 @@ +/// 공공데이터 출처 표기 — 행사·장소가 함께 쓴다. +library; + +/// 사진 공공누리 유형 — Type3 은 변경 금지(자르지 않고 그대로 보인다) +enum ImageLicense { + type1('Type1'), + type3('Type3'); + + const ImageLicense(this.code); + + final String code; + + static ImageLicense? parse(Object? value) { + for (final license in values) { + if (license.code == value) return license; + } + return null; + } +} + +/// 정보 출처 표기 — 화면에 반드시 보인다 +class Attribution { + const Attribution({required this.provider, this.asOf}); + + final String provider; + + /// 원본 기준 시점 — 'YYYY-MM-DD' 또는 상가정보 기준년월 'YYYYMM' + final String? asOf; + + factory Attribution.fromJson(Map json) { + return Attribution( + provider: json['provider'] as String, + asOf: json['asOf'] as String?, + ); + } +} diff --git a/flutter/lib/shared/widgets/public_image.dart b/flutter/lib/shared/widgets/public_image.dart new file mode 100644 index 0000000..92244b4 --- /dev/null +++ b/flutter/lib/shared/widgets/public_image.dart @@ -0,0 +1,76 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +import 'package:wandergo/core/theme/app_colors.dart'; +import 'package:wandergo/core/theme/app_theme.dart'; +import 'package:wandergo/shared/models/source_models.dart'; + +/// 사진이 없거나 못 불러올 때의 자리 표시 — 분류별 배경색·아이콘 +typedef ImagePlaceholder = ({ + Color background, + Color foreground, + IconData icon, +}); + +/// 공공데이터 사진 — 공공누리 Type3 은 자르지 않고(변경 금지) 통째로 보인다. +/// [fit] 을 주지 않으면 Type3 은 contain, 나머지는 cover. +class PublicImage extends StatelessWidget { + const PublicImage({ + super.key, + required this.url, + required this.license, + required this.placeholder, + this.width, + this.height, + this.radius = kRadius, + this.fit, + this.iconSize = 28, + }); + + final String? url; + final ImageLicense? license; + final ImagePlaceholder placeholder; + final double? width; + final double? height; + final double radius; + final BoxFit? fit; + final double iconSize; + + @override + Widget build(BuildContext context) { + final fallback = ColoredBox( + color: placeholder.background, + child: Center( + child: Icon( + placeholder.icon, + size: iconSize, + color: placeholder.foreground, + ), + ), + ); + final imageUrl = url; + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: width, + height: height, + child: imageUrl == null + ? fallback + : ColoredBox( + color: AppColors.placeholder, + child: CachedNetworkImage( + imageUrl: imageUrl, + width: width, + height: height, + fit: + fit ?? + (license == ImageLicense.type3 + ? BoxFit.contain + : BoxFit.cover), + errorWidget: (_, _, _) => fallback, + ), + ), + ), + ); + } +}