fix(flutter): 다이어리 시안 점검 반영

- 기록을 남기고 돌아오면 달력이 아니라 피드로 보여 준다.
- 기록 카드: 폴라로이드 위치, 테이프 양옆 흰 점선, 댓글 입력·수정칸 여백을 시안대로 맞추고
  입력 중이면 주황 테두리를 둔다. 등록 버튼은 제목 서체 800 으로.
- 기록 수정 시트: 글칸 최소 높이 120(작성 화면과 같게), 라벨 간격, 종류 선택에 아이콘을 붙였다.
- 다이어리에 참여하면 시트를 닫고, 표지 리본·그림자와 무지개 색 시작 위치(12시)를 맞췄다.
- 검색창 포커스 테두리, 다이어리 머리 여백을 시안 값으로. 다이어리 제거는 전용 확인창으로.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 01:24:25 +09:00
parent e6f5cd0720
commit 0c16763943
5 changed files with 163 additions and 47 deletions
@@ -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: '수정 완료',
// 비우면 원래 글을 그대로 둔다 (시안)