Repository: Survival-Cartel/photopin
Pull request #221: #206 feat photo screen bug fix
Author: login: roychoi1012
Labels: name: fix
description: 버그 수정
Created: 2025-05-17T05:38:41Z | Merged: 2025-05-17T07:26:11Z

## 🔍 개요 (Summary)

- GridView에서 아래 사진과 높이 차이가 너무 많이 나는 문제
- Stream으로 데이터를 받아오지 않아서 사진을 파이어베이스 스토리지에 저장 시 반영되지 않는 문제
- GridView 내의 컴포넌트의 Title 속성 길이가 너무 길면 컴포넌트가 깨지는 문제
- BottomBar가 BottomSheet를 덮어서 버튼이 모두 보이지 않는 문제

---

## ✅ 변경 사항 (Changes)

- onCancel -> onDelete 명칭 수정
- editBottomSheet를 photo, journal로 분리

---

## 📸 스크린샷 (Screenshots, 선택사항)

> 

https://github.com/user-attachments/assets/64c0edc3-4319-4d8b-8886-5ee868c5be96





---

## 🔗 관련 이슈 (Related Issues)

- Closes #206 

---

## 🧪 테스트 (Test)

- 천천히...

## 📝 참고 사항 (Notes)

- 사진 날짜에 맞는 저널만 드랍다운에 표시되게 수정해야함
- 그냥 apply 버튼을 누를 시 드랍다운이 null값으로 변환되는 것 수정

---

## 👀 리뷰어 체크리스트 (For Reviewer)

- [ ] 기능이 명세에 맞게 동작하는지 확인
- [ ] 불필요한 코드/주석이 없는지 확인
- [ ] 테스트가 적절히 작성되었는지 확인


Linked issue #206: Feat: photo screen bug fix
**기능 요청이 어떤 문제와 관련이 있는지 설명하거나 연관된 이슈를 남겨주세요.**
문제가 무엇인지 명확하고 간결하게 설명해 주세요.

**원하시는 해결책을 설명해주세요**
원하는 바에 대한 명확하고 간결하게 설명해주세요.

**고려한 대안 설명**
고려한 대안 솔루션이나 기능에 대한 명확하고 간결하게 설명해주세요.

**추가 상황**
기능에 대한 다른 상황이나 스크린샷을 여기에 추가하세요.


Diff:
diff --git a/lib/core/di/di_setup.dart b/lib/core/di/di_setup.dart
index 6a0e3e1f..0895a663 100644
--- a/lib/core/di/di_setup.dart
+++ b/lib/core/di/di_setup.dart
@@ -26,6 +26,7 @@ import 'package:photopin/core/usecase/get_compare_model_use_case.dart';
 import 'package:photopin/core/usecase/get_current_location_use_case.dart';
 import 'package:photopin/core/usecase/get_current_user_use_case.dart';
 import 'package:photopin/core/usecase/get_journal_list_use_case.dart';
+import 'package:photopin/core/usecase/get_photo_journal_list_use_case.dart';
 import 'package:photopin/core/usecase/get_photo_list_use_case.dart';
 import 'package:photopin/core/usecase/get_photo_list_with_journal_id_use_case.dart';
 import 'package:photopin/core/usecase/get_place_name_use_case.dart';
@@ -287,6 +288,16 @@ void di() {
         param1: userId,
       ),
       photoRepository: getIt<PhotoRepository>(param1: userId),
+      getPhotoJournalListUseCase: getIt<GetPhotoJournalListUseCase>(
+        param1: userId,
+      ),
+    ),
+  );
+
+  getIt.registerFactoryParam<GetPhotoJournalListUseCase, String, void>(
+    (userId, _) => GetPhotoJournalListUseCase(
+      journalRepository: getIt<JournalRepository>(param1: userId),
+      photoRepository: getIt<PhotoRepository>(param1: userId),
     ),
   );
 
diff --git a/lib/core/domain/photo_journal_list_model.dart b/lib/core/domain/photo_journal_list_model.dart
new file mode 100644
index 00000000..8ab16d33
--- /dev/null
+++ b/lib/core/domain/photo_journal_list_model.dart
@@ -0,0 +1,9 @@
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/photo/domain/model/photo_model.dart';
+
+class PhotoJournalListModel {
+  final List<JournalModel> journals;
+  final List<PhotoModel> photos;
+
+  const PhotoJournalListModel({required this.journals, required this.photos});
+}
diff --git a/lib/core/usecase/get_photo_journal_list_use_case.dart b/lib/core/usecase/get_photo_journal_list_use_case.dart
new file mode 100644
index 00000000..0ba3cd55
--- /dev/null
+++ b/lib/core/usecase/get_photo_journal_list_use_case.dart
@@ -0,0 +1,29 @@
+import 'package:photopin/core/domain/photo_journal_list_model.dart';
+import 'package:photopin/journal/data/repository/journal_repository.dart';
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/photo/data/repository/photo_repository.dart';
+import 'package:photopin/photo/domain/model/photo_model.dart';
+import 'package:rxdart/rxdart.dart';
+
+class GetPhotoJournalListUseCase {
+  final JournalRepository _journalRepository;
+  final PhotoRepository _photoRepository;
+
+  const GetPhotoJournalListUseCase({
+    required JournalRepository journalRepository,
+    required PhotoRepository photoRepository,
+  }) : _journalRepository = journalRepository,
+       _photoRepository = photoRepository;
+
+  Stream<PhotoJournalListModel> execute() {
+    final journalsStream = _journalRepository.watchJournals();
+    final photosStream = _photoRepository.watchPhotos();
+
+    return Rx.combineLatest2(journalsStream, photosStream, (
+      List<JournalModel> journals,
+      List<PhotoModel> photos,
+    ) {
+      return PhotoJournalListModel(journals: journals, photos: photos);
+    });
+  }
+}
diff --git a/lib/photo/data/data_source/photo_data_source.dart b/lib/photo/data/data_source/photo_data_source.dart
index dea95242..6d506ad1 100644
--- a/lib/photo/data/data_source/photo_data_source.dart
+++ b/lib/photo/data/data_source/photo_data_source.dart
@@ -7,4 +7,5 @@ abstract interface class PhotoDataSource {
   Future<void> savePhoto(PhotoDto dto);
   Future<void> deletePhoto(String photoId);
   Future<void> updatePhoto(PhotoDto photoDto);
+  Stream<List<PhotoDto>> watchPhotos();
 }
diff --git a/lib/photo/data/data_source/photo_data_source_impl.dart b/lib/photo/data/data_source/photo_data_source_impl.dart
index fd022fe2..745e3a9d 100644
--- a/lib/photo/data/data_source/photo_data_source_impl.dart
+++ b/lib/photo/data/data_source/photo_data_source_impl.dart
@@ -82,4 +82,15 @@ class PhotoDataSourceImpl implements PhotoDataSource {
   Future<void> updatePhoto(PhotoDto photoDto) async {
     await _photoStore.doc(photoDto.id).set(photoDto, SetOptions(merge: true));
   }
+
+  @override
+  Stream<List<PhotoDto>> watchPhotos() {
+    try {
+      return _photoStore.snapshots().map(
+        (snapshot) => snapshot.docs.map((doc) => doc.data()).toList(),
+      );
+    } catch (e) {
+      throw FirestoreError.notFoundError;
+    }
+  }
 }
diff --git a/lib/photo/data/repository/photo_repository.dart b/lib/photo/data/repository/photo_repository.dart
index 3fcfd959..3b65d4af 100644
--- a/lib/photo/data/repository/photo_repository.dart
+++ b/lib/photo/data/repository/photo_repository.dart
@@ -13,4 +13,5 @@ abstract interface class PhotoRepository
   Future<void> savePhoto(PhotoModel model);
   Future<void> deletePhoto(String photoId);
   Future<void> updatePhoto(PhotoModel photoModel);
+  Stream<List<PhotoModel>> watchPhotos();
 }
diff --git a/lib/photo/data/repository/photo_repository_impl.dart b/lib/photo/data/repository/photo_repository_impl.dart
index a729145a..1a04775e 100644
--- a/lib/photo/data/repository/photo_repository_impl.dart
+++ b/lib/photo/data/repository/photo_repository_impl.dart
@@ -60,4 +60,11 @@ class PhotoRepositoryImpl implements PhotoRepository {
   Future<void> updatePhoto(PhotoModel photoModel) async {
     await _photoDataSource.updatePhoto(photoModel.toDto());
   }
+
+  @override
+  Stream<List<PhotoModel>> watchPhotos() {
+    return _photoDataSource.watchPhotos().map(
+      (dtoList) => dtoList.map((dto) => dto.toModel()).toList(),
+    );
+  }
 }
diff --git a/lib/presentation/component/edit_bottom_sheet.dart b/lib/presentation/component/edit_bottom_sheet.dart
index 3544cb9a..89cb0aef 100644
--- a/lib/presentation/component/edit_bottom_sheet.dart
+++ b/lib/presentation/component/edit_bottom_sheet.dart
@@ -7,7 +7,7 @@ abstract class EditBottomSheet extends StatefulWidget {
 
   final VoidCallback onTapClose;
   // final Function(String photoName, String journalId, String comment) onTapApply;
-  final VoidCallback onTapCancel;
+  final VoidCallback onTapDelete;
 
   const EditBottomSheet({
     super.key,
@@ -15,7 +15,7 @@ abstract class EditBottomSheet extends StatefulWidget {
     required this.title,
     required this.comment,
     required this.onTapClose,
-    required this.onTapCancel,
+    required this.onTapDelete,
   });
 
   @override
@@ -26,171 +26,5 @@ class _EditBottomSheetState extends State<EditBottomSheet> {
   @override
   Widget build(BuildContext context) {
     return const Placeholder();
-    //   return BottomSheet(
-    //     shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
-    //     backgroundColor: AppColors.white,
-    //     builder: (context) {
-    //       return SingleChildScrollView(
-    //         padding: EdgeInsets.only(
-    //           left: 16,
-    //           top: 16,
-    //           right: 16,
-    //           bottom: 16 + MediaQuery.of(context).viewPadding.bottom,
-    //         ),
-    //         child: Column(
-    //           spacing: 16,
-    //           children: [
-    //             Row(
-    //               spacing: 12,
-    //               crossAxisAlignment: CrossAxisAlignment.center,
-    //               children: [
-    //                 Expanded(
-    //                   child: TextLimitInputField(
-    //                     controller: titleController,
-    //                     hintText: 'Write Title',
-    //                     maxLength: 30,
-    //                   ),
-    //                 ),
-    //                 GestureDetector(
-    //                   onTap: () {
-    //                     widget.onTapClose();
-    //                   },
-    //                   child: const Icon(
-    //                     Icons.close,
-    //                     size: 20,
-    //                     color: AppColors.gray2,
-    //                   ),
-    //                 ),
-    //               ],
-    //             ),
-    //             AspectRatio(
-    //               // TODO: 현재는 고정으로 16:9 비율, 추후 카메라 기능 들어오면 수정
-    //               aspectRatio: 16 / 9,
-    //               child: Container(
-    //                 decoration: BoxDecoration(
-    //                   borderRadius: BorderRadius.circular(10),
-    //                   image: DecorationImage(
-    //                     image: NetworkImage(widget.thumbnailUrl),
-    //                     fit: BoxFit.cover,
-    //                   ),
-    //                 ),
-    //               ),
-    //             ),
-    //             Column(
-    //               spacing: 12,
-    //               children: [
-    //                 Row(
-    //                   spacing: 12,
-    //                   children: [
-    //                     const BaseIcon(
-    //                       iconColor: AppColors.primary80,
-    //                       size: 16,
-    //                       iconData: Icons.calendar_month,
-    //                     ),
-    //                     Text(
-    //                       _formattedDateTime(),
-    //                       style: AppFonts.smallTextRegular,
-    //                     ),
-    //                   ],
-    //                 ),
-    //                 Row(
-    //                   spacing: 12,
-    //                   children: [
-    //                     const BaseIcon(
-    //                       iconColor: AppColors.secondary100,
-    //                       size: 16,
-    //                       iconData: Icons.comment,
-    //                     ),
-    //                     Expanded(
-    //                       child: TextLimitInputField(
-    //                         controller: commentController,
-    //                         hintText: 'Write Comment',
-    //                         maxLength: 30,
-    //                       ),
-    //                     ),
-    //                   ],
-    //                 ),
-    //                 Row(
-    //                   children: [
-    //                     const BaseIcon(
-    //                       iconColor: AppColors.primary80,
-    //                       size: 16,
-    //                       iconData: Icons.note,
-    //                     ),
-    //                     const SizedBox(width: 12),
-    //                     Expanded(
-    //                       child: DropdownMenu<String>(
-    //                         controller: journalController,
-    //                         inputDecorationTheme: const InputDecorationTheme(
-    //                           contentPadding: EdgeInsets.zero,
-    //                           border: InputBorder.none,
-    //                         ),
-    //                         initialSelection:
-    //                             widget.journals.isNotEmpty
-    //                                 ? widget.journalId
-    //                                 : null,
-    //                         menuStyle: MenuStyle(
-    //                           backgroundColor: WidgetStateProperty.all(
-    //                             AppColors.white,
-    //                           ),
-    //                           padding: WidgetStateProperty.all(EdgeInsets.zero),
-    //                           elevation: WidgetStateProperty.all(2),
-    //                         ),
-    //                         onSelected: (value) {
-    //                           _journalId = value!;
-    //                         },
-    //                         textStyle: AppFonts.smallTextRegular,
-    //                         dropdownMenuEntries:
-    //                             widget.journals.isEmpty
-    //                                 ? []
-    //                                 : List.generate(widget.journals.length, (
-    //                                   int index,
-    //                                 ) {
-    //                                   return DropdownMenuEntry(
-    //                                     label: widget.journals[index].name,
-    //                                     value: widget.journals[index].id,
-    //                                   );
-    //                                 }),
-    //                       ),
-    //                     ),
-    //                   ],
-    //                 ),
-    //               ],
-    //             ),
-    //             Row(
-    //               spacing: 12,
-    //               children: [
-    //                 Expanded(
-    //                   child: BaseIconButton(
-    //                     buttonType: ButtonType.small,
-    //                     buttonColor: AppColors.primary100,
-    //                     iconName: Icons.edit,
-    //                     buttonName: 'Apply',
-    //                     onClick: () {
-    //                       widget.onTapApply(
-    //                         titleController.text,
-    //                         _journalId,
-    //                         commentController.text,
-    //                       );
-    //                     },
-    //                   ),
-    //                 ),
-    //                 Expanded(
-    //                   child: BaseIconButton(
-    //                     buttonType: ButtonType.small,
-    //                     buttonColor: AppColors.warning,
-    //                     iconName: Icons.cancel,
-    //                     buttonName: 'Cancel',
-    //                     onClick: widget.onTapCancel,
-    //                   ),
-    //                 ),
-    //               ],
-    //             ),
-    //           ],
-    //         ),
-    //       );
-    //     },
-    //     onClosing: widget.onClosing ?? () {},
-    //   );
   }
 }
diff --git a/lib/presentation/component/journal_card_image.dart b/lib/presentation/component/journal_card_image.dart
index a68456b0..fb0f8195 100644
--- a/lib/presentation/component/journal_card_image.dart
+++ b/lib/presentation/component/journal_card_image.dart
@@ -57,11 +57,13 @@ class JournalCardImage extends StatelessWidget {
             children: [
               Text(
                 journeyTitle,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
                 style: AppFonts.normalTextBold.copyWith(color: AppColors.white),
               ),
               Text(
                 description,
-                maxLines: 2,
+                maxLines: 1,
                 overflow: TextOverflow.ellipsis,
                 style: AppFonts.smallerTextRegular.copyWith(
                   color: AppColors.white,
diff --git a/lib/presentation/component/journal_edit_bottom_sheet.dart b/lib/presentation/component/journal_edit_bottom_sheet.dart
index 978ea274..52068bf2 100644
--- a/lib/presentation/component/journal_edit_bottom_sheet.dart
+++ b/lib/presentation/component/journal_edit_bottom_sheet.dart
@@ -18,7 +18,7 @@ class JournalEditBottomSheet extends EditBottomSheet {
     super.thumbnailUrl,
     required super.title,
     required super.comment,
-    required super.onTapCancel,
+    required super.onTapDelete,
     required super.onTapClose,
     required this.journal,
     required this.onTapApply,
@@ -245,7 +245,7 @@ class _JournalEditBottomSheetState extends State<JournalEditBottomSheet> {
                       buttonColor: AppColors.warning,
                       iconName: Icons.cancel,
                       buttonName: 'Cancel',
-                      onClick: widget.onTapCancel,
+                      onClick: widget.onTapDelete,
                     ),
                   ),
                 ],
diff --git a/lib/presentation/component/photo_edit_bottom_sheet.dart b/lib/presentation/component/photo_edit_bottom_sheet.dart
new file mode 100644
index 00000000..8adcdb3b
--- /dev/null
+++ b/lib/presentation/component/photo_edit_bottom_sheet.dart
@@ -0,0 +1,233 @@
+import 'package:flutter/material.dart';
+import 'package:photopin/core/enums/button_type.dart';
+import 'package:photopin/core/extensions/datetime_extension.dart';
+import 'package:photopin/core/styles/app_color.dart';
+import 'package:photopin/core/styles/app_font.dart';
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/presentation/component/base_icon.dart';
+import 'package:photopin/presentation/component/base_icon_button.dart';
+import 'package:photopin/presentation/component/text_limit_input_field.dart';
+import 'package:photopin/presentation/component/edit_bottom_sheet.dart';
+
+class PhotoEditBottomSheet extends EditBottomSheet {
+  final List<JournalModel> journals;
+  final DateTime dateTime;
+  final String journalId;
+  final Function(String photoName, String journalId, String comment) onTapApply;
+
+  const PhotoEditBottomSheet({
+    super.key,
+    super.thumbnailUrl,
+    required super.title,
+    required super.comment,
+    required super.onTapDelete,
+    required super.onTapClose,
+    required this.journals,
+    required this.dateTime,
+    required this.onTapApply,
+    this.journalId = '',
+  });
+
+  @override
+  State<PhotoEditBottomSheet> createState() => _PhotoEditBottomSheetState();
+}
+
+class _PhotoEditBottomSheetState extends State<PhotoEditBottomSheet> {
+  final TextEditingController titleController = TextEditingController();
+  final TextEditingController commentController = TextEditingController();
+  final TextEditingController journalController = TextEditingController();
+  String _journalId = '';
+
+  String _formattedDateTime() => widget.dateTime.formatDateTimeString();
+
+  @override
+  void initState() {
+    super.initState();
+    titleController.value = TextEditingValue(text: widget.title);
+    commentController.value = TextEditingValue(text: widget.comment);
+    _journalId = widget.journalId;
+  }
+
+  @override
+  void dispose() {
+    titleController.dispose();
+    commentController.dispose();
+    journalController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BottomSheet(
+      shape: const RoundedRectangleBorder(
+        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+      ),
+      backgroundColor: AppColors.white,
+      builder: (context) {
+        return SingleChildScrollView(
+          padding: EdgeInsets.only(
+            left: 16,
+            top: 16,
+            right: 16,
+            bottom: 16 + MediaQuery.of(context).viewPadding.bottom,
+          ),
+          child: Column(
+            spacing: 12,
+            children: [
+              Row(
+                spacing: 12,
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  Expanded(
+                    child: TextLimitInputField(
+                      controller: titleController,
+                      hintText: 'Write Title',
+                      maxLength: 30,
+                    ),
+                  ),
+                  GestureDetector(
+                    onTap: () {
+                      widget.onTapClose();
+                    },
+                    child: const Icon(
+                      Icons.close,
+                      size: 20,
+                      color: AppColors.gray2,
+                    ),
+                  ),
+                ],
+              ),
+              AspectRatio(
+                // TODO: 현재는 고정으로 16:9 비율, 추후 카메라 기능 들어오면 수정
+                aspectRatio: 16 / 9,
+                child: Container(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(10),
+                    image: DecorationImage(
+                      image: NetworkImage(
+                        widget.thumbnailUrl ??
+                            'https://web.cau.ac.kr/_images/_board/skin/album/1//no_image.gif',
+                      ),
+                      fit: BoxFit.cover,
+                    ),
+                  ),
+                ),
+              ),
+              Column(
+                spacing: 8,
+                children: [
+                  Row(
+                    spacing: 12,
+                    children: [
+                      const BaseIcon(
+                        iconColor: AppColors.primary80,
+                        size: 16,
+                        iconData: Icons.calendar_month,
+                      ),
+                      Text(
+                        _formattedDateTime(),
+                        style: AppFonts.smallTextRegular,
+                      ),
+                    ],
+                  ),
+                  Row(
+                    spacing: 12,
+                    children: [
+                      const BaseIcon(
+                        iconColor: AppColors.secondary100,
+                        size: 16,
+                        iconData: Icons.comment,
+                      ),
+                      Expanded(
+                        child: TextLimitInputField(
+                          controller: commentController,
+                          hintText: 'Write Comment',
+                          maxLength: 30,
+                        ),
+                      ),
+                    ],
+                  ),
+                  Row(
+                    children: [
+                      const BaseIcon(
+                        iconColor: AppColors.primary80,
+                        size: 16,
+                        iconData: Icons.note,
+                      ),
+                      const SizedBox(width: 12),
+                      Expanded(
+                        child: DropdownMenu<String>(
+                          controller: journalController,
+                          inputDecorationTheme: const InputDecorationTheme(
+                            contentPadding: EdgeInsets.zero,
+                            border: InputBorder.none,
+                          ),
+                          initialSelection:
+                              widget.journals.isNotEmpty
+                                  ? widget.journalId
+                                  : null,
+                          menuStyle: MenuStyle(
+                            backgroundColor: WidgetStateProperty.all(
+                              AppColors.white,
+                            ),
+                            padding: WidgetStateProperty.all(EdgeInsets.zero),
+                            elevation: WidgetStateProperty.all(2),
+                          ),
+                          onSelected: (value) {
+                            _journalId = value!;
+                          },
+                          textStyle: AppFonts.smallTextRegular,
+                          dropdownMenuEntries:
+                              widget.journals.isEmpty
+                                  ? []
+                                  : List.generate(widget.journals.length, (
+                                    int index,
+                                  ) {
+                                    return DropdownMenuEntry(
+                                      label: widget.journals[index].name,
+                                      value: widget.journals[index].id,
+                                    );
+                                  }),
+                        ),
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+              Row(
+                spacing: 12,
+                children: [
+                  Expanded(
+                    child: BaseIconButton(
+                      buttonType: ButtonType.small,
+                      buttonColor: AppColors.primary100,
+                      iconName: Icons.edit,
+                      buttonName: 'Apply',
+                      onClick: () {
+                        widget.onTapApply(
+                          titleController.text,
+                          _journalId,
+                          commentController.text,
+                        );
+                      },
+                    ),
+                  ),
+                  Expanded(
+                    child: BaseIconButton(
+                      buttonType: ButtonType.small,
+                      buttonColor: AppColors.warning,
+                      iconName: Icons.delete,
+                      buttonName: 'Delete',
+                      onClick: widget.onTapDelete,
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        );
+      },
+      onClosing: () {},
+    );
+  }
+}
diff --git a/lib/presentation/screen/journal/journal_screen.dart b/lib/presentation/screen/journal/journal_screen.dart
index d4f11741..19988f28 100644
--- a/lib/presentation/screen/journal/journal_screen.dart
+++ b/lib/presentation/screen/journal/journal_screen.dart
@@ -113,7 +113,7 @@ class _JournalScreenState extends State<JournalScreen> {
 
                                   Navigator.pop(context);
                                 },
-                                onTapCancel: () => Navigator.pop(context),
+                                onTapDelete: () => Navigator.pop(context),
                               );
                             },
                           );
diff --git a/lib/presentation/screen/photos/photos_action.dart b/lib/presentation/screen/photos/photos_action.dart
index 661fb27c..ca4bb18f 100644
--- a/lib/presentation/screen/photos/photos_action.dart
+++ b/lib/presentation/screen/photos/photos_action.dart
@@ -4,7 +4,7 @@ sealed class PhotosAction {
   factory PhotosAction.photoFilterClick(int index) = PhotoFilterClick;
   factory PhotosAction.photoCardClick() = PhotoCardClick;
   factory PhotosAction.applyClick(PhotoModel photoModel) = PhotoApplyClick;
-  factory PhotosAction.shareClick() = PhotoShareClick;
+  factory PhotosAction.deleteClick(String photoId) = PhotoDeleteClick;
 }
 
 class PhotoFilterClick implements PhotosAction {
@@ -21,4 +21,8 @@ class PhotoApplyClick implements PhotosAction {
   PhotoApplyClick(this.photoModel);
 }
 
-class PhotoShareClick implements PhotosAction {}
+class PhotoDeleteClick implements PhotosAction {
+  final String photoId;
+
+  PhotoDeleteClick(this.photoId);
+}
diff --git a/lib/presentation/screen/photos/photos_screen.dart b/lib/presentation/screen/photos/photos_screen.dart
index c60a96af..0231ab54 100644
--- a/lib/presentation/screen/photos/photos_screen.dart
+++ b/lib/presentation/screen/photos/photos_screen.dart
@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:photopin/presentation/component/journal_card_image.dart';
 import 'package:photopin/presentation/component/map_filter.dart';
+import 'package:photopin/presentation/component/photo_edit_bottom_sheet.dart';
 import 'package:photopin/presentation/screen/photos/photos_action.dart';
 import 'package:photopin/presentation/screen/photos/photos_state.dart';
 
@@ -29,6 +30,7 @@ class PhotosScreen extends StatelessWidget {
                 onAction(PhotosAction.photoFilterClick(index));
               },
             ),
+            const SizedBox(height: 20),
             Expanded(
               child: Builder(
                 builder: (context) {
@@ -52,7 +54,7 @@ class PhotosScreen extends StatelessWidget {
                           crossAxisCount: 2, // 가로로 2개씩 표시
                           crossAxisSpacing: 10, // 가로 간격
                           mainAxisSpacing: 10, // 세로 간격
-                          childAspectRatio: 0.75, // 아이템의 가로:세로 비율
+                          childAspectRatio: 1, // 아이템의 가로:세로 비율
                         ),
                     itemCount: state.photos.length,
                     itemBuilder: (context, index) {
@@ -60,51 +62,44 @@ class PhotosScreen extends StatelessWidget {
 
                       return GestureDetector(
                         onTap: () {
-                          // showModalBottomSheet(
-                          //   context: context,
-                          //   isScrollControlled: true,
-                          //   backgroundColor: Colors.transparent,
-                          //   builder: (BuildContext context) {
-                          //     return Container(
-                          //       height: 500,
-                          //       decoration: const BoxDecoration(
-                          //         color: Colors.white,
-                          //         borderRadius: BorderRadius.only(
-                          //           topLeft: Radius.circular(16),
-                          //           topRight: Radius.circular(16),
-                          //         ),
-                          //       ),
-                          //       child: EditBottomSheet(
-                          //         title: photo.name,
-                          //         thumbnailUrl: photo.imageUrl,
-                          //         dateTime: photo.dateTime,
-                          //         comment: photo.comment,
-                          //         journalId: photo.journalId,
-                          //         onTapClose: () => Navigator.pop(context),
-                          //         onTapApply: (
-                          //           photoName,
-                          //           selectJournal,
-                          //           newComment,
-                          //         ) {
-                          //           // 여기서 photo를 업데이트하는 적절한 액션을 생성
-                          //           // 예: onAction(PhotosAction.updatePhotoComment(photo.id, newComment));
-                          //           onAction(
-                          //             PhotosAction.applyClick(
-                          //               photo.copyWith(
-                          //                 name: photoName,
-                          //                 comment: newComment,
-                          //                 journalId: selectJournal,
-                          //               ),
-                          //             ),
-                          //           );
-                          //           Navigator.pop(context);
-                          //         },
-                          //         onTapCancel: () => Navigator.pop(context),
-                          //         journals: state.journals,
-                          //       ),
-                          //     );
-                          //   },
-                          // );
+                          showModalBottomSheet(
+                            context: context,
+                            isScrollControlled: true,
+                            backgroundColor: Colors.transparent,
+                            builder: (BuildContext context) {
+                              return PhotoEditBottomSheet(
+                                title: photo.name,
+                                thumbnailUrl: photo.imageUrl,
+                                dateTime: photo.dateTime,
+                                comment: photo.comment,
+                                journalId: photo.journalId,
+                                onTapClose: () => Navigator.pop(context),
+                                onTapApply: (
+                                  photoName,
+                                  selectJournal,
+                                  newComment,
+                                ) {
+                                  // 여기서 photo를 업데이트하는 적절한 액션을 생성
+                                  // 예: onAction(PhotosAction.updatePhotoComment(photo.id, newComment));
+                                  onAction(
+                                    PhotosAction.applyClick(
+                                      photo.copyWith(
+                                        name: photoName,
+                                        comment: newComment,
+                                        journalId: selectJournal,
+                                      ),
+                                    ),
+                                  );
+                                  Navigator.pop(context);
+                                },
+                                onTapDelete: () {
+                                  onAction(PhotosAction.deleteClick(photo.id));
+                                  Navigator.pop(context);
+                                },
+                                journals: state.journals,
+                              );
+                            },
+                          );
                         },
                         child: JournalCardImage(
                           imageUrl: photo.imageUrl,
diff --git a/lib/presentation/screen/photos/photos_screen_root.dart b/lib/presentation/screen/photos/photos_screen_root.dart
index d750ae72..0cfa06d8 100644
--- a/lib/presentation/screen/photos/photos_screen_root.dart
+++ b/lib/presentation/screen/photos/photos_screen_root.dart
@@ -34,9 +34,8 @@ class _PhotosScreenRootState extends State<PhotosScreenRoot> {
                 throw UnimplementedError();
               case PhotoApplyClick():
                 widget.viewModel.onAction(action);
-              case PhotoShareClick():
-                // TODO: Handle this case.
-                throw UnimplementedError();
+              case PhotoDeleteClick():
+                widget.viewModel.onAction(action);
             }
           },
         );
diff --git a/lib/presentation/screen/photos/photos_view_model.dart b/lib/presentation/screen/photos/photos_view_model.dart
index 70119a9a..778f270a 100644
--- a/lib/presentation/screen/photos/photos_view_model.dart
+++ b/lib/presentation/screen/photos/photos_view_model.dart
@@ -1,6 +1,10 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import 'package:photopin/core/domain/journal_photo_collection.dart';
+import 'package:photopin/core/domain/photo_journal_list_model.dart';
 import 'package:photopin/core/usecase/get_journal_list_use_case.dart';
+import 'package:photopin/core/usecase/get_photo_journal_list_use_case.dart';
 import 'package:photopin/core/usecase/get_photo_list_use_case.dart';
 import 'package:photopin/core/usecase/get_photo_list_with_journal_id_use_case.dart';
 import 'package:photopin/photo/data/repository/photo_repository.dart';
@@ -13,6 +17,9 @@ class PhotosViewModel with ChangeNotifier {
   final GetJournalListUseCase _getJournalListUseCase;
   final GetPhotoListWithJournalIdUseCase _getPhotoListWithJournalIdUseCase;
   final PhotoRepository _photoRepository;
+  final GetPhotoJournalListUseCase _getPhotoJournalListUseCase;
+
+  StreamSubscription<PhotoJournalListModel>? _listStream;
   PhotosState _state = PhotosState();
 
   PhotosViewModel({
@@ -20,13 +27,21 @@ class PhotosViewModel with ChangeNotifier {
     required GetJournalListUseCase getJournalListUseCase,
     required GetPhotoListWithJournalIdUseCase getPhotoListWithJournalIdUseCase,
     required PhotoRepository photoRepository,
+    required GetPhotoJournalListUseCase getPhotoJournalListUseCase,
   }) : _getPhotoListUseCase = getPhotoListUseCase,
        _getJournalListUseCase = getJournalListUseCase,
        _getPhotoListWithJournalIdUseCase = getPhotoListWithJournalIdUseCase,
-       _photoRepository = photoRepository;
+       _photoRepository = photoRepository,
+       _getPhotoJournalListUseCase = getPhotoJournalListUseCase;
 
   PhotosState get state => _state;
 
+  @override
+  void dispose() {
+    _listStream?.cancel();
+    super.dispose();
+  }
+
   Future<void> _photoApplyClick(PhotoModel photoModel) async {
     await _photoRepository.updatePhoto(photoModel);
   }
@@ -67,20 +82,22 @@ class PhotosViewModel with ChangeNotifier {
     notifyListeners();
   }
 
+  Future<void> _photoDeleteClick(String photoId) async {
+    await _photoRepository.deletePhoto(photoId);
+  }
+
   Future<void> init() async {
     _state = _state.copyWith(isLoading: true);
     notifyListeners();
 
-    List<PhotoModel> photos = await _getPhotoListUseCase.execute();
-    final JournalPhotoCollection collection =
-        await _getJournalListUseCase.execute();
-
-    _state = _state.copyWith(
-      photos: photos,
-      journals: collection.journals,
-      isLoading: false,
-    );
-    notifyListeners();
+    _listStream = _getPhotoJournalListUseCase.execute().listen((collection) {
+      _state = _state.copyWith(
+        photos: collection.photos,
+        journals: collection.journals,
+        isLoading: false,
+      );
+      notifyListeners();
+    });
   }
 
   Future<void> onAction(PhotosAction action) async {
@@ -92,9 +109,8 @@ class PhotosViewModel with ChangeNotifier {
         throw UnimplementedError();
       case PhotoApplyClick():
         _photoApplyClick(action.photoModel);
-      case PhotoShareClick():
-        // TODO: Handle this case.
-        throw UnimplementedError();
+      case PhotoDeleteClick():
+        _photoDeleteClick(action.photoId);
     }
   }
 }
diff --git a/pubspec.lock b/pubspec.lock
index 74a8c1f5..f0a158c6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1096,6 +1096,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.5.0"
+  rxdart:
+    dependency: "direct main"
+    description:
+      name: rxdart
+      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.28.0"
   sanitize_html:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 3d0e2516..1998c28d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -69,6 +69,7 @@ dependencies:
   flutter_dotenv: ^5.2.1
   widget_to_marker: ^1.0.6
   google_maps_cluster_manager_2: ^3.2.0
+  rxdart: ^0.28.0
   firebase_messaging: ^15.2.5
   flutter_local_notifications: ^19.2.0
 
diff --git a/test/presentation/component/journal_edit_bottom_sheet_test.dart b/test/presentation/component/journal_edit_bottom_sheet_test.dart
index 010b916e..78ee2c7c 100644
--- a/test/presentation/component/journal_edit_bottom_sheet_test.dart
+++ b/test/presentation/component/journal_edit_bottom_sheet_test.dart
@@ -38,7 +38,7 @@ void main() {
                               title: title,
                               thumbnailUrl: imageUrl,
                               onTapApply: (JournalModel journal) {},
-                              onTapCancel: () {},
+                              onTapDelete: () {},
                               onTapClose: () {},
                               journal: journal,
                             );
@@ -94,7 +94,7 @@ void main() {
                                 title: title,
                                 thumbnailUrl: imageUrl,
                                 onTapApply: (JournalModel journal) {},
-                                onTapCancel: () {},
+                                onTapDelete: () {},
                                 onTapClose: () {
                                   tappedClose = true;
                                   Navigator.pop(context);
@@ -160,7 +160,7 @@ void main() {
                                   expectComment = journal.comment;
                                   expectTripWith = journal.tripWith;
                                 },
-                                onTapCancel: () {},
+                                onTapDelete: () {},
                                 onTapClose: () {},
                               );
                             },
@@ -214,7 +214,7 @@ void main() {
     },
   );
 
-  testWidgets('JournalEditBottomSheet의 Cancel을 탭하면 onTapCancel 콜백이 호출되야한다.', (
+  testWidgets('JournalEditBottomSheet의 Delete를 탭하면 onTapDelete 콜백이 호출되야한다.', (
     WidgetTester tester,
   ) async {
     await mockNetworkImagesFor(() async {
@@ -236,7 +236,7 @@ void main() {
                               journal: journal,
                               thumbnailUrl: imageUrl,
                               onTapApply: (JournalModel journal) {},
-                              onTapCancel: () {
+                              onTapDelete: () {
                                 Navigator.pop(context);
                               },
                               onTapClose: () {},


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR renames the cancel action to delete in bottom sheets, splits photo/journal edit sheets into separate components, and implements a real-time combined stream of photos and journals to power the Photos screen, while adjusting the grid layout and text overflow handling.

- Renamed `onTapCancel` to `onTapDelete` across components and tests
- Added `GetPhotoJournalListUseCase`, wiring up a combined stream and repository watch methods
- Updated UI: replaced commented bottom sheet code with `PhotoEditBottomSheet`, adjusted grid aspect ratio and text overflow

### Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                          | Description                                           |
| ------------------------------------------------------------- | ----------------------------------------------------- |
| test/presentation/component/journal_edit_bottom_sheet_test.dart | Updated tests for delete callback                     |
| pubspec.yaml                                                  | Added `rxdart` dependency                             |
| lib/presentation/screen/photos/photos_view_model.dart         | Subscribed to combined photo/journal stream, handled delete action |
| lib/presentation/screen/photos/photos_screen_root.dart        | Replaced share click with delete action               |
| lib/presentation/screen/photos/photos_screen.dart             | Switched to `PhotoEditBottomSheet`, changed aspect ratio |
| lib/presentation/screen/photos/photos_action.dart             | Replaced `PhotoShareClick` with `PhotoDeleteClick`    |
| lib/presentation/component/photo_edit_bottom_sheet.dart       | New `PhotoEditBottomSheet`, added delete button       |
| lib/presentation/component/journal_edit_bottom_sheet.dart     | Renamed `onTapCancel` to `onTapDelete`                |
| lib/presentation/component/journal_card_image.dart            | Added `maxLines` and `ellipsis` overflow              |
| lib/presentation/component/edit_bottom_sheet.dart             | Updated base API to use `onTapDelete`                 |
| lib/photo/data/repository/photo_repository_impl.dart          | Added `watchPhotos` method                            |
| lib/photo/data/data_source/photo_data_source_impl.dart        | Added `watchPhotos` stream implementation             |
| lib/core/usecase/get_photo_journal_list_use_case.dart         | Introduced combined photo/journal use case            |
| lib/core/di/di_setup.dart                                     | Registered new use case and wiring in DI              |
</details>



<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**lib/presentation/component/edit_bottom_sheet.dart:29**
* There’s a large block of commented-out code in the base `EditBottomSheet`; remove it to improve readability.
```
//   return BottomSheet(
```
**lib/presentation/screen/photos/photos_view_model.dart:22**
* [nitpick] The variable `_listStream` could be renamed to `_photoJournalSubscription` to better convey its purpose.
```
StreamSubscription<PhotoJournalListModel>? _listStream;
```
**lib/presentation/component/journal_edit_bottom_sheet.dart:240**
* The cancel button now invokes `onTapDelete`, which may confuse callers; consider restoring a separate `onTapCancel` callback or renaming for clarity.
```
onClick: widget.onTapDelete,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T05:52:04Z
user: login: dbwpghks56
body: 수정 부탁드려요~
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-17T05:54:13Z
user: login: choiexe1
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-17T07:25:58Z

Review comments:
diff_hunk: @@ -0,0 +1,233 @@
+import 'package:flutter/material.dart';
+import 'package:photopin/core/enums/button_type.dart';
+import 'package:photopin/core/extensions/datetime_extension.dart';
+import 'package:photopin/core/styles/app_color.dart';
+import 'package:photopin/core/styles/app_font.dart';
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/presentation/component/base_icon.dart';
+import 'package:photopin/presentation/component/base_icon_button.dart';
+import 'package:photopin/presentation/component/text_limit_input_field.dart';
+import 'package:photopin/presentation/component/edit_bottom_sheet.dart';
+
+class PhotoEditBottomSheet extends EditBottomSheet {
+  final List<JournalModel> journals;
+  final DateTime dateTime;
+  final String journalId;
+  final Function(String photoName, String journalId, String comment) onTapApply;
+
+  const PhotoEditBottomSheet({
+    super.key,
+    super.thumbnailUrl,
+    required super.title,
+    required super.comment,
+    required super.onTapDelete,
+    required super.onTapClose,
+    required this.journals,
+    required this.dateTime,
+    required this.onTapApply,
+    this.journalId = '',
+  });
+
+  @override
+  State<PhotoEditBottomSheet> createState() => _PhotoEditBottomSheetState();
+}
+
+class _PhotoEditBottomSheetState extends State<PhotoEditBottomSheet> {
+  final TextEditingController titleController = TextEditingController();
+  final TextEditingController commentController = TextEditingController();
+  final TextEditingController journalController = TextEditingController();
+  String _journalId = '';
+
+  String _formattedDateTime() => widget.dateTime.formatDateTimeString();
+
+  List<String> tripWith = [];
+
path: lib/presentation/component/photo_edit_bottom_sheet.dart
user: login: Copilot
body: The `tripWith` variable is declared but never used; consider removing it to reduce dead code.
```suggestion

```
created_at: 2025-05-17T05:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,233 @@
+import 'package:flutter/material.dart';
+import 'package:photopin/core/enums/button_type.dart';
+import 'package:photopin/core/extensions/datetime_extension.dart';
+import 'package:photopin/core/styles/app_color.dart';
+import 'package:photopin/core/styles/app_font.dart';
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/presentation/component/base_icon.dart';
+import 'package:photopin/presentation/component/base_icon_button.dart';
+import 'package:photopin/presentation/component/text_limit_input_field.dart';
+import 'package:photopin/presentation/component/edit_bottom_sheet.dart';
+
+class PhotoEditBottomSheet extends EditBottomSheet {
+  final List<JournalModel> journals;
+  final DateTime dateTime;
+  final String journalId;
+  final Function(String photoName, String journalId, String comment) onTapApply;
+
+  const PhotoEditBottomSheet({
+    super.key,
+    super.thumbnailUrl,
+    required super.title,
+    required super.comment,
+    required super.onTapDelete,
+    required super.onTapClose,
+    required this.journals,
+    required this.dateTime,
+    required this.onTapApply,
+    this.journalId = '',
+  });
+
+  @override
+  State<PhotoEditBottomSheet> createState() => _PhotoEditBottomSheetState();
+}
+
+class _PhotoEditBottomSheetState extends State<PhotoEditBottomSheet> {
+  final TextEditingController titleController = TextEditingController();
+  final TextEditingController commentController = TextEditingController();
+  final TextEditingController journalController = TextEditingController();
path: lib/presentation/component/photo_edit_bottom_sheet.dart
user: login: Copilot
body: The `journalController` isn’t disposed in `dispose()`; add `journalController.dispose()` to avoid memory leaks.
created_at: 2025-05-17T05:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,20 +82,22 @@ class PhotosViewModel with ChangeNotifier {
     notifyListeners();
   }
 
+  Future<void> _photoDeleteClick(String photoId) async {
+    await _photoRepository.deletePhoto(photoId);
+  }
+
   Future<void> init() async {
     _state = _state.copyWith(isLoading: true);
     notifyListeners();
 
-    List<PhotoModel> photos = await _getPhotoListUseCase.execute();
-    final JournalPhotoCollection collection =
-        await _getJournalListUseCase.execute();
-
-    _state = _state.copyWith(
-      photos: photos,
-      journals: collection.journals,
-      isLoading: false,
-    );
-    notifyListeners();
+    _listStream = _getPhotoJournalListUseCase.execute().listen((collection) {
+      _state = _state.copyWith(
+        photos: collection.photos,
+        journals: collection.journals,
+        isLoading: false,
+      );
+      notifyListeners();
+    });
path: lib/presentation/screen/photos/photos_view_model.dart
user: login: Copilot
body: The stream subscription lacks error handling; consider adding `onError` and lifecycle safeguards to handle stream errors gracefully.
created_at: 2025-05-17T05:52:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,233 @@
+import 'package:flutter/material.dart';
+import 'package:photopin/core/enums/button_type.dart';
+import 'package:photopin/core/extensions/datetime_extension.dart';
+import 'package:photopin/core/styles/app_color.dart';
+import 'package:photopin/core/styles/app_font.dart';
+import 'package:photopin/journal/domain/model/journal_model.dart';
+import 'package:photopin/presentation/component/base_icon.dart';
+import 'package:photopin/presentation/component/base_icon_button.dart';
+import 'package:photopin/presentation/component/text_limit_input_field.dart';
+import 'package:photopin/presentation/component/edit_bottom_sheet.dart';
+
+class PhotoEditBottomSheet extends EditBottomSheet {
+  final List<JournalModel> journals;
+  final DateTime dateTime;
+  final String journalId;
+  final Function(String photoName, String journalId, String comment) onTapApply;
+
+  const PhotoEditBottomSheet({
+    super.key,
+    super.thumbnailUrl,
+    required super.title,
+    required super.comment,
+    required super.onTapDelete,
+    required super.onTapClose,
+    required this.journals,
+    required this.dateTime,
+    required this.onTapApply,
+    this.journalId = '',
+  });
+
+  @override
+  State<PhotoEditBottomSheet> createState() => _PhotoEditBottomSheetState();
+}
+
+class _PhotoEditBottomSheetState extends State<PhotoEditBottomSheet> {
+  final TextEditingController titleController = TextEditingController();
+  final TextEditingController commentController = TextEditingController();
+  final TextEditingController journalController = TextEditingController();
+  String _journalId = '';
+
+  String _formattedDateTime() => widget.dateTime.formatDateTimeString();
+
+  List<String> tripWith = [];
+
+  @override
+  void initState() {
+    super.initState();
+    titleController.value = TextEditingValue(text: widget.title);
+    commentController.value = TextEditingValue(text: widget.comment);
+  }
+
+  @override
+  void dispose() {
+    titleController.dispose();
+    commentController.dispose();
+    super.dispose();
path: lib/presentation/component/photo_edit_bottom_sheet.dart
user: login: dbwpghks56
body: journalController.dispose() 가 부족합니다.
created_at: 2025-05-17T05:52:29Z
author_association: MEMBER

PR comments:
user: login: roychoi1012
created_at: 2025-05-17T07:10:23Z
body: @choiexe1 
author_association: MEMBER
user: login: choiexe1
created_at: 2025-05-17T07:24:47Z
body: 제환님이 리뷰해서 다시 어프로브 받아야할듯
author_association: MEMBER