번역 평가 버튼 · 내 퀘스트 검토 버튼 · MY 질문 상세 · MY 완료 퀘스트 수 · 행성/성운 칸 · 라운지 빈 상태 수정

This commit is contained in:
Sn-Kinos
2026-10-05 15:12:33 +09:00
parent 5ad229b971
commit f84d696c21
18 changed files with 666 additions and 365 deletions
+116 -116
View File
@@ -71,120 +71,115 @@ enum PopTxType {
/// Functions 의 `ReasonCode`(`firebase/functions/src/ledger/ledger.ts`)와 짝이며,
/// 모르는 코드는 원문을 그대로 내보이지 않고 중립 문구로 접는다 — 사용자 화면에
/// `QUEST_MUSIC_SHAKE` 같은 내부 식별자가 새면 고장으로 읽힌다.
({String title, String type}) popReasonLabel(String reasonCode) =>
switch (reasonCode) {
'QUEST_STEPS' => (
title: appL10n.popReasonQuestSteps,
type: appL10n.popTypeQuest,
),
'QUEST_MUSIC_SHAKE' => (
title: appL10n.popReasonQuestMusicShake,
type: appL10n.popTypeQuest,
),
'QUEST_PLACE' => (
title: appL10n.popReasonQuestPlace,
type: appL10n.popTypeQuest,
),
'QUEST_ATTEND' => (
title: appL10n.popReasonQuestAttend,
type: appL10n.popTypeQuest,
),
'QUEST_UGC' => (
title: appL10n.popReasonQuestUgc,
type: appL10n.popTypeQuest,
),
'QUEST_FEE' => (
title: appL10n.popReasonQuestFee,
type: appL10n.popTypeFee,
),
'QUEST_POOL_DEPOSIT' => (
title: appL10n.popReasonQuestPoolDeposit,
type: appL10n.popTypeQuest,
),
// 정상 종료 반환(C-09)과 공개 전 실패 환불은 사용자에게 같은 「반환」이다.
'QUEST_POOL_RETURN' || 'QUEST_POOL_REFUND' => (
title: appL10n.popReasonQuestPoolReturn,
type: appL10n.popTxRefund,
),
'QUEST_TIMEATTACK' => (
title: appL10n.popReasonQuestTimeAttack,
type: appL10n.popTypeQuest,
),
'AD_REWARD' => (
title: appL10n.popReasonAdReward,
type: appL10n.popTypeReward,
),
'HERO_DROP' => (
title: appL10n.popReasonHeroDrop,
type: appL10n.popTypeReward,
),
'HERO_DROP_TRIGGER' => (
title: appL10n.popReasonHeroDropTrigger,
type: appL10n.commonContribute,
),
'TRANSLATION' => (
title: appL10n.popReasonTranslation,
type: appL10n.popTypeReward,
),
'IAP_PURCHASE' => (
title: appL10n.popReasonIapPurchase,
type: appL10n.popTypePayment,
),
'FOCUS_DAILY' => (
title: appL10n.popReasonFocusDaily,
type: appL10n.popTypeSubscription,
),
'PREREG_BONUS' => (
title: appL10n.popReasonPreregBonus,
type: appL10n.popTypeReward,
),
'REFERRAL_REWARD' => (
// 사본 POP 내역 `554:23742` 「친구 초대 · 레퍼럴」.
title: appL10n.popReasonReferral,
type: appL10n.popTypeReferral,
),
'ADMIN_GRANT' => (
title: appL10n.popReasonAdminGrant,
type: appL10n.popTypeAdmin,
),
'ADMIN_CLAWBACK' => (
title: appL10n.popReasonAdminClawback,
type: appL10n.popTypeAdmin,
),
'CAMPAIGN_FUND' => (
title: appL10n.popReasonCampaignFund,
type: appL10n.commonContribute,
),
'CAMPAIGN_REFUND' => (
title: appL10n.popReasonCampaignRefund,
type: appL10n.popTxRefund,
),
'CAMPAIGN_FEE_REFUND' => (
title: appL10n.popReasonCampaignFeeRefund,
type: appL10n.popTxRefund,
),
'CAMPAIGN_FEE' => (
title: appL10n.popReasonCampaignFee,
type: appL10n.popTypeFee,
),
'QNA_BOUNTY' => (
title: appL10n.popReasonQnaBounty,
type: appL10n.commonContribute,
),
'QNA_REFUND' => (
title: appL10n.popReasonQnaRefund,
type: appL10n.popTxRefund,
),
'QNA_REWARD' => (
title: appL10n.popReasonQnaReward,
type: appL10n.popTypeReward,
),
'ECLIPSE_DEFENSE' => (
title: appL10n.popReasonEclipseDefense,
type: appL10n.commonContribute,
),
_ => (title: appL10n.popReasonOther, type: appL10n.commonEtc),
};
({String title, String type}) popReasonLabel(
String reasonCode,
) => switch (reasonCode) {
'QUEST_STEPS' => (
title: appL10n.popReasonQuestSteps,
type: appL10n.popTypeQuest,
),
'QUEST_MUSIC_SHAKE' => (
title: appL10n.popReasonQuestMusicShake,
type: appL10n.popTypeQuest,
),
'QUEST_PLACE' => (
title: appL10n.popReasonQuestPlace,
type: appL10n.popTypeQuest,
),
'QUEST_ATTEND' => (
title: appL10n.popReasonQuestAttend,
type: appL10n.popTypeQuest,
),
'QUEST_UGC' => (title: appL10n.popReasonQuestUgc, type: appL10n.popTypeQuest),
'QUEST_FEE' => (title: appL10n.popReasonQuestFee, type: appL10n.popTypeFee),
'QUEST_POOL_DEPOSIT' => (
title: appL10n.popReasonQuestPoolDeposit,
type: appL10n.popTypeQuest,
),
// 정상 종료 반환(C-09)과 공개 전 실패 환불은 사용자에게 같은 「반환」이다.
'QUEST_POOL_RETURN' || 'QUEST_POOL_REFUND' => (
title: appL10n.popReasonQuestPoolReturn,
type: appL10n.popTxRefund,
),
'QUEST_TIMEATTACK' => (
title: appL10n.popReasonQuestTimeAttack,
type: appL10n.popTypeQuest,
),
'AD_REWARD' => (
title: appL10n.popReasonAdReward,
type: appL10n.popTypeReward,
),
'HERO_DROP' => (
title: appL10n.popReasonHeroDrop,
type: appL10n.popTypeReward,
),
'HERO_DROP_TRIGGER' => (
title: appL10n.popReasonHeroDropTrigger,
type: appL10n.commonContribute,
),
'TRANSLATION' => (
title: appL10n.popReasonTranslation,
type: appL10n.popTypeReward,
),
'IAP_PURCHASE' => (
title: appL10n.popReasonIapPurchase,
type: appL10n.popTypePayment,
),
'FOCUS_DAILY' => (
title: appL10n.popReasonFocusDaily,
type: appL10n.popTypeSubscription,
),
'PREREG_BONUS' => (
title: appL10n.popReasonPreregBonus,
type: appL10n.popTypeReward,
),
'REFERRAL_REWARD' => (
// 사본 POP 내역 `554:23742` 「친구 초대 · 레퍼럴」.
title: appL10n.popReasonReferral,
type: appL10n.popTypeReferral,
),
'ADMIN_GRANT' => (
title: appL10n.popReasonAdminGrant,
type: appL10n.popTypeAdmin,
),
'ADMIN_CLAWBACK' => (
title: appL10n.popReasonAdminClawback,
type: appL10n.popTypeAdmin,
),
'CAMPAIGN_FUND' => (
title: appL10n.popReasonCampaignFund,
type: appL10n.commonContribute,
),
'CAMPAIGN_REFUND' => (
title: appL10n.popReasonCampaignRefund,
type: appL10n.popTxRefund,
),
'CAMPAIGN_FEE_REFUND' => (
title: appL10n.popReasonCampaignFeeRefund,
type: appL10n.popTxRefund,
),
'CAMPAIGN_FEE' => (
title: appL10n.popReasonCampaignFee,
type: appL10n.popTypeFee,
),
'QNA_BOUNTY' => (
title: appL10n.popReasonQnaBounty,
type: appL10n.commonContribute,
),
'QNA_REFUND' => (
title: appL10n.popReasonQnaRefund,
type: appL10n.popTxRefund,
),
'QNA_REWARD' => (
title: appL10n.popReasonQnaReward,
type: appL10n.popTypeReward,
),
'ECLIPSE_DEFENSE' => (
title: appL10n.popReasonEclipseDefense,
type: appL10n.commonContribute,
),
_ => (title: appL10n.popReasonOther, type: appL10n.commonEtc),
};
/// 레벨 칭호 — 정책 원장 **C-18 · B-66**.
///
@@ -586,7 +581,11 @@ const int nicknameMaxLength = 20;
/// 운영 사칭으로 읽힐 수 있는 이름 — 대소문자 무시 부분 일치로 막는다.
/// `firestore.rules` 의 `nicknameOk` 가 같은 목록을 본다.
const List<String> reservedNicknameWords = [
'fanuzu', '운영팀', '관리자', 'admin', 'official',
'fanuzu',
'운영팀',
'관리자',
'admin',
'official',
];
/// 닉네임 없는 작성자 표기([storedFanName] 「팬」)와 그 번역 — 정확히 일치하면 막는다.
@@ -603,7 +602,8 @@ String normalizeNickname(String nickname) => nickname
bool nicknameReserved(String nickname) {
final key = normalizeNickname(nickname);
return reservedFanNames.contains(key) || reservedNicknameWords.any(key.contains);
return reservedFanNames.contains(key) ||
reservedNicknameWords.any(key.contains);
}
/// 차단한 계정 한 건 — 설계서 `users/{uid}/blocks/{targetUid}`.
+13 -3
View File
@@ -5,6 +5,7 @@ import '../campaign/campaign_detail_screen.dart';
import '../campaign/widgets/campaign_list_card.dart';
import '../inside/inside_data.dart';
import '../inside/inside_detail_screen.dart';
import '../translation/translation_data.dart' show TranslationRepository;
import '../inside/inside_screen.dart' show InsideQuestionCard;
import 'widgets/my_cards.dart';
import '../../core/i18n/app_locale.dart';
@@ -38,6 +39,7 @@ class MyListsScreen extends StatefulWidget {
required this.kind,
this.campaigns,
this.insides,
this.translation,
});
final MyListKind kind;
@@ -45,6 +47,9 @@ class MyListsScreen extends StatefulWidget {
/// 내 인사이드의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
final InsideRepository? insides;
/// 내 질문 상세의 번역 원천. null 이면 번역 기능을 끈다.
final TranslationRepository? translation;
/// 내 캠페인의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
final CampaignRepository? campaigns;
@@ -119,8 +124,9 @@ class _MyListsScreenState extends State<MyListsScreen> {
Widget _insideItems(BuildContext context, List<InsideQuestion> mine) {
final items = switch (_sort) {
_MySort.pop => [...mine]
..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
_MySort.pop => [
...mine,
]..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
_MySort.solved => mine.where((q) => q.isSolved).toList(),
_ => mine,
};
@@ -144,7 +150,11 @@ class _MyListsScreenState extends State<MyListsScreen> {
question: items[i],
onTap: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => InsideDetailScreen(question: items[i]),
builder: (_) => InsideDetailScreen(
question: items[i],
repository: widget.insides,
translation: widget.translation,
),
),
),
),
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
@@ -14,7 +16,8 @@ import '../../core/i18n/app_locale.dart';
/// 쓰면 어느 것이 진짜인지 화면에서 알 수 없다.
///
/// 읽는 곳 — **서버가 실제로 쓰는 필드**를 읽는다.
/// - 프로필 `users/{uid}` (nickname · photoUrl · originNo · steps · completedQuests)
/// - 프로필 `users/{uid}` (nickname · photoUrl · originNo · steps)
/// - 완료 퀘스트 `users/{uid}/verifiedCompletions` 표식 수(무효 제외)
/// - 누적 기여 POP `users/{uid}.totalContributedPop`(`fn-pop-spend` 가 갱신, 설계서 §3.6)
/// - 레벨 `users/{uid}.{userExp, userLevel}` · Passport `users/{uid}.passportTier`
/// (C-18 · B-65). 필드가 없는 기존 사용자는 Lv.1 · SILVER 로 둔다.
@@ -46,6 +49,18 @@ class FirestoreMyRepository extends SampleMyRepository {
final userF = db.doc('users/$uid').get();
final walletF = db.doc('users/$uid/wallet/main').get();
// 완료 퀘스트 = 검증 활동 표식 수 − 무효 표식 수. 서버 `verifiedQuestCountOf`(fpi.ts)와 같은 식.
// `users.completedQuests` 는 쓰는 서버 코드가 없어 늘 0 이었다.
final done = db.collection('users/$uid/verifiedCompletions');
// 규칙 배포 전 서버에서는 이 읽기가 거부된다 — MY 전체를 떨어뜨리지 않고 0 으로 둔다.
final completedF =
Future.wait([
done.count().get(),
done.where('voided', isEqualTo: true).count().get(),
]).then(
(c) => math.max(0, (c[0].count ?? 0) - (c[1].count ?? 0)),
onError: (Object _) => 0,
);
final user = (await userF).data() ?? const <String, dynamic>{};
final wallet = (await walletF).data() ?? const <String, dynamic>{};
@@ -83,7 +98,7 @@ class FirestoreMyRepository extends SampleMyRepository {
passportTier: PassportTier.parse(user.strOr('passportTier')),
balancePop: wallet.intOr('activityBalance'),
steps: user.intOr('steps'),
completedQuests: user.intOr('completedQuests'),
completedQuests: await completedF,
planetTiers: tiers,
photoUrl: photoUrl.isEmpty ? null : photoUrl,
originNo: user.containsKey('originNo') ? user.intOr('originNo') : null,
@@ -127,10 +142,9 @@ class FirestoreMyRepository extends SampleMyRepository {
@override
Future<void> updateNickname(String nickname) async {
final uid = _backend.requireUid;
await _backend.db.doc('users/$uid').set(
{'nickname': nickname},
SetOptions(merge: true),
);
await _backend.db.doc('users/$uid').set({
'nickname': nickname,
}, SetOptions(merge: true));
}
/// POP 원장 — S-M-WALLET · 명세 `554:23732`.
@@ -174,22 +188,24 @@ class FirestoreMyRepository extends SampleMyRepository {
final reasonCode = data.strOr('reasonCode');
final label = popReasonLabel(reasonCode);
final at = data.dateOr('createdAt', _epoch);
rows.add(_LedgerRow(
reasonCode: reasonCode,
// Zap 한 행이 몇 번분인가. `fn-eclipse-defend` 가 `count` 를 묶어 받으므로
// 행 하나가 여러 번일 수 있다. 없는 문서는 한 번으로 본다.
count: data.intOr('count', 1).clamp(1, 1 << 30),
entry: PopLedgerEntry(
at: at,
title: label.title,
type: label.type,
kind: PopKind.parse(data.strOr('popType')),
txType: PopTxType.parse(data.strOr('type', 'grant')),
delta: data.intOr('amount'),
balanceAfter: data.intOr('balanceAfter'),
cursor: PopCursor(at: at, id: doc.id),
rows.add(
_LedgerRow(
reasonCode: reasonCode,
// Zap 한 행이 몇 번분인가. `fn-eclipse-defend` 가 `count` 를 묶어 받으므로
// 행 하나가 여러 번일 수 있다. 없는 문서는 한 번으로 본다.
count: data.intOr('count', 1).clamp(1, 1 << 30),
entry: PopLedgerEntry(
at: at,
title: label.title,
type: label.type,
kind: PopKind.parse(data.strOr('popType')),
txType: PopTxType.parse(data.strOr('type', 'grant')),
delta: data.intOr('amount'),
balanceAfter: data.intOr('balanceAfter'),
cursor: PopCursor(at: at, id: doc.id),
),
),
));
);
}
return _foldZap(rows);
}
@@ -262,7 +278,10 @@ class FirestoreMyRepository extends SampleMyRepository {
PaymentEntry _payment(String id, Map<String, dynamic> data) => PaymentEntry(
id: id,
at: data.dateOr('purchasedAt', _epoch),
itemName: data.strOr('itemName', data.strOr('productId', appL10n.payItemFallback)),
itemName: data.strOr(
'itemName',
data.strOr('productId', appL10n.payItemFallback),
),
price: data.intOr('priceAmount'),
method: data.strOr('method', appL10n.payMethodInApp),
state: paymentStateOf(data.strOr('state')),
@@ -416,10 +435,9 @@ class FirestoreMyRepository extends SampleMyRepository {
);
}
final url = await ref.getDownloadURL();
await _backend.db.doc('users/$uid').set(
{'photoUrl': url},
SetOptions(merge: true),
);
await _backend.db.doc('users/$uid').set({
'photoUrl': url,
}, SetOptions(merge: true));
return url;
}
}
+43 -9
View File
@@ -9,7 +9,10 @@ import '../../core/ui/app_icons.dart';
import '../auth/auth.dart';
import '../auth/auth_repository.dart';
import '../campaign/campaign_data.dart' show CampaignRepository;
import '../galaxy/galaxy_data.dart';
import '../galaxy/galaxy_home_screen.dart' show GalaxyHomeScreen;
import '../inside/inside_data.dart' show InsideRepository;
import '../translation/translation_data.dart' show TranslationRepository;
import 'my_data.dart';
import 'my_lists_screen.dart';
import 'profile_edit_screen.dart';
@@ -31,16 +34,24 @@ class MyScreen extends StatefulWidget {
this.auth,
this.campaigns,
this.insides,
this.translation,
this.galaxy = const SampleGalaxyRepository(),
});
final MyRepository repository;
/// 「행성/성운」 칸 — GALAXY 「내 행성」과 같은 가입 목록(`users/{uid}/memberships`)을 읽는다.
final GalaxyRepository galaxy;
/// 「내 캠페인」이 읽는 저장소. null 이면 표본.
final CampaignRepository? campaigns;
/// 「내 인사이드」가 읽는 저장소. null 이면 표본.
final InsideRepository? insides;
/// 내 질문 상세의 번역 원천.
final TranslationRepository? translation;
/// 설정 > SNS 연동으로 그대로 내려간다. null 이면 설정 화면이 표본을 쓴다.
final AccountLinker? linker;
@@ -58,6 +69,7 @@ class MyScreen extends StatefulWidget {
class _MyScreenState extends State<MyScreen> {
MyProfile? _profile;
FocusStatus? _focus;
List<PlanetNode> _planets = const [];
bool _claiming = false;
@override
@@ -70,10 +82,16 @@ class _MyScreenState extends State<MyScreen> {
final profile = await widget.repository.profile();
// 구독 조회 실패로 MY 전체가 멈추지 않게 한다 — 카드만 빠진다.
final focus = await widget.repository.focus().catchError((_) => null);
// 행성 목록 실패도 칸만 비운다.
final planets = await widget.galaxy
.load()
.then((s) => s.memberships.take(3).toList())
.catchError((_) => const <PlanetNode>[]);
if (mounted) {
setState(() {
_profile = profile;
_focus = focus;
_planets = planets;
});
}
}
@@ -214,11 +232,9 @@ class _MyScreenState extends State<MyScreen> {
children: [
Row(
children: [
for (final (i, img) in const [
'assets/figma/my-planet-1.png',
'assets/figma/my-planet-2.png',
'assets/figma/my-planet-3.png',
].indexed) ...[
// 시안 세 칸 — 가입하거나 만든 행성/성운(최신 가입순). 그림은
// GALAXY 「내 행성」과 같은 기준(팬덤 stage)이고, 모자라면 빈 칸이다.
for (var i = 0; i < 3; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(
child: AspectRatio(
@@ -227,12 +243,23 @@ class _MyScreenState extends State<MyScreen> {
// 칸 109 안에 행성 88 — 안쪽 10.5.
padding: const EdgeInsets.all(10.5),
decoration: BoxDecoration(
color: const Color(0xFF2C2C2C),
color: i < _planets.length
? const Color(0xFF2C2C2C)
: const Color(0x802C2C2C),
borderRadius: BorderRadius.circular(
AppRadius.md,
),
),
child: Image.asset(img, fit: BoxFit.contain),
child: i < _planets.length
? Image.asset(
GalaxyHomeScreen.cardImageOf(
_planets[i],
i,
),
fit: BoxFit.contain,
semanticLabel: _planets[i].name,
)
: null,
),
),
),
@@ -259,6 +286,7 @@ class _MyScreenState extends State<MyScreen> {
MyListsScreen(
kind: MyListKind.inside,
insides: widget.insides,
translation: widget.translation,
),
),
),
@@ -337,7 +365,9 @@ class _MyScreenState extends State<MyScreen> {
),
);
} catch (_) {
messenger.showSnackBar(SnackBar(content: Text(l10n.commonRequestFailed)));
messenger.showSnackBar(
SnackBar(content: Text(l10n.commonRequestFailed)),
);
}
},
),
@@ -464,7 +494,11 @@ class _MyHeader extends StatelessWidget {
/// `518:88748` 은 B-04 가 대체한 과거 안). 반영 화면이 `MY` 로 정해져 있어 통계 카드 아래에 둔다.
/// 구독 중일 때만 보인다.
class _FocusCard extends StatelessWidget {
const _FocusCard({required this.focus, required this.busy, required this.onClaim});
const _FocusCard({
required this.focus,
required this.busy,
required this.onClaim,
});
final FocusStatus focus;
final bool busy;
+60 -51
View File
@@ -40,10 +40,12 @@ class _PaymentHistoryScreenState extends State<PaymentHistoryScreen> {
await Store.instance?.restore();
} on Exception {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(context.l10n.storeConnectFailed),
));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(context.l10n.storeConnectFailed),
),
);
return;
}
await _load();
@@ -63,7 +65,10 @@ class _PaymentHistoryScreenState extends State<PaymentHistoryScreen> {
// 구매 복원 — 재설치 · 기기 변경 뒤 Focus 구독을 되살린다. 스토어가 돌려준
// 구매는 서버가 거래 ID 로 멱등 검증하므로 두 번 지급되지 않는다.
if (Store.instance != null)
TextButton(onPressed: _restore, child: Text(context.l10n.payRestore)),
TextButton(
onPressed: _restore,
child: Text(context.l10n.payRestore),
),
],
),
body: Column(
@@ -219,7 +224,11 @@ class _PaymentDetailScreenState extends State<PaymentDetailScreen> {
),
_Row(label: context.l10n.payMethod, value: p.method),
_Row(label: context.l10n.payStatus, value: p.state.label),
_Row(label: context.l10n.payAmount, value: '₩ ${_won(p.price)}', amount: true),
_Row(
label: context.l10n.payAmount,
value: '₩ ${_won(p.price)}',
amount: true,
),
const SizedBox(height: 8),
Text(context.l10n.payRefundInfo, style: _guide),
const SizedBox(height: 6),
@@ -328,51 +337,51 @@ class _RefundRequestScreenState extends State<RefundRequestScreen> {
),
// 디폴트 미선택 · 단수 선택.
body: ListView(
children: [
Container(
height: 32,
color: const Color(0xFF232325),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
context.l10n.refundReason,
style: myText(
12,
FontWeight.w600,
color: const Color(0xFF909091),
height: 1,
children: [
Container(
height: 32,
color: const Color(0xFF232325),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
context.l10n.refundReason,
style: myText(
12,
FontWeight.w600,
color: const Color(0xFF909091),
height: 1,
),
),
),
for (final reason in RefundRequestScreen.reasons)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _reason = reason),
child: Container(
height: 48,
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFF313131))),
),
child: Row(
children: [
AppRadio(selected: _reason == reason),
const SizedBox(width: 8),
Text(
reason,
style: myText(
16,
FontWeight.w600,
color: const Color(0xFFD1D1D2),
height: 1,
),
),
],
),
),
),
for (final reason in RefundRequestScreen.reasons)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => _reason = reason),
child: Container(
height: 48,
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFF313131))),
),
child: Row(
children: [
AppRadio(selected: _reason == reason),
const SizedBox(width: 8),
Text(
reason,
style: myText(
16,
FontWeight.w600,
color: const Color(0xFFD1D1D2),
height: 1,
),
),
],
),
),
),
],
),
],
),
);
/// 확인 — 시안 `506:14615` iOS 경고창 「정말 환불 신청을 할까요?」.
@@ -397,9 +406,9 @@ class _RefundRequestScreenState extends State<RefundRequestScreen> {
// 서버·Admin 에는 기준 언어(ko) 사유를 남긴다 — 화면 언어에 따라 원장 값이 갈리지 않게.
await widget.repository.requestRefund(
widget.paymentId,
RefundRequestScreen.reasonsOf(
koL10n,
)[RefundRequestScreen.reasons.indexOf(_reason!)],
RefundRequestScreen.reasonsOf(koL10n)[RefundRequestScreen.reasons.indexOf(
_reason!,
)],
);
if (mounted) Navigator.of(context).pop(_reason);
}
+8 -2
View File
@@ -117,7 +117,10 @@ class _PopHistoryScreenState extends State<PopHistoryScreen> {
child: Padding(
padding: const EdgeInsets.all(AppSpace.s6),
child: Center(
child: Text(context.l10n.commonNoHistory, style: myText(14, FontWeight.w500)),
child: Text(
context.l10n.commonNoHistory,
style: myText(14, FontWeight.w500),
),
),
),
);
@@ -131,7 +134,10 @@ class _PopHistoryScreenState extends State<PopHistoryScreen> {
padding: EdgeInsets.all(AppSpace.s3),
child: CircularProgressIndicator(),
)
: TextButton(onPressed: _more, child: Text(context.l10n.commonMore)),
: TextButton(
onPressed: _more,
child: Text(context.l10n.commonMore),
),
),
);
}
+8 -2
View File
@@ -136,7 +136,10 @@ PreferredSizeWidget myTextActionBar(
}) => AppBar(
automaticallyImplyLeading: false,
leadingWidth: 72,
leading: _BarText(label: context.l10n.commonCancel, onTap: () => Navigator.of(context).pop()),
leading: _BarText(
label: context.l10n.commonCancel,
onTap: () => Navigator.of(context).pop(),
),
title: Text(title),
actions: [_BarText(label: context.l10n.commonDone, onTap: onDone)],
);
@@ -237,7 +240,10 @@ class _ProfilePhotoScreenState extends State<ProfilePhotoScreen> {
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Text(context.l10n.albumItems, style: myText(16, FontWeight.w600, height: 1)),
child: Text(
context.l10n.albumItems,
style: myText(16, FontWeight.w600, height: 1),
),
),
// 시안 4×3 격자 — 카메라 · 앨범 선택기 다음 열 칸은 #000 빈 칸(사진 목록은 기기 선택기가 그린다).
Wrap(
+5 -4
View File
@@ -51,7 +51,10 @@ class _InquiryScreenState extends State<InquiryScreen> {
appBar: AppBar(title: Text(context.l10n.inquiryListTitle)),
body: items.isEmpty
? Center(
child: Text(context.l10n.inquiryEmpty, style: myText(14, FontWeight.w500)),
child: Text(
context.l10n.inquiryEmpty,
style: myText(14, FontWeight.w500),
),
)
: ListView.separated(
padding: const EdgeInsets.all(16),
@@ -347,9 +350,7 @@ class _InquiryComposeScreenState extends State<InquiryComposeScreen> {
if (!mounted) return;
_toast(
context,
edit == null
? context.l10n.inquirySubmitted
: context.l10n.inquiryEdited,
edit == null ? context.l10n.inquirySubmitted : context.l10n.inquiryEdited,
);
Navigator.of(context).pop(true);
}