refactor(flutter): 공공데이터 사진 위젯과 출처 모델을 공용으로 분리

장소 화면도 공공누리 사진과 출처 표기를 써야 해서, 행사 쪽에 있던 것을
shared 로 옮겼다. Type3 사진을 자르지 않는 규칙이 한 위젯(PublicImage)에만 있게 한다.
event_models 는 출처 모델을 다시 내보내 기존 import 는 그대로 동작한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 16:44:55 +09:00
parent b6abc3d118
commit 8f711c1706
4 changed files with 128 additions and 69 deletions
@@ -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<String, dynamic> json) {
return Attribution(
provider: json['provider'] as String,
asOf: json['asOf'] as String?,
);
}
}
/// 상세 — 목록 필드에 소개·시간·링크·출처를 더한다
class EventDetail {
const EventDetail({
@@ -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,
);
}
}