20260319 #689 전체 애니메이션 개선 및 통일감 확보#745
Hidden character warning
Conversation
…on 생성 및 개선. 모션토큰에 대한 명세 추가 #689
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (2)
Walkthrough앱 전역의 애니메이션 상수를 중앙화하는 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
lib/widgets/common/custom_floating_button.dart (1)
57-79:⚠️ Potential issue | 🔴 Critical중복 탭 핸들러로 인해 콜백이 두 번 실행됩니다.
AppPressable(Line 58)과InkWell(Line 69) 모두 동일한onTap콜백을 등록하고 있습니다.AppPressable은 내부적으로GestureDetector를 사용하고,InkWell도 별도로 탭을 처리하므로 사용자가 버튼을 누르면enabledOnPressed또는disabledOnPressed가 두 번 호출됩니다.
lib/widgets/common/app_pressable.dart:159-169의 구현을 보면,enableRipple이 기본값(true)일 때AppPressable내부에서 이미InkWell을 사용합니다. 따라서 내부InkWell의onTap을 제거하거나,AppPressable의onTap을null로 설정해야 합니다.🐛 InkWell onTap 제거 제안
return Center( child: AppPressable( onTap: isEnabled ? enabledOnPressed : disabledOnPressed, enabled: isEnabled, borderRadius: BorderRadius.circular(10.r), child: SizedBox( width: buttonWidth.w, height: buttonHeight.h, child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(10.r), - child: InkWell( - customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.r)), - onTap: isEnabled ? enabledOnPressed : disabledOnPressed, - highlightColor: highlightColor, - splashColor: splashColor, - borderRadius: BorderRadius.circular(10.r), - child: Center( - child: Text(buttonText, style: buttonTextStyle, textAlign: TextAlign.center, softWrap: false), - ), - ), + child: Center( + child: Text(buttonText, style: buttonTextStyle, textAlign: TextAlign.center, softWrap: false), + ), ), ), ), );또는 ripple 효과를 유지하려면
AppPressable에enableRipple: true와rippleColor: highlightColor를 설정하고 내부InkWell을 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/common/custom_floating_button.dart` around lines 57 - 79, The button registers the same tap callback twice because AppPressable and the inner InkWell both set onTap; fix by removing the duplicate tap handler: either remove the inner InkWell.onTap (in the code snippet around the InkWell widget) and let AppPressable handle taps, or set AppPressable.onTap to null and keep the InkWell handler—prefer removing the InkWell.onTap so AppPressable remains the single source of truth; if you still need ripple, enable AppPressable.enableRipple = true and pass rippleColor (use highlightColor) so the visual effect is preserved while avoiding double callbacks.lib/widgets/chat_input_bar.dart (1)
97-124:⚠️ Potential issue | 🟡 MinorsuffixIcon 크기와 constraints 불일치 가능성 확인 필요
AppPressable내부Container가margin: EdgeInsets.all(4.w)와width: 40.w를 가지고 있어 실제 필요 공간은 약48.w입니다. 하지만suffixIconConstraints는maxWidth: 40.w로 설정되어 있어 클리핑이나 오버플로우가 발생할 수 있습니다.🔧 constraints 조정 제안
suffixIconConstraints: BoxConstraints( - minWidth: 40.w, - minHeight: 40.w, - maxWidth: 40.w, - maxHeight: 40.w, + minWidth: 48.w, + minHeight: 48.w, + maxWidth: 48.w, + maxHeight: 48.w, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/chat_input_bar.dart` around lines 97 - 124, The suffixIcon's visual size (AppPressable -> Container with margin: EdgeInsets.all(4.w) and width: 40.w) actually needs about 48.w, but suffixIconConstraints currently caps at maxWidth/maxHeight: 40.w which can cause clipping; update the BoxConstraints in suffixIconConstraints to at least min/maxWidth and min/maxHeight: 48.w (or remove the Container margin and keep 40.w) so the Container inside AppPressable fits without overflow — adjust either the Container width/margin or the BoxConstraints (in chat_input_bar.dart referencing AppPressable and suffixIconConstraints) to match the true rendered size.lib/screens/register_tab_screen.dart (2)
643-655:⚠️ Potential issue | 🟡 Minor코딩 가이드라인 위반:
context.navigateTo()대신MaterialPageRoute를 직접 사용하고 있습니다.코딩 가이드라인에 따르면 화면 전환 시
context.navigateTo()를 사용해야 합니다.🔧 제안된 수정
Future<void> _navigateToItemDetail(Item item) async { if (item.itemId == null) return; - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ItemDetailDescriptionScreen( + final result = await context.navigateTo( + screen: ItemDetailDescriptionScreen( itemId: item.itemId!, imageSize: Size(MediaQuery.of(context).size.width, 400.h), currentImageIndex: 0, heroTag: 'itemImage_${item.itemId}_0', isMyItem: true, isRequestManagement: false, ), - ), );As per coding guidelines: "Use
context.navigateTo()for screen navigation (MaterialPageRoute direct usage prohibited)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/register_tab_screen.dart` around lines 643 - 655, The code uses Navigator.push with MaterialPageRoute to open ItemDetailDescriptionScreen (see Navigator.push and MaterialPageRoute wrapping ItemDetailDescriptionScreen), which violates the guideline to use context.navigateTo(); replace the Navigator.push/MaterialPageRoute call with a call to context.navigateTo(...) that passes the ItemDetailDescriptionScreen (and the same parameters: itemId: item.itemId, imageSize, currentImageIndex: 0, heroTag, isMyItem: true, isRequestManagement: false) so navigation is performed via the context.navigateTo API.
664-675:⚠️ Potential issue | 🟡 Minor코딩 가이드라인 위반:
_navigateToEditItem에서도MaterialPageRoute를 직접 사용하고 있습니다.이 메서드도
context.navigateTo()를 사용하도록 수정해야 합니다.🔧 제안된 수정
Future<void> _navigateToEditItem(Item item) async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ItemModificationScreen( + final result = await context.navigateTo( + screen: ItemModificationScreen( itemId: item.itemId, onClose: () { Navigator.pop(context); }, ), - ), );As per coding guidelines: "Use
context.navigateTo()for screen navigation (MaterialPageRoute direct usage prohibited)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/register_tab_screen.dart` around lines 664 - 675, The _navigateToEditItem function currently uses MaterialPageRoute directly; replace that with the app's navigation helper by calling context.navigateTo(...) and passing a builder that returns ItemModificationScreen with the same parameters (itemId: item.itemId and onClose callback), preserving the await of the Navigator result; ensure the onClose still closes the screen appropriately (use the existing close callback) and that the awaited result variable remains named result.
🧹 Nitpick comments (8)
lib/widgets/onboarding_progress_header.dart (1)
155-190: 정사각형 인디케이터에height: 24.w를 사용하세요.프로그레스 인디케이터가 원형(정사각형 기반)이므로, iPad 반응형 대응을 위해
height도.w단위를 사용하는 것이 좋습니다. As per coding guidelines: "Useheight: N.winstead ofheight: N.hfor images and square elements"♻️ 수정 제안
// 완료된 단계 return Container( width: 24.w, - height: 24.h, + height: 24.w, decoration: const BoxDecoration(shape: BoxShape.circle, color: AppColors.primaryYellow), // 현재 단계 return Container( width: 24.w, - height: 24.h, + height: 24.w, decoration: BoxDecoration( // 대기 단계 return Container( width: 24.w, - height: 24.h, + height: 24.w, decoration: BoxDecoration(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/onboarding_progress_header.dart` around lines 155 - 190, The three circular Container widgets used for the progress indicator use height: 24.h which makes them non-square on some responsive breakpoints; update each Container (the one returning the checked icon, the one for step == widget.currentStep, and the fallback pending step) to use height: 24.w instead of 24.h so the width and height remain square across devices (look for the Containers that set width: 24.w, height: 24.h and replace the height unit to 24.w).lib/widgets/notification_item_widget.dart (1)
118-121: 정사각형 요소에는height: 24.w를 사용하세요.iPad 반응형 대응을 위해 정사각형 요소는
width와height모두.w단위를 사용해야 합니다. As per coding guidelines: "Useheight: N.w(width-based scaling) instead ofheight: N.hfor images and square elements for safer iPad responsiveness"♻️ 수정 제안
Container( width: 24.w, - height: 24.h, + height: 24.w, margin: EdgeInsets.only(top: 1.h, left: 8.w),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/notification_item_widget.dart` around lines 118 - 121, The Container that creates a square icon currently uses width: 24.w and height: 24.h; update the height to use width-based scaling so both sides use .w (change height: 24.h to height: 24.w) to keep the element square for iPad responsiveness—locate the Container inside the NotificationItemWidget (where width: 24.w is set) and make the single-line replacement.lib/widgets/home_feed_item_widget.dart (1)
169-184: 기존 코드에서MaterialPageRoute직접 사용이 발견되었습니다.이 PR의 범위는 아니지만, 코딩 가이드라인에 따르면
context.navigateTo()를 사용해야 합니다. 추후 리팩토링 시 고려해 주세요. As per coding guidelines: "Usecontext.navigateTo()for screen navigation (MaterialPageRoute direct usage prohibited)"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/home_feed_item_widget.dart` around lines 169 - 184, Replace the direct use of MaterialPageRoute in the onTap handler inside home_feed_item_widget (the async onTap that awaits Navigator.push) with the project's navigation helper by calling context.navigateTo<dynamic>(...) and passing the ItemDetailDescriptionScreen instance with the same arguments (itemId: widget.item.itemUuid ?? '', imageSize: Size(screenWidth, screenWidth), currentImageIndex: index, heroTag: 'itemImage_${widget.item.itemUuid ?? widget.item.id}_$index', homeFeedItem: widget.item, isMyItem: false, isRequestManagement: false, isTradeRequestAllowed: true) so the call still awaits and captures the result variable as before.lib/widgets/common_app_bar.dart (1)
65-73: leading 아이콘의 hit test 영역 확대 권장
leadingWidth: 72.w에SizedBox.square(dimension: 32.w)를 배치한 경우, Flutter의 AppBar가 자동으로 icon을 중앙 정렬하므로 시각적 배치는 문제없습니다. 다만, AppPressable의 tap 영역이 32.w로 제한되어 72.w의 leadingWidth 공간을 충분히 활용하지 못하고 있습니다.사용성 개선을 위해 AppPressable을
SizedBox(width: 72.w, height: 56.w, child: Center(child: ...))로 감싸거나, AppPressable 자체를 leadingWidth에 맞춰 확대하여 전체 tap 영역을 활용하도록 수정하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/common_app_bar.dart` around lines 65 - 73, The leading icon's tappable area is currently limited because AppPressable wraps a SizedBox.square(dimension: 32.w) while the AppBar uses leadingWidth: 72.w; expand the hit-test area by making the AppPressable occupy the full leading area (e.g., wrap AppPressable with a SizedBox(width: 72.w, height: 56.w, child: Center(child: ...)) or change AppPressable to use width: 72.w and height: 56.w) so that the touch target matches the visual leadingWidth; update the widget containing leading (the leading: AppPressable(...) block) to center the 32.w icon inside the larger tappable container.lib/screens/chat_tab_screen.dart (1)
331-387: SliverList 내 AppFadeSlideIn 사용 시 스크롤할 때마다 애니메이션이 재실행됩니다.SliverList는 화면 밖으로 나간 아이템의 위젯을 파괴하고 다시 화면에 들어올 때 재생성합니다. AppFadeSlideIn은 StatefulWidget이므로 위젯이 재생성될 때마다 initState가 실행되고, 새로운 AnimationController를 통해 애니메이션이 처음부터 다시 시작됩니다.
이를 해결하기 위해 다음 중 하나를 검토하세요:
AutomaticKeepAliveClientMixin을 사용하여 위젯 상태를 메모리에 유지하기- 애니메이션 완료 후 상태를 추적하여 재실행되지 않도록 방지하기
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/chat_tab_screen.dart` around lines 331 - 387, The AppFadeSlideIn animation replays when SliverList rebuilds because its State is destroyed/recreated; fix by preserving its state: either make AppFadeSlideIn's State mixin with AutomaticKeepAliveClientMixin (add "with AutomaticKeepAliveClientMixin" to its State class, override get wantKeepAlive => true, and call super.build(context) so the AnimationController isn't disposed) or give each AppFadeSlideIn a stable identity so it isn't rebuilt (wrap the returned widget with a Key like Key(chatRoomDetail.chatRoomId) or pass a PageStorageKey(chatRoomDetail.chatRoomId) into AppFadeSlideIn); reference AppFadeSlideIn and chatRoomDetail.chatRoomId (and the surrounding ChatRoomListItem) when making the change.lib/screens/notification_screen.dart (1)
411-416:separatorBuilder를 제거하고itemBuilder에서 직접 처리하는 것을 고려해 보세요.
SizedBox.shrink()를 반환하는separatorBuilder는 실질적으로 구분선을 렌더링하지 않으므로,ListView.builder를 사용하거나separatorBuilder파라미터를 제거하는 것이 더 명확합니다.♻️ 제안된 수정
- child: ListView.separated( + child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: notifications.length, - separatorBuilder: (_, index) => const SizedBox.shrink(), itemBuilder: (context, index) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/notification_screen.dart` around lines 411 - 416, The ListView.separated usage currently supplies a no-op separatorBuilder returning SizedBox.shrink(); replace it with a plain ListView.builder (or remove the separatorBuilder) to make intent clear: use ListView.builder with the same shrinkWrap, physics: const NeverScrollableScrollPhysics(), itemCount: notifications.length and the existing itemBuilder logic (referencing ListView.separated, separatorBuilder, itemBuilder, notifications.length, shrinkWrap, and NeverScrollableScrollPhysics in the diff) so separators are not redundantly provided.lib/screens/register_tab_screen.dart (1)
495-500:AppPressable의enabled파라미터를 활용하여 중복 탭 방지를 단순화할 수 있습니다.현재
_isFabProcessing플래그로 내부 가드를 구현했지만,AppPressable의enabled파라미터를 사용하면 시각적 피드백과 탭 방지가 함께 처리됩니다.♻️ 제안된 수정
child: AppPressable( scaleDown: AppPressable.scaleButton, enableRipple: false, borderRadius: BorderRadius.circular(100.r), + enabled: !_isFabProcessing, onTap: () async { - if (_isFabProcessing) return; setState(() => _isFabProcessing = true);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/register_tab_screen.dart` around lines 495 - 500, Replace the manual tap-guard inside the AppPressable onTap with the widget's enabled parameter: set enabled: !_isFabProcessing on the AppPressable and remove the early-return check in the onTap handler; ensure you still flip _isFabProcessing to true at the start of the async operation and back to false when it completes or fails (inside the same async method invoked by onTap) so the visual disabled state and tap prevention are kept in sync (symbols to update: AppPressable, enabled, _isFabProcessing, onTap).lib/widgets/common/app_fade_slide_in.dart (1)
44-45:slideOffset단위 정의(px)와 실제 애니메이션 단위가 불일치합니다.Line 44-45는 px 기준으로 설명하지만, Line 67의
SlideTransition오프셋은 child 크기 비율 기반이라 위젯 크기에 따라 이동량이 달라집니다. 동일한 모션 토큰을 써도 화면별 체감이 달라질 수 있습니다.수정 제안 (px 의미를 실제로 보장)
class _AppFadeSlideInState extends State<AppFadeSlideIn> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _opacity; - late Animation<Offset> _slide; + late Animation<double> _translateY; @@ - _slide = Tween<Offset>( - begin: Offset(0, widget.slideOffset / 100), - end: Offset.zero, - ).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); + _translateY = + Tween<double>(begin: widget.slideOffset, end: 0).animate(CurvedAnimation(parent: _controller, curve: widget.curve)); @@ Widget build(BuildContext context) { return FadeTransition( opacity: _opacity, - child: SlideTransition(position: _slide, child: widget.child), + child: AnimatedBuilder( + animation: _translateY, + child: widget.child, + builder: (context, child) => Transform.translate( + offset: Offset(0, _translateY.value), + child: child, + ), + ), ); } }In Flutter, does SlideTransition.position use an Offset in logical pixels or as a fraction of the child's size?Also applies to: 66-69, 90-93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/widgets/common/app_fade_slide_in.dart` around lines 44 - 45, 현재 slideOffset 필드가 "px"로 문서화되어 있지만 SlideTransition에 전달되는 Offset은 자식 크기 대비 비율이므로 동작이 일치하지 않습니다; slideOffset을 실제 픽셀 단위로 보장하려면 build 내부에서 LayoutBuilder/BoxConstraints로 자식 높이(또는 너비, 방향에 따라)를 얻어 dyFraction = slideOffset / constraints.maxHeight(또는 적절한 축 크기)로 변환한 후 그 비율을 SlideTransition의 Offset으로 사용하도록 변경하세요 (참조: slideOffset 필드, SlideTransition 사용 부분, build 메서드에서 Tween/Animation 생성 지점). Ensure you handle zero-height defensively and update the doc comment to state that slideOffset accepts pixels and is converted to a fractional Offset using the child's size.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/screens/chat_tab_screen.dart`:
- Around line 374-376: The code uses
Navigator.of(context).push<bool>(MaterialPageRoute(builder: (_) =>
ChatRoomScreen(chatRoomId: roomId))) directly; change this to use the project
navigation helper by calling context.navigateTo(...) with the ChatRoomScreen
route instead, passing the same ChatRoomScreen(chatRoomId: roomId) as the target
and preserving the returned bool (refreshed) semantics; update the call site
where Navigator.of(...).push<bool>(MaterialPageRoute(...)) appears to use
context.navigateTo(ChatRoomScreen(chatRoomId: roomId)) or the equivalent
context.navigateTo(...) overload used in the app.
In `@lib/screens/main_screen.dart`:
- Around line 59-65: AnimatedSwitcher + KeyedSubtree
(ValueKey<int>(_currentTabIndex)) rebuilds each tab from _navigationTabScreens
and causes duplicate API calls in screens like NotificationScreen,
RegisterTabScreen, RequestManagementTabScreen (initState calls
_loadNotifications/_loadMyItems/_loadInitialItems). Replace AnimatedSwitcher
with an IndexedStack to preserve each child instance and apply fade with an
AnimatedOpacity (or FadeTransition) on the active child to keep the visual
animation, or alternatively implement AutomaticKeepAliveClientMixin in the
mentioned screens so their state and API calls are preserved; update
main_screen.dart to use IndexedStack(_navigationTabScreens) and control opacity
for the selected index or add keep-alive mixin to
NotificationScreen/RegisterTabScreen/RequestManagementTabScreen to prevent
reinitialization.
In `@lib/screens/request_management_tab_screen.dart`:
- Around line 763-764: The code force-unwraps giveItem.address and
giveItem.createdDate (address: giveItem.address!, createdDate:
giveItem.createdDate!), which can crash at runtime; change these to safe
null-aware values by checking for null and providing fallbacks (e.g., address:
giveItem.address ?? await resolveAndCacheAddress(giveItem) ?? '' and
createdDate: giveItem.createdDate ?? DateTime.now()), or pre-resolve and cache
the address via resolveAndCacheAddress(giveItem) before building the widget so
you pass non-null values into the constructor; update the code paths that
construct the widget to use these null-safe expressions or pre-processing to
avoid any forced unwrapping.
In `@lib/widgets/common/app_pressable.dart`:
- Around line 155-181: GestureDetector is currently passing
widget.onTap/widget.onLongPress while InkWell also handles them when
widget.enableRipple is true, causing duplicate callbacks; update the
GestureDetector onTap/onLongPress to only forward taps when enableRipple is
false (e.g. use onTap: widget.enableRipple ? null : (widget.enabled ?
widget.onTap : null) and same for onLongPress) while keeping
_onTapDown/_onTapUp/_onTapCancel for the scale animation, and keep InkWell
handling taps when widget.enableRipple is true (ensure InkWell still uses
widget.enabled to gate its onTap/onLongPress, and preserve effectiveRadius,
highlightColor and splashColor).
In `@lib/widgets/common/completion_button.dart`:
- Around line 62-90: Remove the duplicate tap handler by letting AppPressable
handle taps and deleting the inner InkWell onTap; specifically, in the
CompletionButton widget remove the onTap passed to the inner InkWell (the calls
using enabledOnPressed/disabledOnPressed) so only AppPressable's onTap (and its
enableRipple behavior) invokes callbacks; ensure effectiveEnabled,
enabledOnPressed and disabledOnPressed remain wired to AppPressable and keep
InkWell for visual ripple only (no tap handler).
In `@lib/widgets/common/romrom_context_menu.dart`:
- Around line 87-90: The reverseCurve is hardcoded to Curves.easeInCubic which
violates the AppMotion token policy; update the CurvedAnimation instantiation
that uses _animationController and curve: AppMotion.decelerate to set
reverseCurve: AppMotion.accelerate (add AppMotion.accelerate constant if missing
in the AppMotion class) so both forward and reverse curves use the AppMotion
tokens consistently.
---
Outside diff comments:
In `@lib/screens/register_tab_screen.dart`:
- Around line 643-655: The code uses Navigator.push with MaterialPageRoute to
open ItemDetailDescriptionScreen (see Navigator.push and MaterialPageRoute
wrapping ItemDetailDescriptionScreen), which violates the guideline to use
context.navigateTo(); replace the Navigator.push/MaterialPageRoute call with a
call to context.navigateTo(...) that passes the ItemDetailDescriptionScreen (and
the same parameters: itemId: item.itemId, imageSize, currentImageIndex: 0,
heroTag, isMyItem: true, isRequestManagement: false) so navigation is performed
via the context.navigateTo API.
- Around line 664-675: The _navigateToEditItem function currently uses
MaterialPageRoute directly; replace that with the app's navigation helper by
calling context.navigateTo(...) and passing a builder that returns
ItemModificationScreen with the same parameters (itemId: item.itemId and onClose
callback), preserving the await of the Navigator result; ensure the onClose
still closes the screen appropriately (use the existing close callback) and that
the awaited result variable remains named result.
In `@lib/widgets/chat_input_bar.dart`:
- Around line 97-124: The suffixIcon's visual size (AppPressable -> Container
with margin: EdgeInsets.all(4.w) and width: 40.w) actually needs about 48.w, but
suffixIconConstraints currently caps at maxWidth/maxHeight: 40.w which can cause
clipping; update the BoxConstraints in suffixIconConstraints to at least
min/maxWidth and min/maxHeight: 48.w (or remove the Container margin and keep
40.w) so the Container inside AppPressable fits without overflow — adjust either
the Container width/margin or the BoxConstraints (in chat_input_bar.dart
referencing AppPressable and suffixIconConstraints) to match the true rendered
size.
In `@lib/widgets/common/custom_floating_button.dart`:
- Around line 57-79: The button registers the same tap callback twice because
AppPressable and the inner InkWell both set onTap; fix by removing the duplicate
tap handler: either remove the inner InkWell.onTap (in the code snippet around
the InkWell widget) and let AppPressable handle taps, or set AppPressable.onTap
to null and keep the InkWell handler—prefer removing the InkWell.onTap so
AppPressable remains the single source of truth; if you still need ripple,
enable AppPressable.enableRipple = true and pass rippleColor (use
highlightColor) so the visual effect is preserved while avoiding double
callbacks.
---
Nitpick comments:
In `@lib/screens/chat_tab_screen.dart`:
- Around line 331-387: The AppFadeSlideIn animation replays when SliverList
rebuilds because its State is destroyed/recreated; fix by preserving its state:
either make AppFadeSlideIn's State mixin with AutomaticKeepAliveClientMixin (add
"with AutomaticKeepAliveClientMixin" to its State class, override get
wantKeepAlive => true, and call super.build(context) so the AnimationController
isn't disposed) or give each AppFadeSlideIn a stable identity so it isn't
rebuilt (wrap the returned widget with a Key like Key(chatRoomDetail.chatRoomId)
or pass a PageStorageKey(chatRoomDetail.chatRoomId) into AppFadeSlideIn);
reference AppFadeSlideIn and chatRoomDetail.chatRoomId (and the surrounding
ChatRoomListItem) when making the change.
In `@lib/screens/notification_screen.dart`:
- Around line 411-416: The ListView.separated usage currently supplies a no-op
separatorBuilder returning SizedBox.shrink(); replace it with a plain
ListView.builder (or remove the separatorBuilder) to make intent clear: use
ListView.builder with the same shrinkWrap, physics: const
NeverScrollableScrollPhysics(), itemCount: notifications.length and the existing
itemBuilder logic (referencing ListView.separated, separatorBuilder,
itemBuilder, notifications.length, shrinkWrap, and NeverScrollableScrollPhysics
in the diff) so separators are not redundantly provided.
In `@lib/screens/register_tab_screen.dart`:
- Around line 495-500: Replace the manual tap-guard inside the AppPressable
onTap with the widget's enabled parameter: set enabled: !_isFabProcessing on the
AppPressable and remove the early-return check in the onTap handler; ensure you
still flip _isFabProcessing to true at the start of the async operation and back
to false when it completes or fails (inside the same async method invoked by
onTap) so the visual disabled state and tap prevention are kept in sync (symbols
to update: AppPressable, enabled, _isFabProcessing, onTap).
In `@lib/widgets/common_app_bar.dart`:
- Around line 65-73: The leading icon's tappable area is currently limited
because AppPressable wraps a SizedBox.square(dimension: 32.w) while the AppBar
uses leadingWidth: 72.w; expand the hit-test area by making the AppPressable
occupy the full leading area (e.g., wrap AppPressable with a SizedBox(width:
72.w, height: 56.w, child: Center(child: ...)) or change AppPressable to use
width: 72.w and height: 56.w) so that the touch target matches the visual
leadingWidth; update the widget containing leading (the leading:
AppPressable(...) block) to center the 32.w icon inside the larger tappable
container.
In `@lib/widgets/common/app_fade_slide_in.dart`:
- Around line 44-45: 현재 slideOffset 필드가 "px"로 문서화되어 있지만 SlideTransition에 전달되는
Offset은 자식 크기 대비 비율이므로 동작이 일치하지 않습니다; slideOffset을 실제 픽셀 단위로 보장하려면 build 내부에서
LayoutBuilder/BoxConstraints로 자식 높이(또는 너비, 방향에 따라)를 얻어 dyFraction = slideOffset
/ constraints.maxHeight(또는 적절한 축 크기)로 변환한 후 그 비율을 SlideTransition의 Offset으로
사용하도록 변경하세요 (참조: slideOffset 필드, SlideTransition 사용 부분, build 메서드에서
Tween/Animation 생성 지점). Ensure you handle zero-height defensively and update the
doc comment to state that slideOffset accepts pixels and is converted to a
fractional Offset using the child's size.
In `@lib/widgets/home_feed_item_widget.dart`:
- Around line 169-184: Replace the direct use of MaterialPageRoute in the onTap
handler inside home_feed_item_widget (the async onTap that awaits
Navigator.push) with the project's navigation helper by calling
context.navigateTo<dynamic>(...) and passing the ItemDetailDescriptionScreen
instance with the same arguments (itemId: widget.item.itemUuid ?? '', imageSize:
Size(screenWidth, screenWidth), currentImageIndex: index, heroTag:
'itemImage_${widget.item.itemUuid ?? widget.item.id}_$index', homeFeedItem:
widget.item, isMyItem: false, isRequestManagement: false, isTradeRequestAllowed:
true) so the call still awaits and captures the result variable as before.
In `@lib/widgets/notification_item_widget.dart`:
- Around line 118-121: The Container that creates a square icon currently uses
width: 24.w and height: 24.h; update the height to use width-based scaling so
both sides use .w (change height: 24.h to height: 24.w) to keep the element
square for iPad responsiveness—locate the Container inside the
NotificationItemWidget (where width: 24.w is set) and make the single-line
replacement.
In `@lib/widgets/onboarding_progress_header.dart`:
- Around line 155-190: The three circular Container widgets used for the
progress indicator use height: 24.h which makes them non-square on some
responsive breakpoints; update each Container (the one returning the checked
icon, the one for step == widget.currentStep, and the fallback pending step) to
use height: 24.w instead of 24.h so the width and height remain square across
devices (look for the Containers that set width: 24.w, height: 24.h and replace
the height unit to 24.w).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dd56260a-9b5c-48f1-9df2-8770034eefa8
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (42)
lib/enums/navigation_types.dartlib/models/app_motion.dartlib/screens/chat_room_screen.dartlib/screens/chat_tab_screen.dartlib/screens/home_tab_screen.dartlib/screens/main_screen.dartlib/screens/my_page_tab_screen.dartlib/screens/notification_screen.dartlib/screens/onboarding/onboarding_flow_screen.dartlib/screens/register_tab_screen.dartlib/screens/request_management_tab_screen.dartlib/screens/splash_screen.dartlib/utils/common_utils.dartlib/widgets/chat_input_bar.dartlib/widgets/chat_room_list_item.dartlib/widgets/common/app_fade_slide_in.dartlib/widgets/common/app_pressable.dartlib/widgets/common/app_skeleton.dartlib/widgets/common/common_modal.dartlib/widgets/common/common_snack_bar.dartlib/widgets/common/completed_toggle_switch.dartlib/widgets/common/completion_button.dartlib/widgets/common/custom_floating_button.dartlib/widgets/common/glass_header_delegate.dartlib/widgets/common/range_slider_widget.dartlib/widgets/common/romrom_context_menu.dartlib/widgets/common/scrollable_header.dartlib/widgets/common/toggle_selector.dartlib/widgets/common/triple_toggle_switch.dartlib/widgets/common_app_bar.dartlib/widgets/custom_bottom_navigation_bar.dartlib/widgets/home_feed_ai_sort_button.dartlib/widgets/home_feed_item_widget.dartlib/widgets/login_button.dartlib/widgets/notification_item_widget.dartlib/widgets/onboarding_progress_header.dartlib/widgets/request_list_item_card_widget.dartlib/widgets/request_management_item_card_widget.dartlib/widgets/sent_request_item_card.dartlib/widgets/skeletons/home_tab_skeleton.dartlib/widgets/skeletons/notification_skeleton.dartpubspec.yaml
| final refreshed = await Navigator.of( | ||
| context, | ||
| ).push<bool>(MaterialPageRoute(builder: (_) => ChatRoomScreen(chatRoomId: roomId))); |
There was a problem hiding this comment.
context.navigateTo() 대신 MaterialPageRoute를 직접 사용하고 있습니다.
코딩 가이드라인에 따르면 화면 전환 시 context.navigateTo()를 사용해야 합니다.
🔧 context.navigateTo() 사용으로 변경
- final refreshed = await Navigator.of(
- context,
- ).push<bool>(MaterialPageRoute(builder: (_) => ChatRoomScreen(chatRoomId: roomId)));
+ final refreshed = await context.navigateTo<bool>(
+ screen: ChatRoomScreen(chatRoomId: roomId),
+ );As per coding guidelines: "Use context.navigateTo() for screen navigation (MaterialPageRoute direct usage prohibited)"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final refreshed = await Navigator.of( | |
| context, | |
| ).push<bool>(MaterialPageRoute(builder: (_) => ChatRoomScreen(chatRoomId: roomId))); | |
| final refreshed = await context.navigateTo<bool>( | |
| screen: ChatRoomScreen(chatRoomId: roomId), | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/screens/chat_tab_screen.dart` around lines 374 - 376, The code uses
Navigator.of(context).push<bool>(MaterialPageRoute(builder: (_) =>
ChatRoomScreen(chatRoomId: roomId))) directly; change this to use the project
navigation helper by calling context.navigateTo(...) with the ChatRoomScreen
route instead, passing the same ChatRoomScreen(chatRoomId: roomId) as the target
and preserving the returned bool (refreshed) semantics; update the call site
where Navigator.of(...).push<bool>(MaterialPageRoute(...)) appears to use
context.navigateTo(ChatRoomScreen(chatRoomId: roomId)) or the equivalent
context.navigateTo(...) overload used in the app.
| body: AnimatedSwitcher( | ||
| duration: AppMotion.normal, | ||
| switchInCurve: AppMotion.entry, | ||
| switchOutCurve: AppMotion.entry, | ||
| transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), | ||
| child: KeyedSubtree(key: ValueKey<int>(_currentTabIndex), child: _navigationTabScreens[_currentTabIndex]), | ||
| ), |
There was a problem hiding this comment.
AnimatedSwitcher로 인해 탭 전환 시마다 위젯이 재생성되어 API가 중복 호출됩니다.
AnimatedSwitcher는 자식 위젯을 완전히 파괴하고 재생성합니다. NotificationScreen, RegisterTabScreen, RequestManagementTabScreen 등의 initState에서 API 호출(_loadNotifications, _loadMyItems, _loadInitialItems)이 이루어지므로, 탭을 전환할 때마다 불필요한 네트워크 요청이 발생하고 스크롤 위치 등의 상태가 초기화됩니다.
기존의 IndexedStack 방식을 유지하면서 페이드 애니메이션만 적용하거나, AutomaticKeepAliveClientMixin을 활용하는 것을 권장합니다.
🔧 IndexedStack + AnimatedOpacity 조합 제안
- body: AnimatedSwitcher(
- duration: AppMotion.normal,
- switchInCurve: AppMotion.entry,
- switchOutCurve: AppMotion.entry,
- transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),
- child: KeyedSubtree(key: ValueKey<int>(_currentTabIndex), child: _navigationTabScreens[_currentTabIndex]),
- ),
+ body: IndexedStack(
+ index: _currentTabIndex,
+ children: _navigationTabScreens,
+ ),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| body: AnimatedSwitcher( | |
| duration: AppMotion.normal, | |
| switchInCurve: AppMotion.entry, | |
| switchOutCurve: AppMotion.entry, | |
| transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), | |
| child: KeyedSubtree(key: ValueKey<int>(_currentTabIndex), child: _navigationTabScreens[_currentTabIndex]), | |
| ), | |
| body: IndexedStack( | |
| index: _currentTabIndex, | |
| children: _navigationTabScreens, | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/screens/main_screen.dart` around lines 59 - 65, AnimatedSwitcher +
KeyedSubtree (ValueKey<int>(_currentTabIndex)) rebuilds each tab from
_navigationTabScreens and causes duplicate API calls in screens like
NotificationScreen, RegisterTabScreen, RequestManagementTabScreen (initState
calls _loadNotifications/_loadMyItems/_loadInitialItems). Replace
AnimatedSwitcher with an IndexedStack to preserve each child instance and apply
fade with an AnimatedOpacity (or FadeTransition) on the active child to keep the
visual animation, or alternatively implement AutomaticKeepAliveClientMixin in
the mentioned screens so their state and API calls are preserved; update
main_screen.dart to use IndexedStack(_navigationTabScreens) and control opacity
for the selected index or add keep-alive mixin to
NotificationScreen/RegisterTabScreen/RequestManagementTabScreen to prevent
reinitialization.
| address: giveItem.address!, | ||
| createdDate: giveItem.createdDate!, |
There was a problem hiding this comment.
address와 createdDate의 강제 언래핑이 런타임 크래시를 유발할 수 있습니다.
giveItem.address!와 giveItem.createdDate!는 값이 null일 경우 앱이 크래시됩니다. resolveAndCacheAddress()가 실패하거나 값이 없는 경우를 대비한 fallback 처리가 필요합니다.
🛡️ 제안된 수정
- address: giveItem.address!,
- createdDate: giveItem.createdDate!,
+ address: giveItem.address ?? '주소 미등록',
+ createdDate: giveItem.createdDate ?? DateTime.now(),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| address: giveItem.address!, | |
| createdDate: giveItem.createdDate!, | |
| address: giveItem.address ?? '주소 미등록', | |
| createdDate: giveItem.createdDate ?? DateTime.now(), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/screens/request_management_tab_screen.dart` around lines 763 - 764, The
code force-unwraps giveItem.address and giveItem.createdDate (address:
giveItem.address!, createdDate: giveItem.createdDate!), which can crash at
runtime; change these to safe null-aware values by checking for null and
providing fallbacks (e.g., address: giveItem.address ?? await
resolveAndCacheAddress(giveItem) ?? '' and createdDate: giveItem.createdDate ??
DateTime.now()), or pre-resolve and cache the address via
resolveAndCacheAddress(giveItem) before building the widget so you pass non-null
values into the constructor; update the code paths that construct the widget to
use these null-safe expressions or pre-processing to avoid any forced
unwrapping.
| return GestureDetector( | ||
| onTapDown: _onTapDown, | ||
| onTapUp: _onTapUp, | ||
| onTapCancel: _onTapCancel, | ||
| onTap: widget.enabled ? widget.onTap : null, | ||
| onLongPress: widget.enabled ? widget.onLongPress : null, | ||
| behavior: HitTestBehavior.opaque, | ||
| child: Transform.scale( | ||
| scale: _currentScale, | ||
| child: widget.enableRipple | ||
| ? Material( | ||
| color: Colors.transparent, | ||
| borderRadius: effectiveRadius, | ||
| child: InkWell( | ||
| onTap: widget.enabled ? widget.onTap : null, | ||
| onLongPress: widget.enabled ? widget.onLongPress : null, | ||
| borderRadius: effectiveRadius, | ||
| highlightColor: widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface), | ||
| splashColor: (widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface)).withValues( | ||
| alpha: 0.3, | ||
| ), | ||
| child: widget.child, | ||
| ), | ||
| ) | ||
| : widget.child, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
onTap 콜백이 두 번 호출되는 버그가 있습니다.
enableRipple이 true일 때, GestureDetector와 InkWell 모두 onTap/onLongPress를 처리하고 있어 콜백이 두 번 실행됩니다. GestureDetector는 스케일 애니메이션만 담당하고, 실제 탭 콜백은 InkWell에서만 처리해야 합니다.
🐛 수정 제안
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
- onTap: widget.enabled ? widget.onTap : null,
- onLongPress: widget.enabled ? widget.onLongPress : null,
+ onTap: widget.enableRipple ? null : (widget.enabled ? widget.onTap : null),
+ onLongPress: widget.enableRipple ? null : (widget.enabled ? widget.onLongPress : null),
behavior: HitTestBehavior.opaque,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return GestureDetector( | |
| onTapDown: _onTapDown, | |
| onTapUp: _onTapUp, | |
| onTapCancel: _onTapCancel, | |
| onTap: widget.enabled ? widget.onTap : null, | |
| onLongPress: widget.enabled ? widget.onLongPress : null, | |
| behavior: HitTestBehavior.opaque, | |
| child: Transform.scale( | |
| scale: _currentScale, | |
| child: widget.enableRipple | |
| ? Material( | |
| color: Colors.transparent, | |
| borderRadius: effectiveRadius, | |
| child: InkWell( | |
| onTap: widget.enabled ? widget.onTap : null, | |
| onLongPress: widget.enabled ? widget.onLongPress : null, | |
| borderRadius: effectiveRadius, | |
| highlightColor: widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface), | |
| splashColor: (widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface)).withValues( | |
| alpha: 0.3, | |
| ), | |
| child: widget.child, | |
| ), | |
| ) | |
| : widget.child, | |
| ), | |
| ); | |
| return GestureDetector( | |
| onTapDown: _onTapDown, | |
| onTapUp: _onTapUp, | |
| onTapCancel: _onTapCancel, | |
| onTap: widget.enableRipple ? null : (widget.enabled ? widget.onTap : null), | |
| onLongPress: widget.enableRipple ? null : (widget.enabled ? widget.onLongPress : null), | |
| behavior: HitTestBehavior.opaque, | |
| child: Transform.scale( | |
| scale: _currentScale, | |
| child: widget.enableRipple | |
| ? Material( | |
| color: Colors.transparent, | |
| borderRadius: effectiveRadius, | |
| child: InkWell( | |
| onTap: widget.enabled ? widget.onTap : null, | |
| onLongPress: widget.enabled ? widget.onLongPress : null, | |
| borderRadius: effectiveRadius, | |
| highlightColor: widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface), | |
| splashColor: (widget.rippleColor ?? darkenBlend(Theme.of(context).colorScheme.surface)).withValues( | |
| alpha: 0.3, | |
| ), | |
| child: widget.child, | |
| ), | |
| ) | |
| : widget.child, | |
| ), | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/widgets/common/app_pressable.dart` around lines 155 - 181,
GestureDetector is currently passing widget.onTap/widget.onLongPress while
InkWell also handles them when widget.enableRipple is true, causing duplicate
callbacks; update the GestureDetector onTap/onLongPress to only forward taps
when enableRipple is false (e.g. use onTap: widget.enableRipple ? null :
(widget.enabled ? widget.onTap : null) and same for onLongPress) while keeping
_onTapDown/_onTapUp/_onTapCancel for the scale animation, and keep InkWell
handling taps when widget.enableRipple is true (ensure InkWell still uses
widget.enabled to gate its onTap/onLongPress, and preserve effectiveRadius,
highlightColor and splashColor).
| child: AppPressable( | ||
| onTap: effectiveEnabled ? enabledOnPressed : disabledOnPressed, | ||
| enabled: effectiveEnabled, | ||
| borderRadius: BorderRadius.circular(10.r), | ||
| child: SizedBox( | ||
| width: buttonWidth.w, | ||
| height: buttonHeight.h, | ||
| child: Material( | ||
| color: backgroundColor, | ||
| borderRadius: BorderRadius.circular(10.r), | ||
| child: Center( | ||
| child: isLoading | ||
| ? SizedBox( | ||
| width: 24.w, | ||
| height: 24.h, | ||
| child: CircularProgressIndicator( | ||
| strokeWidth: 2.w, | ||
| valueColor: const AlwaysStoppedAnimation<Color>(AppColors.textColorWhite), | ||
| ), | ||
| ) | ||
| : Text(buttonText, style: buttonTextStyle, textAlign: TextAlign.center, softWrap: false), | ||
| child: InkWell( | ||
| onTap: effectiveEnabled ? enabledOnPressed : disabledOnPressed, | ||
| customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.r)), | ||
| highlightColor: highlightColor, | ||
| splashColor: splashColor, | ||
| borderRadius: BorderRadius.circular(10.r), | ||
| child: Center( | ||
| child: isLoading | ||
| ? SizedBox( | ||
| width: 24.w, | ||
| height: 24.h, | ||
| child: CircularProgressIndicator( | ||
| strokeWidth: 2.w, | ||
| valueColor: const AlwaysStoppedAnimation<Color>(AppColors.textColorWhite), | ||
| ), | ||
| ) | ||
| : Text(buttonText, style: buttonTextStyle, textAlign: TextAlign.center, softWrap: false), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
중복 탭 핸들러로 인해 콜백이 두 번 실행됩니다.
AppPressable은 내부적으로 GestureDetector의 onTap을 사용하고, enableRipple이 기본값(true)일 때 자체적으로 InkWell도 렌더링합니다. 현재 코드에서 내부의 InkWell(Line 72-77)에도 onTap 핸들러가 있어서, 버튼을 탭하면 콜백이 두 번 호출될 수 있습니다.
AppPressable의 enableRipple을 활용하고 내부 InkWell의 onTap을 제거하거나, AppPressable 없이 기존 InkWell만 사용하는 것을 권장합니다.
🐛 AppPressable 활용 시 내부 InkWell onTap 제거
return Center(
child: AppPressable(
onTap: effectiveEnabled ? enabledOnPressed : disabledOnPressed,
enabled: effectiveEnabled,
borderRadius: BorderRadius.circular(10.r),
+ enableRipple: true,
+ rippleColor: highlightColor,
child: SizedBox(
width: buttonWidth.w,
height: buttonHeight.h,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(10.r),
- child: InkWell(
- onTap: effectiveEnabled ? enabledOnPressed : disabledOnPressed,
- customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.r)),
- highlightColor: highlightColor,
- splashColor: splashColor,
- borderRadius: BorderRadius.circular(10.r),
- child: Center(
+ child: Center(
child: isLoading
? SizedBox(
width: 24.w,
height: 24.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: const AlwaysStoppedAnimation<Color>(AppColors.textColorWhite),
),
)
: Text(buttonText, style: buttonTextStyle, textAlign: TextAlign.center, softWrap: false),
- ),
),
),
),
),
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/widgets/common/completion_button.dart` around lines 62 - 90, Remove the
duplicate tap handler by letting AppPressable handle taps and deleting the inner
InkWell onTap; specifically, in the CompletionButton widget remove the onTap
passed to the inner InkWell (the calls using enabledOnPressed/disabledOnPressed)
so only AppPressable's onTap (and its enableRipple behavior) invokes callbacks;
ensure effectiveEnabled, enabledOnPressed and disabledOnPressed remain wired to
AppPressable and keep InkWell for visual ripple only (no tap handler).
| final curve = CurvedAnimation( | ||
| parent: _animationController, | ||
| curve: Curves.easeOutCubic, | ||
| curve: AppMotion.decelerate, | ||
| reverseCurve: Curves.easeInCubic, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 목적: 모션 토큰화 누락(하드코딩 Curves) 점검
# 기대 결과: context menu 내 Curves 하드코딩 0건(또는 허용 목록만 존재)
rg -n "reverseCurve:\s*Curves\." lib/widgets/common/romrom_context_menu.dart
rg -n "Curves\." lib/models/app_motion.dartRepository: TEAM-ROMROM/RomRom-FE
Length of output: 329
🏁 Script executed:
#!/bin/bash
# Check imports and full context around lines 87-90 in romrom_context_menu.dart
head -50 lib/widgets/common/romrom_context_menu.dart | cat -n
echo "---"
sed -n '80,95p' lib/widgets/common/romrom_context_menu.dart | cat -nRepository: TEAM-ROMROM/RomRom-FE
Length of output: 2524
🏁 Script executed:
#!/bin/bash
# Check complete AppMotion definition to understand the motion token pattern
cat -n lib/models/app_motion.dartRepository: TEAM-ROMROM/RomRom-FE
Length of output: 1971
reverseCurve도 모션 토큰으로 통일하세요.
Line 90의 Curves.easeInCubic 하드코딩이 AppMotion 정책(반드시 이 클래스의 상수를 사용할 것)을 위반합니다.
AppMotion.decelerate의 역방향 애니메이션으로 AppMotion.accelerate를 추가하고 사용하세요:
수정 필요 사항
// lib/widgets/common/romrom_context_menu.dart
final curve = CurvedAnimation(
parent: _animationController,
curve: AppMotion.decelerate,
- reverseCurve: Curves.easeInCubic,
+ reverseCurve: AppMotion.accelerate,
);// lib/models/app_motion.dart (Curve 토큰 섹션에 추가)
/// 퇴장/역방향 전환 — 빠른 끝맺음 (감속의 역)
static const Curve accelerate = Curves.easeInCubic;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final curve = CurvedAnimation( | |
| parent: _animationController, | |
| curve: Curves.easeOutCubic, | |
| curve: AppMotion.decelerate, | |
| reverseCurve: Curves.easeInCubic, | |
| final curve = CurvedAnimation( | |
| parent: _animationController, | |
| curve: AppMotion.decelerate, | |
| reverseCurve: AppMotion.accelerate, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/widgets/common/romrom_context_menu.dart` around lines 87 - 90, The
reverseCurve is hardcoded to Curves.easeInCubic which violates the AppMotion
token policy; update the CurvedAnimation instantiation that uses
_animationController and curve: AppMotion.decelerate to set reverseCurve:
AppMotion.accelerate (add AppMotion.accelerate constant if missing in the
AppMotion class) so both forward and reverse curves use the AppMotion tokens
consistently.
|
확인했습니다 curve랑 duration을 공통 모델로 빼서 정의하셧군요 |
# Conflicts: # lib/screens/splash_screen.dart
|
✅ 프로젝트 빌드 성공 APK 빌드가 완료되었습니다. |
Summary by CodeRabbit
Release Notes
New Features
UI/UX Improvements