generated from ttipo/project-base
fix(flutter): 다이어리 시안 점검 반영
- 기록을 남기고 돌아오면 달력이 아니라 피드로 보여 준다. - 기록 카드: 폴라로이드 위치, 테이프 양옆 흰 점선, 댓글 입력·수정칸 여백을 시안대로 맞추고 입력 중이면 주황 테두리를 둔다. 등록 버튼은 제목 서체 800 으로. - 기록 수정 시트: 글칸 최소 높이 120(작성 화면과 같게), 라벨 간격, 종류 선택에 아이콘을 붙였다. - 다이어리에 참여하면 시트를 닫고, 표지 리본·그림자와 무지개 색 시작 위치(12시)를 맞췄다. - 검색창 포커스 테두리, 다이어리 머리 여백을 시안 값으로. 다이어리 제거는 전용 확인창으로. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,8 @@ class _DiaryComposePageState extends ConsumerState<DiaryComposePage> {
|
||||
photoPath: _photo,
|
||||
);
|
||||
toast.show('기록을 남겼어요');
|
||||
if (mounted) context.pop();
|
||||
// 남겼음을 알려 다이어리가 피드 보기로 돌아가게 한다
|
||||
if (mounted) context.pop(true);
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ enum FeedMode {
|
||||
/// 탭바 뒤로 콘텐츠가 가려지지 않게 두는 여유 (추억 보관함에서 열 때)
|
||||
const double _kTabBottomGap = 30;
|
||||
|
||||
/// 추억 보관함 안에 끼워 열 때 머리가 위아래로 2씩 좁다 (시안 embedded margin 2px 0 16px)
|
||||
/// 다이어리 머리가 위아래로 2씩 좁다 (시안 embedded margin 2px 0 16px — 기록 탭·보관함 모두)
|
||||
const double _kEmbeddedTrim = 2;
|
||||
|
||||
/// 다이어리 안 — 기록 피드(모두·사진·글) / 달력 보기, 검색, 오늘 쓰기
|
||||
@@ -92,7 +92,13 @@ class _DiaryPageState extends ConsumerState<DiaryPage> {
|
||||
ref.read(toastProvider.notifier).show('오늘 기록은 이미 남겼어요. 내일 다시 작성할 수 있어요');
|
||||
return;
|
||||
}
|
||||
unawaited(context.push(AppRoutes.diaryWrite(widget.diaryId)));
|
||||
unawaited(_openCompose());
|
||||
}
|
||||
|
||||
/// 기록하기 — 남기고 돌아오면 달력이 아니라 피드로 보여 준다
|
||||
Future<void> _openCompose() async {
|
||||
final saved = await context.push<bool>(AppRoutes.diaryWrite(widget.diaryId));
|
||||
if (saved == true && mounted) setState(() => _calendar = false);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -119,7 +125,7 @@ class _DiaryPageState extends ConsumerState<DiaryPage> {
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
AppSpacing.xl,
|
||||
AppSpacing.xs - (archived ? _kEmbeddedTrim : 0),
|
||||
AppSpacing.xs - _kEmbeddedTrim,
|
||||
AppSpacing.xl,
|
||||
safeBottom + (archived ? _kTabBottomGap : _kFabGap + _kFabSize),
|
||||
),
|
||||
@@ -129,7 +135,7 @@ class _DiaryPageState extends ConsumerState<DiaryPage> {
|
||||
subtitle: '기록 ${posts.length}개',
|
||||
subtitleLeading: archived ? const RupGlyphIcon(RupGlyph.memory, size: 12, color: AppColors.memory) : null,
|
||||
displayFont: true,
|
||||
bottomGap: archived ? 18 - _kEmbeddedTrim : 18,
|
||||
bottomGap: 18 - _kEmbeddedTrim,
|
||||
onBack: () => context.pop(),
|
||||
trailing: archived
|
||||
? null
|
||||
@@ -184,13 +190,13 @@ class _DiaryPageState extends ConsumerState<DiaryPage> {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
|
||||
child: SectionTitle(title: _mode.title, sub: true),
|
||||
child: SectionTitle(title: _mode.title, sub: true, subDisplayFont: true),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
if (posts.isEmpty)
|
||||
RupCard(
|
||||
child: searching
|
||||
? const EmptyMessage('검색 결과가 없어요', padding: 40)
|
||||
? const EmptyMessage('검색 결과가 없어요')
|
||||
: _NoPosts(onWrite: wroteToday ? null : () => _write(false)),
|
||||
)
|
||||
else if (_mode == FeedMode.image && shown.isEmpty)
|
||||
@@ -324,11 +330,33 @@ class _HeaderIconButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 검색칸 — 돋보기 + 지우기
|
||||
class _SearchField extends StatelessWidget {
|
||||
/// 입력 중이면 주황 테두리 (시안 .rup-input:focus)
|
||||
class _SearchField extends StatefulWidget {
|
||||
const _SearchField({required this.controller});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
State<_SearchField> createState() => _SearchFieldState();
|
||||
}
|
||||
|
||||
class _SearchFieldState extends State<_SearchField> {
|
||||
final _focus = FocusNode();
|
||||
|
||||
TextEditingController get controller => widget.controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focus.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 14, height: 1.2);
|
||||
@@ -338,7 +366,7 @@ class _SearchField extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(color: AppColors.line, width: 1.5),
|
||||
border: Border.all(color: _focus.hasFocus ? AppColors.primary : AppColors.line, width: 1.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -347,6 +375,7 @@ class _SearchField extends StatelessWidget {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: _focus,
|
||||
style: style,
|
||||
cursorColor: AppColors.primary,
|
||||
textInputAction: TextInputAction.search,
|
||||
|
||||
@@ -39,13 +39,10 @@ class DiarySettingsPage extends ConsumerWidget {
|
||||
final String diaryId;
|
||||
|
||||
Future<void> _delete(BuildContext context, WidgetRef ref, Diary diary) async {
|
||||
final ok = await showRupConfirm(
|
||||
final ok = await showRupRemoveConfirm(
|
||||
context,
|
||||
title: '다이어리를 제거할까요?',
|
||||
message: '다이어리를 삭제하면 다시 복구할 수 없으며,\n이때까지 적은 기록이 전부 삭제돼요.',
|
||||
confirmLabel: '제거하기',
|
||||
destructive: true,
|
||||
layout: RupConfirmLayout.row,
|
||||
);
|
||||
if (!ok || !context.mounted) return;
|
||||
// 먼저 책장으로 돌아가 지워진 다이어리를 그리지 않게 한다
|
||||
@@ -143,7 +140,7 @@ class DiarySettingsPage extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
const SizedBox(width: 13),
|
||||
HeartToggle(
|
||||
value: diary.notifies(row.notice),
|
||||
label: row.title,
|
||||
|
||||
@@ -121,8 +121,9 @@ class _PolaroidPostState extends State<PolaroidPost> {
|
||||
Widget build(BuildContext context) {
|
||||
final post = widget.post;
|
||||
final photo = post.photoPath;
|
||||
// 시안 margin 4px 2px 28px — 위 4 는 앞 요소의 아래 여백(제목 12, 앞 기록 28)과 겹쳐 사라진다
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 4, 2, 28),
|
||||
padding: const EdgeInsets.fromLTRB(2, 0, 2, 28),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
@@ -189,10 +190,40 @@ class _Tape extends StatelessWidget {
|
||||
),
|
||||
boxShadow: [BoxShadow(color: Color(0x12000000), blurRadius: 3, offset: Offset(0, 1))],
|
||||
),
|
||||
// 시안: 양옆 1px 흰 점선 (rgba(255,255,255,.55))
|
||||
foregroundDecoration: const _DashedSides(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 좌우 가장자리에 세로 점선 — CSS border-left/right: 1px dashed
|
||||
class _DashedSides extends Decoration {
|
||||
const _DashedSides();
|
||||
|
||||
@override
|
||||
BoxPainter createBoxPainter([VoidCallback? onChanged]) => _DashedSidesPainter();
|
||||
}
|
||||
|
||||
class _DashedSidesPainter extends BoxPainter {
|
||||
static const double _dash = 3;
|
||||
static const Color _color = Color(0x8CFFFFFF);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
|
||||
final size = configuration.size;
|
||||
if (size == null) return;
|
||||
final paint = Paint()
|
||||
..color = _color
|
||||
..strokeWidth = 1;
|
||||
final bottom = offset.dy + size.height;
|
||||
for (final x in [offset.dx + .5, offset.dx + size.width - .5]) {
|
||||
for (var y = offset.dy; y < bottom; y += _dash * 2) {
|
||||
canvas.drawLine(Offset(x, y), Offset(x, y + _dash > bottom ? bottom : y + _dash), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// "댓글 N" — 누르면 댓글을 펼친다
|
||||
class _CommentToggle extends ConsumerWidget {
|
||||
const _CommentToggle({required this.postId, required this.open, required this.onTap});
|
||||
@@ -282,6 +313,7 @@ class _CommentsState extends ConsumerState<_Comments> {
|
||||
message: '삭제한 댓글은 다시 볼 수 없어요.',
|
||||
confirmLabel: '삭제하기',
|
||||
layout: RupConfirmLayout.row,
|
||||
tightTop: true,
|
||||
);
|
||||
if (ok) await ref.read(diaryProvider.notifier).deleteComment(c.id);
|
||||
}
|
||||
@@ -328,14 +360,18 @@ class _CommentsState extends ConsumerState<_Comments> {
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
// 시안: 수정칸 위 5, 댓글 글 위 6
|
||||
SizedBox(height: _editingId == c.id ? 5 : 6),
|
||||
if (_editingId == c.id)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _CommentInput(controller: _edit, autofocus: true, onSubmit: _saveEdit)),
|
||||
Expanded(
|
||||
child: _CommentInput(controller: _edit, autofocus: true, compact: true, onSubmit: _saveEdit),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_SoftIconButton(
|
||||
label: '수정 저장',
|
||||
horizontal: 13,
|
||||
onTap: () => unawaited(_saveEdit()),
|
||||
child: const RupIcon(RupIconName.check, size: 16, stroke: 2.4, color: AppColors.primary600),
|
||||
),
|
||||
@@ -357,13 +393,11 @@ class _CommentsState extends ConsumerState<_Comments> {
|
||||
child: _CommentInput(controller: _draft, hint: '댓글 달기…', onSubmit: _add),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Opacity(
|
||||
opacity: _draft.text.trim().isEmpty ? .5 : 1,
|
||||
child: _SoftIconButton(
|
||||
label: '등록',
|
||||
onTap: () => unawaited(_add()),
|
||||
child: Text('등록', style: _ui(context, 13, FontWeight.w700, AppColors.primary600)),
|
||||
),
|
||||
// 시안 .rup-btn — 제목 서체 800, 비어 있어도 모양은 그대로 (누르면 아무 일도 없다)
|
||||
_SoftIconButton(
|
||||
label: '등록',
|
||||
onTap: () => unawaited(_add()),
|
||||
child: Text('등록', style: AppTypography.button(13, color: AppColors.primary600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -387,12 +421,15 @@ class _SmallAction extends StatelessWidget {
|
||||
|
||||
/// 연주황 작은 버튼 (.rup-btn--soft) — 등록 · ✓
|
||||
class _SoftIconButton extends StatelessWidget {
|
||||
const _SoftIconButton({required this.label, required this.onTap, required this.child});
|
||||
const _SoftIconButton({required this.label, required this.onTap, required this.child, this.horizontal = 14});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
|
||||
/// 좌우 여백 — 등록 14, 수정 저장 13
|
||||
final double horizontal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
@@ -402,7 +439,7 @@ class _SoftIconButton extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: 9),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary50,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
@@ -415,34 +452,64 @@ class _SoftIconButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 댓글 입력칸 — 50자까지
|
||||
class _CommentInput extends StatelessWidget {
|
||||
const _CommentInput({required this.controller, required this.onSubmit, this.hint, this.autofocus = false});
|
||||
/// 댓글 입력칸 — 50자까지. 입력 중이면 주황 테두리 (시안 .rup-input:focus)
|
||||
class _CommentInput extends StatefulWidget {
|
||||
const _CommentInput({
|
||||
required this.controller,
|
||||
required this.onSubmit,
|
||||
this.hint,
|
||||
this.autofocus = false,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final Future<void> Function() onSubmit;
|
||||
final String? hint;
|
||||
final bool autofocus;
|
||||
|
||||
/// 댓글 수정칸 — 여백 8/11, 모서리 9 (새 댓글은 9/12, 10)
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
State<_CommentInput> createState() => _CommentInputState();
|
||||
}
|
||||
|
||||
class _CommentInputState extends State<_CommentInput> {
|
||||
final _focus = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focus.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _ui(context, 13.5, FontWeight.w400, AppColors.ink, height: 1.2);
|
||||
final compact = widget.compact;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||
padding: EdgeInsets.symmetric(horizontal: compact ? 11 : 12, vertical: compact ? 8 : 9),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(color: AppColors.line, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(compact ? 9 : AppRadius.card),
|
||||
border: Border.all(color: _focus.hasFocus ? AppColors.primary : AppColors.line, width: 1.5),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
autofocus: autofocus,
|
||||
controller: widget.controller,
|
||||
focusNode: _focus,
|
||||
autofocus: widget.autofocus,
|
||||
maxLength: kCommentMaxLength,
|
||||
style: style,
|
||||
cursorColor: AppColors.primary,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => unawaited(onSubmit()),
|
||||
decoration: InputDecoration.collapsed(hintText: hint, hintStyle: style.copyWith(color: AppColors.muted))
|
||||
onSubmitted: (_) => unawaited(widget.onSubmit()),
|
||||
decoration: InputDecoration.collapsed(hintText: widget.hint, hintStyle: style.copyWith(color: AppColors.muted))
|
||||
.copyWith(counterText: ''),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -131,7 +132,10 @@ Future<void> showAddDiarySheet(BuildContext context, WidgetRef ref) {
|
||||
iconBackground: AppColors.accent100,
|
||||
title: '공유 다이어리 참여하기',
|
||||
description: '초대 코드로 함께 쓰는 다이어리에 참여해요',
|
||||
onTap: () => ref.read(toastProvider.notifier).show(kSharedDiarySoon, icon: RupIconName.users),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
ref.read(toastProvider.notifier).show(kSharedDiarySoon, icon: RupIconName.users);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -261,6 +265,7 @@ class _DiaryFormState extends ConsumerState<_DiaryForm> {
|
||||
children: [
|
||||
RupSegmented<bool>(
|
||||
options: const [(false, '개인 다이어리'), (true, '공유 다이어리')],
|
||||
icons: const [RupIconName.user, RupIconName.users],
|
||||
value: false,
|
||||
onChanged: (shared) {
|
||||
if (shared) ref.read(toastProvider.notifier).show(kSharedDiarySoon, icon: RupIconName.users);
|
||||
@@ -304,6 +309,8 @@ class _DiaryFormState extends ConsumerState<_DiaryForm> {
|
||||
label: '책갈피 리본 색상',
|
||||
child: RibbonPicker(color: _ribbon, onPick: (c) => setState(() => _ribbon = c), onCustom: _pickCustomRibbon),
|
||||
),
|
||||
// 시안 .rup-field 아래 16 (바닥 버튼 위 18 과 겹치지 않는다)
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
RupSheetBleedButton(
|
||||
label: _editing ? '수정 완료' : '다이어리 만들기',
|
||||
onPressed: () => unawaited(_submit()),
|
||||
@@ -324,10 +331,14 @@ class RibbonPicker extends StatelessWidget {
|
||||
|
||||
static const double _size = 36;
|
||||
|
||||
static const _rainbow = SweepGradient(colors: [
|
||||
Color(0xFFFF7500), Color(0xFFFF7AA0), Color(0xFF8E7CF0), Color(0xFF4DA6E0),
|
||||
Color(0xFF3FB58C), Color(0xFFFFD43B), Color(0xFFFF7500),
|
||||
]);
|
||||
// CSS conic-gradient 는 12시에서 시작한다 (Flutter 기본은 3시)
|
||||
static const _rainbow = SweepGradient(
|
||||
transform: GradientRotation(-math.pi / 2),
|
||||
colors: [
|
||||
Color(0xFFFF7500), Color(0xFFFF7AA0), Color(0xFF8E7CF0), Color(0xFF4DA6E0),
|
||||
Color(0xFF3FB58C), Color(0xFFFFD43B), Color(0xFFFF7500),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -384,11 +395,17 @@ class RibbonPicker extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
child: custom
|
||||
? check(color)
|
||||
: const RupIcon(
|
||||
RupIconName.plus,
|
||||
size: 16,
|
||||
stroke: 2.6,
|
||||
color: Colors.white,
|
||||
: const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [BoxShadow(color: Color(0x33000000), offset: Offset(0, 1), blurRadius: 2)],
|
||||
),
|
||||
child: RupIcon(
|
||||
RupIconName.plus,
|
||||
size: 16,
|
||||
stroke: 2.6,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -439,6 +456,9 @@ Future<PostAction?> showPostMenuSheet(BuildContext context) {
|
||||
/// 지우기·나가기 등 위험한 동작의 색 (시안 #E05A4A)
|
||||
const Color kDangerTone = Color(0xFFE05A4A);
|
||||
|
||||
/// 기록 수정 글칸 높이 (시안 minHeight 120 — 작성 화면과 같다)
|
||||
const double _kEditTextMinHeight = 120;
|
||||
|
||||
/// 기록 글 수정 — 저장하면 새 글, 닫으면 null
|
||||
Future<String?> showEditPostSheet(BuildContext context, String initial) {
|
||||
return showRupSheet<String>(
|
||||
@@ -477,8 +497,10 @@ class _EditPostBodyState extends State<_EditPostBody> {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
RupField(
|
||||
label: '오늘 하루는 어떠셨나요?',
|
||||
child: RupTextField(controller: _text, multiline: true),
|
||||
labelGap: AppSpacing.md,
|
||||
child: RupTextField(controller: _text, multiline: true, minHeight: _kEditTextMinHeight),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
RupSheetBleedButton(
|
||||
label: '수정 완료',
|
||||
// 비우면 원래 글을 그대로 둔다 (시안)
|
||||
|
||||
Reference in New Issue
Block a user