generated from ttipo/project-base
perf(flutter): 사진을 그릴 크기로 줄여 메모리 사용을 줄임
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 20m27s
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 11m41s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 10m47s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m8s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 20m40s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m4s
CI / Frontend (lint · type-check · test) (push) Successful in 10m55s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m7s
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 20m27s
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 11m41s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 10m47s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m8s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 20m40s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m4s
CI / Frontend (lint · type-check · test) (push) Successful in 10m55s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m7s
공연(KOPIS) 포스터는 목록 썸네일도 원본이라(표본 15개 중앙 0.45MB · 최대 9.2MB) 그대로 풀면 사진 한 장에 수십 MB 를 쓴다. 그릴 크기 × 화면 배율로 가로를 정해 줄여 푼다(memCacheWidth). cover 는 가로가 긴 사진도 세로를 채우도록 세로 × 2 까지 가로를 두고, 원본보다 크게 늘리지는 않는다. 내려받는 양은 그대로다(원본만 제공된다). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #5.
This commit is contained in:
@@ -60,6 +60,7 @@ lib/
|
||||
- 한국어 문단은 `keepAll()` 을 거쳐 단어 단위로 줄바꿈한다(Flutter 기본은 글자 단위).
|
||||
- 카드·배지 대신 흰 바탕 위 평면 목록과 구분선, 색 글자로 메타 정보를 보인다.
|
||||
- 공공누리 Type3 사진은 자르지 않는다(`PublicImage` 가 contain 으로 보인다 — 행사·장소 사진 모두 이 위젯을 거친다).
|
||||
- 사진은 화면에 그릴 크기로 줄여 푼다(`PublicImage` → `decodeWidthFor`). 공연 포스터는 목록 썸네일도 원본(최대 9MB)이라 그대로 풀면 메모리를 많이 쓴다.
|
||||
- 영업시간을 모르는 장소는 "영업시간 미확인 · 방문 전 확인"으로 보이고, 확인 안 된 상가정보 매장은 "함께 가기 좋은 곳"에 고르지 않는다
|
||||
(서버가 `trusted=true` 로 거른다 — 앱에서 거르면 상가가 빽빽한 도심에서 칸이 비었다).
|
||||
- `Semantics(excludeSemantics: true)` 로 버튼을 감싸면 안쪽 InkWell 의 누르기 동작까지 지워진다. Semantics 에도 `onTap` 을 같이 준다
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
/// cover 로 채울 때 가로가 긴 사진(최대 2:1)도 세로를 채우도록 세로 길이의 이만큼까지 가로를 둔다
|
||||
const double kCoverAspectAllowance = 2;
|
||||
|
||||
/// 사진을 화면에 그릴 크기에 맞춰 줄여 풀 가로 픽셀 수 (순수 함수).
|
||||
///
|
||||
/// 공연 포스터는 목록 썸네일도 원본(수 MB, 수천 픽셀)이라 그대로 풀면 사진 한 장에 수십 MB 를 쓴다.
|
||||
/// 가로만 정하면 비율은 유지되고, 원본보다 크게 늘리지는 않는다.
|
||||
/// - contain: 상자 가로만큼이면 된다
|
||||
/// - cover: 가로가 긴 사진도 세로를 채워야 하므로 max(가로, 세로 × [kCoverAspectAllowance])
|
||||
/// 크기를 모르면 null(원본 그대로 푼다).
|
||||
int? decodeWidthFor({
|
||||
required double? boxWidth,
|
||||
required double? boxHeight,
|
||||
required BoxFit fit,
|
||||
required double devicePixelRatio,
|
||||
}) {
|
||||
final width = boxWidth != null && boxWidth.isFinite && boxWidth > 0
|
||||
? boxWidth
|
||||
: null;
|
||||
final height = boxHeight != null && boxHeight.isFinite && boxHeight > 0
|
||||
? boxHeight
|
||||
: null;
|
||||
final logical = switch ((width, height)) {
|
||||
(final w?, final h?) =>
|
||||
fit == BoxFit.cover ? math.max(w, h * kCoverAspectAllowance) : w,
|
||||
(final w?, null) => w,
|
||||
(null, final h?) => h * kCoverAspectAllowance,
|
||||
(null, null) => null,
|
||||
};
|
||||
return logical == null ? null : (logical * devicePixelRatio).ceil();
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:wandergo/core/theme/app_colors.dart';
|
||||
import 'package:wandergo/core/theme/app_theme.dart';
|
||||
import 'package:wandergo/core/utils/image_decode.dart';
|
||||
import 'package:wandergo/shared/models/source_models.dart';
|
||||
|
||||
/// 사진이 없거나 못 불러올 때의 자리 표시 — 분류별 배경색·아이콘
|
||||
@@ -49,6 +50,8 @@ class PublicImage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
final imageUrl = url;
|
||||
final boxFit =
|
||||
fit ?? (license == ImageLicense.type3 ? BoxFit.contain : BoxFit.cover);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: SizedBox(
|
||||
@@ -58,16 +61,21 @@ class PublicImage extends StatelessWidget {
|
||||
? fallback
|
||||
: ColoredBox(
|
||||
color: AppColors.placeholder,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit:
|
||||
fit ??
|
||||
(license == ImageLicense.type3
|
||||
? BoxFit.contain
|
||||
: BoxFit.cover),
|
||||
errorWidget: (_, _, _) => fallback,
|
||||
// 크기를 정하지 않은 쪽(상세 위쪽 사진의 가로 등)은 실제로 받은 폭으로 계산한다
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: boxFit,
|
||||
memCacheWidth: decodeWidthFor(
|
||||
boxWidth: width ?? constraints.maxWidth,
|
||||
boxHeight: height ?? constraints.maxHeight,
|
||||
fit: boxFit,
|
||||
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
|
||||
),
|
||||
errorWidget: (_, _, _) => fallback,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wandergo/core/utils/image_decode.dart';
|
||||
|
||||
void main() {
|
||||
group('decodeWidthFor', () {
|
||||
test('cover 는 가로가 긴 사진도 세로를 채우도록 세로 × 2 까지 가로를 둔다', () {
|
||||
// 목록 줄 64×64, 화면 배율 3 → max(64, 128) × 3
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 64,
|
||||
boxHeight: 64,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 3,
|
||||
),
|
||||
384,
|
||||
);
|
||||
// 함께 가기 카드 148×104 → max(148, 208) × 3
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 148,
|
||||
boxHeight: 104,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 3,
|
||||
),
|
||||
624,
|
||||
);
|
||||
});
|
||||
|
||||
test('contain(공공누리 Type3)은 상자 가로만큼이면 된다', () {
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: 148,
|
||||
boxHeight: 104,
|
||||
fit: BoxFit.contain,
|
||||
devicePixelRatio: 2.625,
|
||||
),
|
||||
389,
|
||||
);
|
||||
});
|
||||
|
||||
test('가로를 모르면 세로로, 둘 다 모르면 null(원본 그대로)', () {
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: double.infinity,
|
||||
boxHeight: 240,
|
||||
fit: BoxFit.contain,
|
||||
devicePixelRatio: 2,
|
||||
),
|
||||
960,
|
||||
);
|
||||
expect(
|
||||
decodeWidthFor(
|
||||
boxWidth: null,
|
||||
boxHeight: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
devicePixelRatio: 2,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user