Skip to content

feat: 공지 리스트/상세/작성 본 구현#61

Open
Dongju3079 wants to merge 28 commits into
feat/#60-meetdetail-redesignfrom
feat/#37-notice-impl
Open

feat: 공지 리스트/상세/작성 본 구현#61
Dongju3079 wants to merge 28 commits into
feat/#60-meetdetail-redesignfrom
feat/#37-notice-impl

Conversation

@Dongju3079

Copy link
Copy Markdown
Collaborator

Summary

  • feat/#60-meetdetail-redesign이 placeholder만 두고 보류했던 공지 화면 3개를 실제 동작하도록 본 구현
  • Data 레이어(DefaultNoticeRepo + 8개 endpoint + DTO) + Domain UseCase 7종(Mock 포함)
  • Presentation 레이어 — SwiftUI + @StateObject/ObservableObject 패턴
    • 리스트: 밑줄 세그먼트(슬라이드 애니메이션) + 모임장 전용 leading swipe 핀(토글 API) + 우상단 연필
    • 상세: 모임공지/시스템 분기 + 우상단 점 세개 메뉴(DefaultSheetViewController) + 댓글 메뉴(수정/삭제/신고) + 댓글 수정 모드 + 프로필 이미지 확대(PhotoBookViewController)
    • 작성: 등록/수정 공용(Mode.create/.edit) + 키보드 dismiss + TextEditor 가변 + 500자 제한
  • 인프라: CustomNavigationBar trailing 슬롯 확장 + InteractivePopHostingController(SwiftUI 자식 화면 edge swipe pop)

API

Endpoint 비고
GET /notice/list/{meetId} 페이지네이션
POST /notice/create 작성
PATCH /notice/update/{id} 수정
DELETE /notice/{id} 삭제
PATCH/DELETE /notice/pin/{id} 고정/해제
GET/POST /comment/notice/{id} 공지 댓글
PATCH/DELETE /comment/{id} 일반 댓글 API 재사용
POST /comment/report 댓글 신고 (ReportPost.comment)

API 미구현: 공지 자체 신고(/notice/report) — 모임원 페이지 메뉴 "신고하기"는 placeholder Toast로 처리 (TODO 주석)

주요 결정

  • NoticeFlowEntry.detail에 isCreator 추가 — 페이지 메뉴 분기에 필요
  • 시스템 공지(.system)는 같은 View에서 분기 — 댓글/입력바 숨김, 작성자 = "Mople" + 앱 로고
  • 공지 단건 GET이 없음 → entry에 Notice 객체 통째로 전달, list 응답을 detail에 그대로 사용
  • Notification.noticeUpdated 채널로 리스트 자동 reload
  • edge swipe 분리: list(root) 좌 edge → 모달 dismiss / detail·compose(자식) 좌 edge → 표준 pop

Base

이 PR의 base는 feat/#60-meetdetail-redesign. #60이 develop으로 머지된 뒤 따라서 develop으로 도달.

Test plan

  • 리스트 — 세그먼트 탭 시 검은 바 슬라이드 / 좌 edge swipe로 modal dismiss / 모임장 swipe 핀 토글 동작
  • 리스트 → 상세 진입 후 좌 edge swipe → 리스트로 pop
  • 모임공지 상세: 우상단 점 세개 → 모임장(수정/삭제) vs 모임원(신고)
  • 시스템 공지 상세: 메뉴 버튼 안 보임, 댓글/입력바 없음
  • 댓글 셀 점 세개 → 본인(수정/삭제) vs 타인(신고) 분기
  • 댓글 수정 → 입력바 상단 "댓글 수정중" 표시 + 수정 완료 후 정상 갱신
  • 프로필 이미지 탭 → PhotoBook 모달
  • 작성: 키보드 → 버튼이 따라 올라옴 / 빈 영역 탭 dismiss / 500자 초과 시 cut
  • 공지 수정 → Compose에 prefill + "수정 완료" 버튼 → pop 후 리스트 자동 reload
  • 공지 삭제 → 상세 pop + 리스트에서 사라짐

Closes #37

- DefaultNoticeRepo: /notice/list, /notice/create, /notice/update,
  /notice/{id} DELETE, /notice/pin/{id} PATCH·DELETE,
  /comment/notice/{id} GET·POST 매핑
- NoticeCommentResponse DTO 추가 (Post 댓글과 달리 likes/replies/mentions
  필드가 응답에 없어 Comment 도메인 일부만 채움)
- Notice UseCase 7종: FetchNoticeList / CreateNotice / UpdateNotice /
  DeleteNotice / TogglePinNotice / FetchNoticeCommentList /
  CreateNoticeComment (각각 #if DEV Mock 포함)
- APIEndpoints에 위 엔드포인트 함수 추가
리스트
- 피그마 4154-3802 기준 밑줄 세그먼트(전체/모임공지/시스템) +
  matchedGeometryEffect로 슬라이드 애니메이션
- 모임장 전용 leading swipe action 핀(토글 API 연동)
- 우상단 연필(모임장만) → 작성 화면 push
- 셀 디자인: 흰 row + hairline, 본문 1줄 + 메타(절대/상대시간)

상세
- 본문 카드(작성자/날짜/본문) + 댓글 섹션 + 하단 입력바
- 시스템 공지(.system) 분기: 댓글/입력바 숨김, 작성자 = Mople 앱 로고
- 우상단 점 세개 메뉴(DefaultSheetViewController):
  · 모임장: 공지 수정 → Compose .edit / 공지 삭제 → DeleteNotice
  · 모임원: 신고하기(공지 신고 API 미구현이라 placeholder Toast)
- 댓글 셀 메뉴: 본인은 수정/삭제, 타인은 신고(ReportPost .comment)
- 댓글 수정 모드: 입력바 상단 "댓글 수정중" 라벨 + 취소 버튼
- 프로필 이미지 탭 → PhotoBookViewController modal present

작성
- 등록/수정 공용 화면 (NoticeComposeViewModel.Mode .create/.edit)
- 키보드 처리: 빈 영역 탭 dismiss, 버튼이 키보드 따라 올라옴
- TextEditor 가변 + 스크롤, 500자 제한(prefix)
- 등록 버튼 색: PlanCreate 패턴 (.appPrimary↔.disablePrimary,
  .primaryText↔.disableText)

인프라
- CustomNavigationBar trailing 슬롯 추가 (제네릭 ViewBuilder)
- InteractivePopHostingController: SwiftUI 자식 화면에서 좌 edge swipe
  표준 pop 동작하도록 viewDidAppear에서 popGesture 활성화
- Notification.noticeUpdated 채널: 공지 변경 시 리스트 자동 reload
- MeetDetail → NoticeDetail 진입 시 isCreator 전달

Closes #37
Dongju3079 added 26 commits May 26, 2026 23:03
SwiftUI .background+.onTapGesture는 자식(TextEditor 등)이 차지한
영역에서 hit testing이 부모로 전달되지 않아 간헐적으로 안 잡히는
문제가 있었음.

InteractivePopHostingController의 root view에 UITapGestureRecognizer
를 달아 더 신뢰성 있게 처리.
- cancelsTouchesInView = false: SwiftUI Button 등 컨트롤은 함께 동작
- shouldReceive에서 UITextView/UITextField 위 탭은 제외 →
  입력 컴포넌트의 포커싱/커서 동작 정상 유지

NoticeDetail, NoticeCompose 둘 다 이 controller로 감싸져 있어 자동 적용.
양쪽 View의 SwiftUI background tap 코드는 중복 동작이라 제거.
기존 SwiftUI 구현이 단순 TextField + 아이콘이라 일정 상세의
ChatingTextFieldView와 시각적으로 달랐음. UIKit 원본과 동일하게:

- 박스형 입력 (.bgInput + cornerRadius 8), 내부 padding 좌우 8/상하 18
- placeholder는 .text04, ZStack(.topLeading)로 동일 위치 겹침
- send 영역 폭 52pt, 아이콘은 bottom-trailing에 leading 12 / bottom 6
  (height = width 1:1)
- isEditMode 동작: 키보드 떠있을 때(inputFocused)만 send 노출
- editLabel: 25x70, cornerRadius 4, bgSecondary, Body2.medium, text03
- 전체 패딩 top 16 / horizontal 20 / bottom safeArea
- maxTextLine 4 (lineLimit 1...4)
- tint = .text02

멘션 기능은 그대로 제외.
TextField(axis: .vertical)은 UITextView 기반인 ChatingTextFieldView의
줄바꿈/자동 높이/스크롤 동작과 미묘하게 달랐음.

- TextEditor(UITextView 기반)로 변경 → 줄바꿈 자유, 자동 높이 확장,
  초과 시 자체 스크롤 모두 매칭
- minHeight ≈ 1줄(20pt), maxHeight ≈ 4줄(80pt). maxTextLine 4와 매핑
- TextEditor 내부 inset(~5pt) 보정해 ChatingTextFieldView 외관과 일치
- placeholder는 ZStack(.topLeading) overlay 유지
- Notice/{anchor,do_anchor,meet_megaphone,megaphone,pencil,system}.imageset
  생성. iOS 17+ ImageResource로 자동 노출(.anchor, .pencil, .doAnchor 등)
- MeetDetailViewController 확성기 버튼: SF Symbol → .meetMegaphone 에셋
- NoticeListView 우상단 작성 버튼: square.and.pencil → .pencil 에셋 (24pt)
- NoticeListRow 좌측 타입 아이콘 추가
  · 고정(pinned) 공지: .anchor (24x24)
  · 모임공지(.custom) / 기본: .megaphone (24 컨테이너에 20)
  · 시스템 공지(.system): .system (24 컨테이너에 20)
- 셀 leading swipe: pin.fill → .doAnchor
  · 고정 요청 시 .appPrimary, 해제 시 .appRed 배경 분기
원인
- MockTogglePinNoticeUseCase가 응답으로 type: .custom을 항상 반환
- ViewModel에서 응답을 통째로 로컬 배열에 덮어써서 시스템 공지가
  고정되면 type이 .custom으로 바뀌고 megaphone 아이콘이 표시됨

수정
- togglePin은 의미상 isPinned/version 외 필드를 바꾸지 않으므로
  응답에서 isPinned/version만 머지하고 type/content/createdAt 등은
  기존 Notice 값 보존
- Mock의 잘못된 응답 + 실제 서버 응답이 일부 필드를 누락해도 안전
증상 (라이브 모드)
- 모임장으로 모임 상세 진입해도 "공지를 작성해보세요" 툴팁 미노출
- 공지 리스트 우상단 연필(작성) 버튼 미노출

원인
- 서버 MeetResponse에 isCreator 필드가 없어 MeetResponse.toDomain()이
  isCreator=false(default)로 시작
- FetchMeetPageUseCase는 verifyCreator(currentUserId == creatorId)
  호출로 isCreator를 채우는데, FetchMeetDetailUseCase는 그 호출이 누락
- Mock은 isCreator: true 하드코딩이라 dev 모드에선 정상 동작했음
- 결과: 라이브 모드 모임장이라도 isCreator=false 고정 → 모임장 전용 UI 미노출

수정
- FetchMeetDetailUseCase에 UserSessionProvider 주입
- execute 시 verifyCreator(with:) 호출하여 currentUserId와
  creatorId 비교로 isCreator 채움 (FetchMeetPage와 동일 패턴)
- DI 컨테이너(MeetDetailSceneDIContainer)에서 userSession 전달
기존 상태
- FetchMeetPageUseCase만 verifyCreator로 isCreator 채움
- FetchMeetDetail / CreateMeet / EditMeet UseCase는 verifyCreator 누락
- MeetSetupViewReactor는 자체적으로 UserInfoStorage 비교
- 두 패턴(UseCase / Reactor)이 혼재 — 일관성 없음

통일 방향
- 단일 진실 소스: Meet.isCreator
- UseCase 레이어가 채우는 책임 (FetchMeetPage와 동일 패턴)
- Reactor/ViewModel은 meet.isCreator를 그대로 신뢰

변경
- CreateMeetUseCase: UserSessionProvider 주입 + verifyCreator 추가
- EditMeetUseCase: UserSessionProvider 주입 + verifyCreator 추가
- ViewDIContainer: userSession 전달받도록 생성자 확장
- AppDIContainer: ViewDIContainer 생성 시 userSession 전달
- MeetSetupViewReactor.setMeetInfo: UserInfoStorage 직접 비교 제거,
  meet.isCreator 그대로 사용

영향
- 라이브 모드에서 새 모임 생성/수정 후 모임장 인식 정상화
  (이전에 잠재 버그 — Mock이 isCreator: true 박혀있어 dev에선 안 드러났음)
- TransferMeetViewModel은 양도 후 isCreator=false 명시 세팅 그대로 유지
  (양도 직후 본인은 모임장 아님 — 의미적으로 옳음)
1. 작성 유도 툴팁 — 모임별 첫 진입 1회만 표시
   - NoticeTooltipMemory(UserDefaults)로 meetId별 seen 플래그 저장
   - applyMeet에서 hasSeen 체크 후 첫 노출 시 즉시 markSeen

2. 확성기 파란 점 배지 — 향후 개발 예정으로 일단 비활성화
   - megaphoneBadge.isHidden = true 고정
   - 서버에 unread 신호가 추가되면 hasNotice 분기 복원 예정

3-4. 핀 토글 → 모임상세 pinnedNotice 자동 반영
   - NotificationManager에 NoticePayload(.created/.updated/.deleted) 추가
   - NoticeListViewModel.togglePin 후 NoticePayload.updated 발행
   - MeetDetailViewReactor가 addNoticeObservable 구독
     · 핀(isPinned=true): 기존 pinnedNotice 유무 무관하게 새 공지로 교체
     · 해제(isPinned=false):
         - 메인의 pinnedNotice가 같은 noticeId면 nil 처리
         - 다른 공지면 변경 없음
   - Meet 엔티티에 with(pinnedNotice:) copy 헬퍼 추가
     (pinnedNotice가 let이라 부분 갱신 불가 → 새 인스턴스로)
- 후기 추천 알럿 게시글별 최초 1회만 노출 (UserDefaults)
- 댓글 최신순 정렬에서 새 댓글 상단 추가
- 대댓글 페이지 부모 댓글 상단 고정 표시
- 대댓글 페이지 당겨서 새로고침 정상 종료
- 대댓글 생성/삭제 시 부모 댓글 답글 수 반영
- 메인 댓글 수에서 대댓글 제외 (부모 댓글 수만 표시)
- 대댓글 페이지 부모 댓글 좋아요 메인 반영
- 댓글 전송 후 키보드 내림
- 댓글 정렬(상단 삽입)·페이지네이션·서버 totalCount·수정 포커스·본문 갱신·Kingfisher·pull-to-refresh·로딩셀·L10n·수정모드 취소버튼 제거 등 일정/리뷰와 정합화
- 키보드: iOS18 ScrollPosition/onScrollGeometryChange로 규칙1/2 구현, 입력바 fixedSize, 스크롤 보정 타이밍 수정
- 새로고침: isLoading(블로킹 로더)이 refresh 제스처를 취소시키는 문제 → showLoadingIndicator 분리 + .handled 에러 필터 (상세/목록 둘 다)
- 단건 조회 연결: 진입/새로고침 시 공지 본문도 fresh fetch (loadNotice)
- 공지 작성자(writer) 표시 + 본문 수정 즉시 반영
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant