Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림

This commit is contained in:
Sn-Kinos
2026-10-06 03:03:20 +09:00
parent 64825e72b0
commit ecf5998725
77 changed files with 2703 additions and 640 deletions
+196 -78
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/planet_25d.dart' show PlanetArt;
import 'planet_data.dart';
import 'widgets/action_sheet.dart';
import '../../core/i18n/app_locale.dart';
@@ -32,10 +33,38 @@ class FpiRankingScreen extends StatefulWidget {
}
class _FpiRankingScreenState extends State<FpiRankingScreen> {
late final Future<FpiRanking> _ranking = widget.repository.planetRanking(
/// 명세 `546:14639` — 최초 30개, 스크롤 시 10개씩.
int _limit = 30;
late Future<FpiRanking> _ranking = widget.repository.planetRanking(
metric: widget.metric,
limit: _limit,
);
Future<FpiRanking> _load(int limit) =>
widget.repository.planetRanking(metric: widget.metric, limit: limit);
bool _loadingMore = false;
/// 지금 장이 꽉 찼을 때만 10개 더 읽는다(덜 찼으면 끝이다).
Future<void> _next() async {
if (_loadingMore) return;
_loadingMore = true;
try {
final now = await _ranking;
if (now.rows.length < _limit || !mounted) return;
final next = _load(_limit + 10);
setState(() {
_limit += 10;
_ranking = next;
});
await next;
} catch (_) {
// 다음 장 실패는 앞 장을 그대로 둔다.
} finally {
_loadingMore = false;
}
}
void _openUsers(FpiRankRow planet) =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
@@ -57,6 +86,9 @@ class _FpiRankingScreenState extends State<FpiRankingScreen> {
myLabel: context.l10n.rankMyPlanet(widget.metric.label),
emptyLabel: context.l10n.rankPlanetEmpty,
onTapRow: _openUsers,
// 명세 `546:14629` — 소속 행성 랭킹 영역도 「사용자 랭킹 화면 이동」.
onTapMine: _openUsers,
onEnd: _next,
planets: true,
),
);
@@ -102,18 +134,45 @@ class FpiUserRankingScreen extends StatefulWidget {
class _FpiUserRankingScreenState extends State<FpiUserRankingScreen> {
FpiRankScope _scope = FpiRankScope.live;
late Future<FpiRanking> _ranking = _load();
int _limit = 30;
late Future<FpiRanking> _ranking = _load(_limit);
Future<FpiRanking> _load() => switch (_scope) {
Future<FpiRanking> _load(int limit) => switch (_scope) {
FpiRankScope.live => widget.repository.userRanking(
metric: widget.metric,
fandomId: widget.fandomId,
limit: limit,
),
FpiRankScope.byCountry => widget.repository.countryRanking(
metric: widget.metric,
limit: limit,
),
};
/// 국가별은 명세 `546:14713` 「최초 30개 노출 스크롤시 10개씩」. 실시간 사용자 랭킹은 명세에 수가 없어
/// 같은 방식으로 이어 읽는다.
bool _loadingMore = false;
/// 지금 장이 꽉 찼을 때만 10개 더 읽는다(덜 찼으면 끝이다).
Future<void> _next() async {
if (_loadingMore) return;
_loadingMore = true;
try {
final now = await _ranking;
if (now.rows.length < _limit || !mounted) return;
final next = _load(_limit + 10);
setState(() {
_limit += 10;
_ranking = next;
});
await next;
} catch (_) {
// 다음 장 실패는 앞 장을 그대로 둔다.
} finally {
_loadingMore = false;
}
}
@override
Widget build(BuildContext context) => Scaffold(
// 시안 `373:436695` — 제목 「USER RANKING」, 범위 필터는 본문 「실시간 랭킹 ⌄」.
@@ -130,6 +189,8 @@ class _FpiUserRankingScreenState extends State<FpiUserRankingScreen> {
emptyLabel: context.l10n.rankEmpty,
scopeLabel: context.l10n.rankScope(_scope.label),
onPickScope: _pickScope,
onEnd: _next,
flags: _scope == FpiRankScope.byCountry,
),
);
@@ -143,7 +204,8 @@ class _FpiUserRankingScreenState extends State<FpiUserRankingScreen> {
if (next != null && next != _scope && mounted) {
setState(() {
_scope = next;
_ranking = _load();
_limit = 30;
_ranking = _load(_limit);
});
}
}
@@ -162,8 +224,20 @@ class _RankingBody extends StatelessWidget {
this.planets = false,
this.scopeLabel,
this.onPickScope,
this.onTapMine,
this.onEnd,
this.flags = false,
});
/// 내 줄 카드를 눌렀을 때.
final ValueChanged<FpiRankRow>? onTapMine;
/// 목록 끝 가까이 스크롤했을 때 — 다음 10개.
final VoidCallback? onEnd;
/// 국가 랭킹 — 아바타 자리에 국기(명세 `546:14713` 「국기 이미지」).
final bool flags;
final Future<FpiRanking> future;
final String myLabel;
final String emptyLabel;
@@ -180,7 +254,8 @@ class _RankingBody extends StatelessWidget {
Widget build(BuildContext context) => FutureBuilder<FpiRanking>(
future: future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
// 다음 장을 읽는 동안에는 앞 장을 그대로 둔다 — 바퀴로 바꾸면 스크롤 자리가 사라진다.
if (!snap.hasData && snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
final data = snap.data;
@@ -189,84 +264,115 @@ class _RankingBody extends StatelessWidget {
}
final podium = scopeLabel != null && data.rows.length >= 3;
final rows = podium ? data.rows.skip(3).toList() : data.rows;
return ListView(
padding: const EdgeInsets.all(AppSpace.s4),
children: [
// 별첨 1 ② 예외처리 — 갱신 주기 · 마지막 갱신 시각을 화면에서 안내한다.
if (data.refreshMinutes case final minutes?) ...[
Text(
[
if (data.updatedAt case final at?)
context.l10n.planetLastUpdated(
'${at.toLocal().hour.toString().padLeft(2, '0')}:${at.toLocal().minute.toString().padLeft(2, '0')}',
),
context.l10n.rankRefreshEvery('$minutes'),
].join(' · '),
style: _rt(12, AppColors.onCosmicMuted, FontWeight.w500),
),
const SizedBox(height: AppSpace.s3),
],
if (data.mine case final me?) ...[
Text(myLabel, style: _rt(24, AppColors.onCosmic, FontWeight.w500)),
const SizedBox(height: AppSpace.s4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
return NotificationListener<ScrollNotification>(
onNotification: (n) {
if (n.metrics.extentAfter < 200) onEnd?.call();
return false;
},
child: ListView(
padding: const EdgeInsets.all(AppSpace.s4),
children: [
// 별첨 1 ② 예외처리 — 갱신 주기 · 마지막 갱신 시각을 화면에서 안내한다.
if (data.refreshMinutes case final minutes?) ...[
Text(
[
if (data.updatedAt case final at?)
context.l10n.planetLastUpdated(
'${at.toLocal().hour.toString().padLeft(2, '0')}:${at.toLocal().minute.toString().padLeft(2, '0')}',
),
context.l10n.rankRefreshEvery('$minutes'),
].join(' · '),
style: _rt(12, AppColors.onCosmicMuted, FontWeight.w500),
),
const SizedBox(height: AppSpace.s3),
],
if (data.mine case final me?) ...[
Text(
myLabel,
style: _rt(24, AppColors.onCosmic, FontWeight.w500),
),
const SizedBox(height: AppSpace.s4),
Material(
color: const Color(0xFF19191C),
borderRadius: BorderRadius.circular(8),
),
child: _RankRow(row: me, index: -1, planet: planets, mark: false),
),
const SizedBox(height: 24),
],
if (scopeLabel case final label?) ...[
InkWell(
onTap: onPickScope,
child: Row(
children: [
Text(
label,
style: _rt(
18,
AppColors.onCosmic,
FontWeight.w600,
height: null,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTapMine == null ? null : () => onTapMine!(me),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: _RankRow(
row: me,
index: -1,
planet: planets,
flag: flags,
mark: false,
),
),
const SizedBox(width: 4),
const Icon(
Icons.keyboard_arrow_down,
size: 20,
color: AppColors.onCosmic,
),
],
),
),
),
const SizedBox(height: 12),
],
if (data.rows.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 48),
child: Center(child: Text(emptyLabel)),
),
if (podium) ...[
_Podium(rows: data.rows.take(3).toList()),
const SizedBox(height: 12),
],
for (final (i, row) in rows.indexed)
InkWell(
onTap: onTapRow == null ? null : () => onTapRow!(row),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: _RankRow(row: row, index: i),
const SizedBox(height: 24),
],
if (scopeLabel case final label?) ...[
InkWell(
onTap: onPickScope,
child: Row(
children: [
Text(
label,
style: _rt(
18,
AppColors.onCosmic,
FontWeight.w600,
height: null,
),
),
const SizedBox(width: 4),
const Icon(
Icons.keyboard_arrow_down,
size: 20,
color: AppColors.onCosmic,
),
],
),
),
),
],
const SizedBox(height: 12),
],
if (data.rows.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 48),
child: Center(child: Text(emptyLabel)),
),
if (podium) ...[
_Podium(rows: data.rows.take(3).toList()),
const SizedBox(height: 12),
],
for (final (i, row) in rows.indexed)
InkWell(
onTap: onTapRow == null ? null : () => onTapRow!(row),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: _RankRow(row: row, index: i, planet: planets, flag: flags),
),
),
],
),
);
},
);
}
/// 국가 코드 → 국기 이모지. 미지정(`ZZ`) · 형식 밖은 흰 깃발.
String flagOf(String code) {
final c = code.toUpperCase();
if (c.length != 2 || c == 'ZZ' || !RegExp(r'^[A-Z]{2}$').hasMatch(c)) {
return '\u{1F3F3}';
}
return String.fromCharCodes(c.codeUnits.map((u) => 0x1F1E6 + u - 65));
}
TextStyle _rt(
double size,
Color color,
@@ -302,11 +408,14 @@ const _avatarColors = [
Color(0xFFD2F5B8),
];
Widget _avatar(int index, double size, {bool planet = false}) => planet
? Image.asset(
'assets/figma/planet-detail-hero.png',
width: size,
height: size,
Widget _avatar(int index, double size, {bool planet = false, FpiRankRow? row}) => planet
? SizedBox.square(
dimension: size,
child: PlanetArt(
fallback: 'assets/figma/planet-detail-hero.png',
planetType: row?.planetType,
colorKey: row?.colorKey,
),
)
: Container(
width: size,
@@ -348,12 +457,14 @@ class _RankRow extends StatelessWidget {
required this.row,
required this.index,
this.planet = false,
this.flag = false,
this.mark = true,
});
final FpiRankRow row;
final int index;
final bool planet;
final bool flag;
/// 목록 안 내 행에 「(나)」를 붙인다. 위 내 줄 카드는 시안대로 이름만 둔다.
final bool mark;
@@ -381,7 +492,14 @@ class _RankRow extends StatelessWidget {
// 사본 — 40 프레임 가운데 지름 36 원.
SizedBox.square(
dimension: 40,
child: Center(child: _avatar(index, 36, planet: planet)),
child: Center(
child: flag
? Text(
flagOf(row.id),
style: const TextStyle(fontSize: 28, height: 1),
)
: _avatar(index, 36, planet: planet, row: row),
),
),
const SizedBox(width: 6),
Expanded(
+31
View File
@@ -32,6 +32,26 @@ enum FpiMetric {
final String label;
}
/// 팬덤 규모 — Figma 행성 정보 영역 `544:19765` 「소형 : 1,000명 미만 · 중형 : 50,000명 미만 ·
/// 대형 : 50,000명 이상」. 기준은 가입자 수다(행성 상세 · GALAXY 검색 결과가 같이 쓴다).
enum FandomSize {
small,
medium,
large;
static FandomSize of(int members) => members < 1000
? small
: members < 50000
? medium
: large;
String get label => switch (this) {
small => appL10n.planetSizeSmall,
medium => appL10n.planetSizeMedium,
large => appL10n.planetSizeLarge,
};
}
/// 지표별 값. 명세 — *값 · 전체 순위 · 최근 동향 · 내 기여도 상위 값*.
class FpiScore {
const FpiScore({
@@ -93,8 +113,14 @@ class FpiRankRow {
required this.value,
this.change = 0,
this.isMine = false,
this.planetType,
this.colorKey,
});
/// 행성 랭킹 줄의 행성 그림(Figma `546:14629` 「행성 이미지」) — 생성 때 고른 형태 · 색. 없으면 기본 그림.
final String? planetType;
final String? colorKey;
/// 행성 랭킹이면 `fandomId`, 사용자 랭킹이면 `uid`, 국가 랭킹이면 국가 코드.
final String id;
@@ -272,8 +298,13 @@ class PlanetDetail {
this.growthPop = 0,
this.nebulaTargetPop = 1000,
this.nebulaTargetMembers = 10,
this.isOwner = false,
});
/// 내가 만든 행성/성운인가(`fandoms.founderUid`). Figma `544:19765` 「가입 버튼 - 내가 생성했을 경우
/// hide」 · `543:19721` 「(타인 행성) 신고/차단 · (내 행성) 편집」 — 편집은 C-50 으로 두지 않는다.
final bool isOwner;
/// 성운 → 정식 행성 전환 조건 — 서버와 같은 `config/policy`(원장 B-10 · `nebulaMinPop` · `nebulaMinMembers`).
final int nebulaTargetPop;
final int nebulaTargetMembers;
@@ -148,7 +148,8 @@ class _PlanetDetailScreenState extends State<PlanetDetailScreen> {
actions: [
IconButton(
tooltip: context.l10n.commonMore,
onPressed: () => _showMore(context, d.name),
onPressed: () =>
_showMore(context, d.name, mine: d.isOwner),
icon: const ImageIcon(
AppIcons.ellipsisVertical,
size: 24,
@@ -169,6 +170,7 @@ class _PlanetDetailScreenState extends State<PlanetDetailScreen> {
ownerName: d.ownerName,
ownerPhoto: d.ownerPhoto,
joined: d.joined,
isOwner: d.isOwner,
onJoin: d.joined ? _leave : _join,
onOpenHistory: () => Navigator.of(context).push(
MaterialPageRoute<void>(
@@ -209,7 +211,9 @@ class _PlanetDetailScreenState extends State<PlanetDetailScreen> {
},
);
void _showMore(BuildContext context, String name) =>
/// Figma `543:19721` — 더보기: (타인 행성) 신고/차단 · (내 행성) 편집. 편집은 행성 사용자 커스텀이라
/// 두지 않는다(원장 C-50 「생성 후 사용자가 외형을 바꾸는 진입점 0건」). 내 행성에는 신고 · 차단도 없다.
void _showMore(BuildContext context, String name, {required bool mine}) =>
showActionSheet(context, [
// 팬덤 역사는 시안 밖 기능이라 남기되 중립색으로 둔다.
ActionSheetItem(context.l10n.planetPassportHistory, () {
@@ -222,14 +226,16 @@ class _PlanetDetailScreenState extends State<PlanetDetailScreen> {
),
);
}, destructive: false),
ActionSheetItem(
context.l10n.reportTitle,
() => _moderate(context, 'report', name),
),
ActionSheetItem(
context.l10n.commonBlockAction,
() => _moderate(context, 'block', name),
),
if (!mine) ...[
ActionSheetItem(
context.l10n.reportTitle,
() => _moderate(context, 'report', name),
),
ActionSheetItem(
context.l10n.commonBlockAction,
() => _moderate(context, 'block', name),
),
],
]);
Future<void> _moderate(
@@ -459,6 +465,7 @@ class _FpiTab extends StatelessWidget {
EclipseBanner(
repository: eclipse,
fandomId: detail.fandomId,
nebula: detail.isNebula,
margin: const EdgeInsets.only(bottom: AppSpace.s3),
),
Container(
@@ -177,6 +177,12 @@ class FirestorePlanetRepository implements PlanetRepository {
final member = memberSnap.data() ?? const <String, dynamic>{};
final posts = await postsF;
final targets = nebulaTargetsFrom((await policyF).data());
// 생성자 = `founderUid`(성운 생성자 · 탄생 전 탈퇴 시 승계자, C-100). 프로필 · 닉네임은 그 멤버 문서에서
// 읽는다(Figma `544:19765` 「생성자 프로필 · 닉네임」). 떠났거나 시드 행성이면 줄을 그리지 않는다.
final founderUid = fandom.strOr('founderUid');
final founder = founderUid.isEmpty
? null
: (await db.doc('fandoms/$fandomId/members/$founderUid').get()).data();
final axes = _map(fandom, 'fpiAxes');
final ranks = _map(fandom, 'fpiRanks');
@@ -199,6 +205,8 @@ class FirestorePlanetRepository implements PlanetRepository {
_ => null,
};
return PlanetDetail(
isOwner: founderUid.isNotEmpty && founderUid == uid,
ownerName: founder == null ? null : publicName(founder['nickname']),
joined: memberSnap.exists,
stage: stage,
fandomId: fandomId,
@@ -325,6 +333,8 @@ class FirestorePlanetRepository implements PlanetRepository {
// 순위 동향 = 직전 순위 − 현재 순위. 둘 다 서버가 적은 값이다.
change: _map(data, 'fpiRanksPrev').intOr(axis, rank) - rank,
isMine: id == fandomId,
planetType: data['planetType'] is String ? data['planetType'] as String : null,
colorKey: data['colorKey'] is String ? data['colorKey'] as String : null,
);
}
@@ -335,7 +345,11 @@ class FirestorePlanetRepository implements PlanetRepository {
required String fandomId,
int limit = 30,
}) async {
final ranking = await _userRanking(metric: metric, fandomId: fandomId, limit: limit);
final ranking = await _userRanking(
metric: metric,
fandomId: fandomId,
limit: limit,
);
final fandom = (await _backend.db.doc('fandoms/$fandomId').get()).data();
return ranking.withUpdate(_time(fandom?['memberRankedAt']), 5);
}
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import '../../../core/theme/tokens.dart';
import '../planet_data.dart';
/// 팬덤 규모 칩 — Figma 「대형 팬덤」 `#AEC054` · 「중형 / 소형 팬덤」 `#54C07D`(바탕 10%, r4).
/// 행성 상세 히어로는 12/500 + 1px 테두리, GALAXY 목록은 10/500.
class FandomSizeBadge extends StatelessWidget {
const FandomSizeBadge({super.key, required this.members, this.large = false});
final int members;
/// 행성 상세 히어로 크기(12 · 테두리).
final bool large;
@override
Widget build(BuildContext context) {
final size = FandomSize.of(members);
final color = size == FandomSize.large
? const Color(0xFFAEC054)
: const Color(0xFF54C07D);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
border: large ? Border.all(color: color) : null,
borderRadius: BorderRadius.circular(4),
),
child: Text(
size.label,
style: TextStyle(
fontFamily: AppFonts.body,
fontSize: large ? 12 : 10,
fontWeight: FontWeight.w500,
height: 1,
color: color,
),
),
);
}
}
@@ -7,6 +7,7 @@ import '../../../core/ui/live_planet.dart';
import '../../../core/data/planet_visual_state.dart';
import '../../home/home_screen.dart' show formatPop;
import '../../../core/i18n/app_locale.dart';
import 'fandom_size_badge.dart';
/// 행성 상세 히어로 — 시안 `373:441117` 의 `Frame 1984081117`(375×543).
///
@@ -24,15 +25,20 @@ class PlanetDetailHero extends StatelessWidget {
this.nebula = false,
this.onHeroParticipate,
this.onJoin,
this.isOwner = false,
this.onOpenHistory,
this.fandomId = '',
this.watchPlanet = noPlanetWatch,
this.stage,
this.ownerName,
this.ownerPhoto,
this.sizeLabel,
this.onOpenOwner,
});
/// 개설자 줄을 눌렀을 때 — Figma `544:19765` 「프로필 및 닉네임 클릭 시 해당 사용자의 MY로 이동」.
/// 타인 프로필 화면(`549:21969`)이 생기면 연결한다(3차 MY). null 이면 누르지 않는다.
final VoidCallback? onOpenOwner;
final VoidCallback? onOpenHistory;
final String fandomId;
final PlanetWatch watchPlanet;
@@ -46,6 +52,9 @@ class PlanetDetailHero extends StatelessWidget {
/// 알약을 눌렀을 때 — 「가입하기」면 가입, 「가입중」이면 탈퇴 시트. null 이면 상태만 보인다.
final VoidCallback? onJoin;
/// 내가 만든 행성이면 가입 알약을 숨긴다(Figma `544:19765` 「가입 버튼 - 내가 생성했을 경우 hide」).
final bool isOwner;
/// 성운 상세(`373:442708`)는 규모 칩과 FPI 줄이 없다.
final bool nebula;
@@ -56,9 +65,6 @@ class PlanetDetailHero extends StatelessWidget {
/// 개설자 사진(URL 또는 에셋). 없으면 회색 원.
final String? ownerPhoto;
/// 팬덤 규모 라벨. 정책 §P-08 등급명이 붙기 전까지 시안 문구를 쓴다.
final String? sizeLabel;
static const double designHeight = 543;
/// 시안의 상태 표시줄 + 내비 높이. 본문은 그 아래 439 안에 가운데 놓인다.
@@ -74,7 +80,8 @@ class PlanetDetailHero extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1인당 화력 — 시안 「1.44」. 총 FPI 를 멤버 수로 나눈 값이며 정책 확정 전 표시용이다.
// 1인당 화력 — 시안 「1.44」. Figma `544:19765` 정의(「모든 포인트 획득, 소비 활동 및 1인당 활동 값을
// 집계한 값」)와 FPI 원천(C-34 활동 기준)이 갈려 해석 확인 중(전수대조 영역A Q-A23). 회신 전 임시로 총 FPI ÷ 멤버 수.
final perHead = members == 0 ? 0.0 : totalFpi / members;
const info = Color(0xFFDEDEDE);
@@ -111,31 +118,36 @@ class PlanetDetailHero extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (ownerName case final owner?) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 13,
backgroundColor: const Color(0xFFD9D9D9),
backgroundImage: switch (ownerPhoto) {
final p? when p.startsWith('http') => NetworkImage(p),
final p? => AssetImage(p),
null => null,
},
),
const SizedBox(width: 9),
Text(
owner,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.3,
letterSpacing: -0.4,
color: AppColors.onCosmic,
GestureDetector(
onTap: onOpenOwner,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 13,
backgroundColor: const Color(0xFFD9D9D9),
backgroundImage: switch (ownerPhoto) {
final p? when p.startsWith('http') => NetworkImage(
p,
),
final p? => AssetImage(p),
null => null,
},
),
),
],
const SizedBox(width: 9),
Text(
owner,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.3,
letterSpacing: -0.4,
color: AppColors.onCosmic,
),
),
],
),
),
const SizedBox(height: 25),
],
@@ -159,21 +171,8 @@ class PlanetDetailHero extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (!nebula) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0x1A54C07D),
border: Border.all(color: const Color(0xFF54C07D)),
borderRadius: BorderRadius.circular(4),
),
child: Text(
sizeLabel ?? context.l10n.planetSizeMedium,
style: _body(12, const Color(0xFF54C07D)),
),
),
// 소형 · 중형 · 대형 — 가입자 수 기준(Figma `544:19765`).
FandomSizeBadge(members: members, large: true),
const SizedBox(width: 4),
],
Image.asset(
@@ -227,31 +226,32 @@ class PlanetDetailHero extends StatelessWidget {
),
const SizedBox(height: 12),
],
Semantics(
button: onJoin != null,
child: GestureDetector(
onTap: onJoin,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
joined
? context.l10n.planetJoined
: context.l10n.planetJoin,
style: _body(
14,
const Color(0xFFD5D7D9),
).copyWith(letterSpacing: -0.35),
if (!isOwner)
Semantics(
button: onJoin != null,
child: GestureDetector(
onTap: onJoin,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
joined
? context.l10n.planetJoined
: context.l10n.planetJoin,
style: _body(
14,
const Color(0xFFD5D7D9),
).copyWith(letterSpacing: -0.35),
),
),
),
),
),
],
),
),