Skip to content

[FEAT/#1580] 솝레터 메인 화면 뷰 구현#1586

Merged
seungjunGong merged 18 commits into
developfrom
feat/#1580-soptletter-main-ui
Jun 1, 2026
Merged

[FEAT/#1580] 솝레터 메인 화면 뷰 구현#1586
seungjunGong merged 18 commits into
developfrom
feat/#1580-soptletter-main-ui

Conversation

@seungjunGong
Copy link
Copy Markdown
Member

@seungjunGong seungjunGong commented May 21, 2026

Related issue 🛠

Work Description ✏️

  • 솝레터 메인 화면 뷰 구현
  • 화면이 빈경우 UI
  • 메모 리스트 UI
  • 메모 디테일 다이얼로그 UI

Screenshot 📸

image

Uncompleted Tasks 😅

  • 상단 스낵바 처리
  • 서버 스펙에 맞는 model 값 수정(현재는 하드코딩)
  • pull to refresh 추가 예정

To Reviewers 📢

아직 서버 스펙이랑 불안정한 부분들이 몇가지 있어서 우선 UI 그린거 먼저 올립니다!
추후에 서버 스펙이랑 미완성된 로직 추가할게요.

++ 스낵바, pull to refresh 추가했습니다. 스낵바 쪽은 write 에서도 쓰이는 것 같아서 래퍼 형태로 만들어보았는데 상위단의 공통 route 에서 처리하는 것도 괜찮을거 같은데 의견 부탁드립니다.

- `ic_active_heart_24`, `ic_inactive_heart_24` 하트 아이콘 추가
- `ic_alert_32`, `ic_close_32`, `ic_download_32`, `ic_edit_32`, `ic_trash_32` 기능성 아이콘 추가
- `ic_sopletter_memo_cloud`, `point`, `sharp`, `smooth` 등 메모지 형태 벡터 리소스 추가
- `img_sopletter_empty` 빈 화면 이미지 추가
- `SopletterMainScreen` 및 `SopletterMemoCard` 등 메인 화면 구성 요소 추가
- `SopletterMainViewModel` 및 `SopletterMainUiState`를 통한 상태 관리 로직 추가
- 메모 카드 모델(`SopletterMemoUiModel`) 및 관련 상수(색상, 회전 각도) 정의
- 작성된 메모가 없을 경우를 위한 `EmptySopletterContent` 추가
- `ic_edit_28.xml` 아이콘 리소스 추가
- `SopletterMainScreen` 내 작성 버튼(`EditSopletterFloatingButton`) 구현 및 배치
- `SopletterMainScreen` 내부에 구현되어 있던 컴포넌트들을 개별 파일로 분리 (`EmptySopletterContent`, `EditSopletterFloatingActionButton`, `SopletterMainTopBar`, `SopletterMemoCard`).
- 상단 바(`SopletterMainTopBar`)에 닫기, 다운로드, 신고하기 버튼 이벤트 핸들러 추가.
- 플로팅 버튼(`EditSopletterFloatingActionButton`)에 클릭 이벤트 처리를 위한 `noRippleClickable` 적용.
- 작성된 메모 유무에 따라 다운로드 버튼의 노출 여부를 제어하는 로직 추가.
- `SopletterMemoDetailDialog` 컴포넌트 및 관련 UI 상태(`SopletterMemoDetailDialogState`) 추가
- `SopletterMainUiState`에 선택된 메모 상세 상태(`selectedMemoDetail`) 필드 추가
- `SopletterMainViewModel` 내 메모 선택 및 해제 로직 구현
- `SopletterMemoCard` 클릭 이벤트 연결 및 메인 화면 다이얼로그 노출 로직 추가
- `SopletterMainPreviewParameterProvider`를 통한 미리보기 데이터 구조 개선 및 다이얼로그 프리뷰 추가
- 불필요한 중첩 Column을 제거하고 최상위 Column에 배경색과 패딩 설정을 통합하여 구조를 단순화했습니다.
- 수정 및 삭제 아이콘의 고정 크기(`size(20.dp)`) 제약과 좋아요 아이콘의 크기(`size(16.dp)`) 제약을 제거했습니다.
- 좋아요 섹션의 아이콘과 텍스트 사이 간격을 4.dp에서 2.dp로 조정했습니다.
- `SopletterMainViewModel`에 `onLikeClick` 메서드를 추가하여 좋아요 상태 및 카운트 토글 로직 구현
- `SopletterMainScreen`에서 상세 다이얼로그의 좋아요 클릭 이벤트를 ViewModel과 연결
- `SopletterMemoUiModel`에 서버 스펙 관련 작업 예정 주석 추가
@seungjunGong seungjunGong requested a review from a team as a code owner May 21, 2026 16:21
@seungjunGong seungjunGong added this to the 38th Android milestone May 21, 2026
- Replace `IconButton` with `Icon` using `noRippleClickable` modifier to remove the ripple effect.
- Apply the change to close, download, and report buttons within `SopletterMainTopBar`.
- `SopletterMainScreen` 내 `onEditFABClick` 버튼의 하단 패딩을 66.dp에서 24.dp로 조정함.
Copy link
Copy Markdown
Member

@sonms sonms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우 승준님의 고민이 돋보이는 pr이였습니다 와우띵! 역시 대승준!

onDeleteClick = { },
onDismissClick = { },
),
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapper를 따로 분리하신 이유가 있으실까요
제 생각은 순수한 데이터 모델을 유지하고 관심사를 분리하여 나누신 것 같은데
맞을까요?

제 생각에는 이 매퍼 함수가 오직 SopletterMemoUiModel과만 밀접하게 연관되어 있어서 나중에 모델에 필드가 추가되거나 변경될 때 한눈에 파악할 수 있도록 같은 파일에 묶어 응집도를 높이는 게 유지보수 측면에서 더 편리할 것 같은데 승준님의 생각을 듣고 싶습니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 방향에 공감해요! 처음에는 변환 로직을 분리하려는 의도로 mapper를 별도 파일로 두었는데 현재 mapper가 SopletterMemoUiModel 과 밀접하게 연관되어 있어 모델 필드 변경 시 같은 파일에서 함께 확인하는 편이 더 유지보수하기 좋을 것 같습니다.

해당 방식으로 수정해보겠습니다!

Comment on lines +5 to +23
@Stable
data class SopletterMemoDetailDialogState(
val memoId: Long,
val memoColor: SopletterMemoColor,
val writerName: String,
val isMine: Boolean,
val isLiked: Boolean,
val likeCount: Long,
val date: String,
val content: String,
val event: Event,
) {
@Stable
data class Event(
val onLikeClick: () -> Unit,
val onEditClick: () -> Unit,
val onDeleteClick: () -> Unit,
val onDismissClick: () -> Unit,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stable은 람다를 data class 내부 프로퍼티로 들고 있으면 Compose 컴파일러가 stable, immutable 추론을 하지 못할 때가 많다고 알고 있어요
추가로 state와 event가 DialogState안에 혼재되어 데이터와 이벤트가 함께 묶여있어 Event 객체가 State가 변경될때마다 불필요하게 재구성될 수도 있을 것 같습니다

그래서 interface로 따로 이벤트는 변수에서 제거하고

data class SopletterMemoDetailDialogState(
    val memoId: Long,
    val memoColor: SopletterMemoColor,
    val writerName: String,
    val isMine: Boolean,
    val isLiked: Boolean,
    val likeCount: Long,
    val date: String,
    val content: String,
)

interface SopletterMemoDetailActions {
    fun onLikeClick()
    fun onEditClick()
    fun onDeleteClick()
    fun onDismissClick()
}

class SopletterMainViewModel : ViewModel(), SopletterMemoDetailActions {

뷰모델에서 구현하는 방식이나

MVI 를 필요한 부분에 적용하여

// Event 정의
sealed interface SopletterMemoDetailEvent {
    // 다이얼로그 관련 액션
    data class MemoClick(val memoId: Long) : SopletterMainEvent // 어떤 메모를 눌렀는지 데이터 포함 가능
    object LikeClick : SopletterMainEvent
    object EditClick : SopletterMainEvent
    object DeleteClick : SopletterMainEvent
    object DialogDismiss : SopletterMainEvent
}

@Composable
fun SopletterMainScreen(
    uiState: SopletterMainUiState,
    onEvent: (SopletterMainEvent) -> Unit,
) {
if (uiState.selectedMemo != null) {
        SopletterMemoDetailDialog(
            state = uiState.selectedMemo,
            // 하위 다이얼로그에도 동일한 단일 통로를 넘기거나, 필요한 이벤트만 맵핑해서 전달
            onEvent = onEvent 
        )
    }
}

// ViewModel
fun onEvent(event: SopletterMainEvent) {
        when (event) {
            is SopletterMainEvent.MemoClick -> handleMemoClick(event.memoId)
            SopletterMainEvent.LikeClick -> handleLike()
            SopletterMainEvent.EditClick -> navigateToEdit()
            SopletterMainEvent.DeleteClick -> deleteMemo()
            SopletterMainEvent.DialogDismiss -> clearSelectedMemo()
          
    }

방식을 생각했는데 승준님의 생각을 여쭤보고 싶습니다!

Copy link
Copy Markdown
Member Author

@seungjunGong seungjunGong May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 의견과 예시까지 감사합니다!

초기에는 메인 화면과 다이얼로그의 상태/이벤트를 분리하고 싶어서 다이얼로그에 필요한 데이터와 액션을 하나의 모델로 묶는 방식으로 구성했습니다. 말씀주신 것처럼 DialogState 내부에 Event를 함께 두면 상태와 액션의 책임이 섞이고 상태 변경 시 copy 과정에서 리컴포지션의 우려도 있겠네요..!

제안 주신 내용 참고해서 다이얼로그는 별도 contract로 분리하고 State는 순수 데이터만 가지도록 두고 Actions는 별도로 전달하는 방향으로 수정해볼게요! 메인 화면은 기존 구조를 유지하되 다이얼로그 쪽만 분리하는 방식으로 반영하겠습니다.

import org.sopt.official.feature.sopletter.main.model.SopletterMemoUiModel
import org.sopt.official.sopletter.R

class SopletterMainPreviewParameterProvider : PreviewParameterProvider<SopletterMainUiState> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크... 대승준

- 수정 및 삭제 아이콘의 `tint`를 `Color.Unspecified`로 변경하여 원본 아이콘 색상을 유지하도록 수정했습니다.
- 확인 버튼의 텍스트 색상을 `White`에서 `SoptTheme.colors.primary`로 변경했습니다.
Copy link
Copy Markdown
Member

@1971123-seongmin 1971123-seongmin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다

val content: String,
val event: Event,
) {
@Stable
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 @stable 사용하신 이유가 궁금합니다

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event도 DialogState와 함께 Composable에 전달되는 콜백 묶음 객체라, 안정성 힌트를 주려는 의도로 @stable을 붙였습니다.

다만 위에서 말한거처럼 상태와 함께 이벤트를 같이 두다보니 recomposition 이 발생할 수 있는 지점이 있어서 이부분은 수정해두겠습니다 !

- `SopletterMemoDetailDialogContract`를 신규 생성하여 State와 Actions(이벤트 핸들러)를 인터페이스로 분리 정의했습니다.
- 기존 `SopletterMemoDetailDialogState` 클래스를 삭제하고 Contract 기반의 구조로 대체했습니다.
- `SopletterMainViewModel`에서 `Actions` 인터페이스를 구현하여 이벤트 처리를 담당하도록 변경했습니다.
- UI 모델 및 스크린 컴포넌트에서 상태 모델과 액션 파라미터를 분리하여 가독성을 높였습니다.
- `SopletterMainUiState` 및 관련 모델에 `@Immutable` 어노테이션을 추가하여 Compose 안정성을 강화했습니다.
- `SopletterMemoMapper.kt`를 삭제하고 매핑 로직을 `SopletterMemoDetailDialogContract.kt` 내부로 이동.
- `toDetailDialogState` 함수명을 `toMemoDetailDialogState`로 변경.
- `SopletterMainScreen` 및 `SopletterMainPreviewParameterProvider`에서 변경된 함수명을 사용하도록 수정.
- `SopletterScaffold` 및 `SopletterSnackBar` 컴포넌트 추가
- `SopletterMainViewModel`에 `snackbarMessage` SharedFlow 추가 및 본인 글 좋아요 클릭 시 에러 메시지 노출 로직 구현
- `SopletterMainScreen`에 `SnackbarHostState` 적용 및 메시지 수집 로직 추가
- 스낵바용 경고 아이콘 리소스(`ic_alert_circle_20.xml`) 추가
- `SopletterMainViewModel` 내 리스트 새로고침을 위한 `refreshMemoList` 함수 추가
- `SopletterMainScreen`에 `PullRefreshIndicator` 및 `pullRefresh` modifier 적용
- 로딩 상태(`isLoading`)에 따른 새로고침 UI 연동
- `SopletterMemoDetailDialog.kt` 파일에서 사용되지 않는 `White` 색상 임포트 제거.
Copy link
Copy Markdown
Member

@sonms sonms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 어푸푸푸푸풒ㅍ

Comment on lines +6 to +25
interface SopletterMemoDetailDialogContract {
@Immutable
data class State(
val memoId: Long,
val memoColor: SopletterMemoColor,
val writerName: String,
val isMine: Boolean,
val isLiked: Boolean,
val likeCount: Long,
val date: String,
val content: String,
)

interface Actions {
fun onLikeClick()
fun onEditClick()
fun onDeleteClick()
fun onDismissClick()
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따봉!

onDeleteClick = { /* TODO delete click */ },
onDismissClick = viewModel::clearSelectedMemo,
),
dialogActions = viewModel,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JW) 혹시 이 부분은 viewModel을 넘기시는 이유가 있을까요?

Copy link
Copy Markdown
Member Author

@seungjunGong seungjunGong May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ViewModel이 SopletterMemoDetailDialogContract.Actions의 구현체 역할을 하고 있어서 우선은 Actions 를 위한 별도 래핑 객체를 없이 사용하기 위해서 그대로 전달했습니다.
근데 이제 보니 현재처럼 바로 viewModel을 넘기면 의도가 충분히 드러나지 않을 수 있을거 같다는 생각이 드네요.

val dialogActions: SopletterMemoDetailDialogContract.Actions = viewModel 요런식으로

Actions 타입으로 한 번 명시해서 전달하는 방식이 더 나을거 같은데 괜찮으면 해당 방식으로 수정해볼게요 !

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좀 더 가독성이 올라갈 것 같아 좋은 방식 같습니당! 확인 감사합니다~~

- `viewModel`에서 `SopletterMemoDetailDialogContract.Actions` 타입의 `dialogActions` 변수를 별도로 추출하여 가독성 개선
- UI 컴포넌트에 `viewModel` 대신 추출된 `dialogActions`를 전달하도록 수정
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Summary by CodeRabbit

새로운 기능

  • 솝레터 메인 화면 - 메모 목록을 2열 그리드로 표시
  • 메모 상세 보기 - 메모 선택 시 상세 정보 다이얼로그 표시
  • 좋아요 기능 - 메모에 좋아요 토글 가능 (본인 작성 제외)
  • 당겨서 새로고침 - 메모 목록 갱신 기능
  • 빈 상태 화면 - 메모가 없을 때 안내 메시지 표시
  • 상단 네비게이션 - 기수 정보, 다운로드, 신고 아이콘 포함
  • 메모 작성 버튼 - 우측 하단 플로팅 버튼으로 새 메모 작성 접근

워크스루

Sopletter 메인 화면의 Jetpack Compose 기반 전체 구현입니다. 상태 모델부터 시작하여 뷰모델을 통한 상태 관리, Scaffold 구성, 메인 화면 라우팅, 메모 리스트 및 상세 다이얼로그 렌더링, 보조 컴포넌트 제공, 그리고 미리보기 및 드로어블 리소스를 포함합니다.

변경사항

Sopletter 메인 화면 기능

레이어 / 파일 요약
상태 및 UI 모델 정의
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/model/SopletterMainUiState.kt, SopletterMemoUiModel.kt
SopletterMainUiState는 generation, 메모 리스트, 선택된 상세, 로딩 상태를 @Immutable로 관리합니다. SopletterMemoUiModel은 회전 타입과 색상 enum과 함께 메모 ID, 메시지, 도형 이미지 리소스를 포함합니다.
메모 상세 다이얼로그 계약 정의
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/contract/SopletterMemoDetailDialogContract.kt
SopletterMemoDetailDialogContract는 State 데이터 클래스(메모 ID, 색상, 작성자, 본인 여부, 좋아요 여부/수, 날짜, 내용)와 Actions 인터페이스(좋아요, 수정, 삭제, 닫기)를 정의합니다. 확장 함수로 메모 모델을 다이얼로그 상태로 변환합니다.
뷰모델 구현 및 상태 관리
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/SopletterMainViewModel.kt
SopletterMainViewModel은 uiState와 snackbarMessage를 StateFlow/SharedFlow로 노출합니다. 메모 선택 시 상세를 갱신하고, 좋아요 클릭 시 본인 메모면 스낵바 메시지를 방출하고 타인 메모면 상태를 업데이트합니다. 새로고침과 다이얼로그 dismiss 동작을 처리합니다.
Scaffold 컨테이너 및 SnackbarHost 구성
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/component/SopletterScaffold.kt
SopletterScaffold는 테마 색상을 적용한 Scaffold를 제공하고, 상단 중앙에 배치된 SnackbarHost를 설정합니다. SopletterSnackBar는 알림 아이콘과 메시지를 가로로 배열하여 테마 색상, 라운드 배경, 간격/패딩을 적용합니다.
메인 화면 라우트 및 ViewModel 통합
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/SopletterMainScreen.kt (라우트 부분)
SopletterMainRoute는 Hilt로 ViewModel을 주입받고, lifecycleRepeatingJob을 사용해 uiState를 수집합니다. LaunchedEffect에서 snackbarMessage 플로우를 구독하여 SnackbarHostState에 스낵바를 표시한 후 SopletterScaffold를 통해 메인 화면을 렌더링합니다.
메인 화면 콘텐츠 렌더링
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/SopletterMainScreen.kt (화면 부분)
SopletterMainScreen은 rememberPullRefreshState를 사용해 Pull-to-refresh를 구현합니다. 메모 리스트 여부에 따라 빈 상태 또는 2열 LazyVerticalStaggeredGrid로 메모 카드를 렌더링하며, 인덱스별 오프셋을 적용합니다. 하단 우측에 FAB, 상단에 새로고침 인디케이터를 배치하고, selectedMemoDetail이 있을 때 다이얼로그를 표시합니다.
메모 카드 및 상세 다이얼로그
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/component/SopletterMemoCard.kt, SopletterMemoDetailDialog.kt
SopletterMemoCard는 회전, 도형 이미지, 메시지를 지정된 크기와 스타일로 렌더링합니다. SopletterMemoDetailDialog는 플랫폼 기본 폭을 사용하지 않으며, 작성자명 표시, 본인 여부에 따른 수정/삭제 아이콘 조건부 표시, 메모 본문과 날짜, 좋아요 영역(동적 하트 아이콘과 좋아요 수)을 포함합니다.
보조 UI 컴포넌트
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/component/SopletterMainTopBar.kt, EditSopletterFloatingButton.kt, EmptySopletterContent.kt
SopletterMainTopBar는 generation 텍스트와 닫기 아이콘을 항상 표시하고, 다운로드 아이콘을 조건부로 표시합니다. EditSopletterFloatingActionButton은 64dp 크기의 흰색 배경 사각형에 편집 아이콘을 중앙 배치합니다. EmptySopletterContent는 빈 상태 일러스트와 안내 텍스트를 중앙에 렌더링합니다.
미리보기 파라미터 제공자 및 드로어블
feature/sopletter/src/main/java/org/sopt/official/feature/sopletter/main/preview/SopletterMainPreviewParameterProvider.kt, feature/sopletter/src/main/res/drawable/*
SopletterMainPreviewParameterProvider는 빈 상태, 메모 리스트, 선택된 메모의 세 가지 미리보기 시나리오를 제공합니다. 14개의 벡터 드로어블(활성/비활성 하트, 알림, 닫기, 다운로드, 편집, 삭제, 메모 구름/점/샤프/스무스 모양)이 메모 카드와 다이얼로그의 아이콘을 지원합니다.

시퀀스 다이어그램

sequenceDiagram
  participant User as 사용자
  participant Route as MainRoute
  participant ViewModel as ViewModel
  participant Screen as MainScreen
  participant Dialog as Dialog
  
  User->>Route: 화면 진입
  Route->>ViewModel: uiState 수집
  Route->>Screen: 상태 전달
  Screen->>Screen: Pull-to-refresh 상태 구성
  alt 메모 리스트 있음
    Screen->>Screen: LazyVerticalStaggeredGrid 렌더링
    User->>Screen: 메모 카드 클릭
    Screen->>ViewModel: onMemoClick 호출
    ViewModel->>Route: selectedMemoDetail 갱신
    Route->>Dialog: 다이얼로그 표시
    User->>Dialog: 좋아요 클릭
    Dialog->>ViewModel: onLikeClick 호출
    ViewModel->>ViewModel: 좋아요 상태 갱신
  else 메모 리스트 비어있음
    Screen->>Screen: EmptySopletterContent 표시
  end
  User->>Screen: Pull-to-refresh
  Screen->>ViewModel: refreshMemoList 호출
Loading

🎯 3 (중간) | ⏱️ ~25분

🐰 Sopletter의 메인 화면이 피어났네요!
메모 카드들이 춤을 춘다 (회전까지!)
다이얼로그가 열렸다 닫혔다
좋아요 하트는 반짝반짝
당겨서 새로고침, 아주 멋진 흐름! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly identifies the main objective: implementing the Sopletter main screen view (솝레터 메인 화면 뷰 구현), which matches the primary changes in the changeset.
Description check ✅ Passed The description is directly related to the changeset, detailing the implemented UI components (empty state, memo list, memo detail dialog) and noting incomplete tasks like server-spec model updates and hardcoded values.
Linked Issues check ✅ Passed The PR satisfies the linked issue #1580 objectives by implementing the Sopletter main screen view with all required UI components (main view, empty state, memo list, detail dialog) and common components as specified.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the Sopletter main screen feature. Added files include UI components, ViewModels, UI models, contracts, resources (drawables), and preview utilities—all directly supporting the main screen implementation objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#1580-soptletter-main-ui

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seungjunGong seungjunGong merged commit 057b3e1 into develop Jun 1, 2026
1 of 2 checks passed
@seungjunGong seungjunGong deleted the feat/#1580-soptletter-main-ui branch June 1, 2026 14:33
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.

[FEAT] 솝레터 메인 화면 뷰 구현

4 participants