# MailplugBoard
> 메일플러그 게시판과 게시글을 조회하는 앱입니다.
>
> 프로젝트 기간: 2023-10-26 ~ 2023-10-31
>
> 소스 코드는 보안상의 이유(API의 인증 헤더)로, Github의 private repository에 작업하였습니다.
> [README만을 공개하기 위한 repository](https://github.com/kokkilE/ios-board-readme/blob/main/README.md)를 참고 부탁드립니다.
</br>
## 지원자
| 조향래 |
| :---: |
| <Img src = "https://hackmd.io/_uploads/SJL_ZRGw2.jpg" height=300> |
| [Github Profile](https://github.com/kokkilE) |
</br>
## 목차
1. [개발 환경](#1-개발-환경)
1.1. [적용 프레임워크](#11-적용-프레임워크)
1.2. [아키텍처](#12-아키텍처)
2. [구현 기능](#2-구현-기능)
2.1. [게시판 선택 화면](#21-게시판-선택-화면)
2.2. [게시글 리스트 화면](#22-게시글-리스트-화면)
2.3. [검색 화면](#23-검색-화면)
3. [프로젝트 설명](#3-프로젝트-설명)
3.1. [개요](#31-개요)
3.2. [데이터베이스 프레임워크](#32-데이터베이스-프레임워크)
3.3. [도식화된 구조](#33-도식화된-구조)
3.4. [주요 객체 기능](#34-주요-객체-기능)
</br>
## 1. 개발 환경
<img src = "https://img.shields.io/badge/swift-5.9-orange"> <img src = "https://img.shields.io/badge/Xcode-14.0-orange"> <img src = "https://img.shields.io/badge/Minimum%20Deployments-14.0-orange">
### 1.1. 적용 프레임워크
<img src = "https://img.shields.io/badge/Foundation--green"> <img src = "https://img.shields.io/badge/UIKit--green"> <img src = "https://img.shields.io/badge/RxSwift--green"> <img src = "https://img.shields.io/badge/Realm--green">
### 1.2. 아키텍처
<img src = "https://img.shields.io/badge/MVVM--green">
</br></br>
## 2. 구현 기능
앱은 3개 화면으로 구성되어 있습니다.
- 게시판을 선택하는 화면인 BoardList
- 선택된 게시판의 게시글 리스트를 표시하는 화면인 PostList
- 선택된 게시판에서 게시글을 검색하는 화면인 PostSearch
### 2.1. 게시판 선택 화면
| **게시판 데이터 로드** | **modal 방식으로 표시** |
| :---: | :---: |
|<img src="https://hackmd.io/_uploads/r1nzYrafp.png" width=250>|<img src="https://hackmd.io/_uploads/SySicrTGT.png" width=250>|
- 네트워크 통신을 통해 게시판 목록을 불러옵니다. 불러온 게시판 목록은 `UITableView`로 표시됩니다.
- 게시판 선택 화면은 modal 방식으로 표시되며, 하단으로 드래그하여 내릴 수 있습니다.
- 게시판을 탭하여 선택하면, 게시판 선택 화면은 내려가고 게시글 리스트 화면이 나타납니다.
### 2.2. 게시글 리스트 화면
| **게시판을 선택하지 않은 경우** | **게시글 리스트 로드 후** | **페이징** |
| :---: | :---: | :---: |
| <img src="https://hackmd.io/_uploads/Sy2JgLpzT.png" width=250> | <img src="https://hackmd.io/_uploads/HJCsX8TGT.png" width=250> | <img src="https://hackmd.io/_uploads/B1Z0mLaMT.gif" width=250> |
- 게시판 선택 화면에서 게시판을 선택하지 않고 화면을 내린 경우, 선택된 게시판이 없으므로 게시글 목록은 로드되지 않습니다. 게시판 이름도 표시되지 않습니다.
- 게시판이 선택되면 해당 게시판의 게시글 리스트를 로드합니다.
- 게시글 리스트는 30개 단위로 페이징됩니다. 로드된 데이터 셀을 최하단까지 스크롤하면 30개씩 추가로 로드됩니다. 더이상 로드할 데이터가 없으면 데이터를 요청하지 않습니다.
- 좌측 상단의 리스트 버튼을 클릭하면 게시판 선택 화면이 나타납니다.
- 우측 상단의 돋보기 버튼을 클릭하면 검색 화면이 나타납니다.
### 2.3. 검색 화면
| **ㅇㅇ** | **ㅇㅇ** | **ㅇㅇ** | **ㅇㅇ** |
| :---: | :---: | :---: | :---: |
| <img src="https://hackmd.io/_uploads/r1bSIlAGp.gif" width=250> | <img src="https://hackmd.io/_uploads/SymuOyCGa.gif" width=250> | <img src="https://hackmd.io/_uploads/r168YJ0MT.gif" width=250> | <img src="https://hackmd.io/_uploads/HJfoWxCfp.gif" width=250> |
- 게시판이 선택되지 않은 상태에서 검색을 시도하면 `"검색할 게시판을 먼저 선택해주세요."` Alert이 표시됩니다.
- 검색 기록은 앱 실행 시 LocalDB에서 불러옵니다. 이후 변경사항이 LocalDB에 반영됩니다.
- 검색 기록은 시간 순으로 정렬됩니다. 각 셀의 삭제 버튼을 누르면 검색 기록이 삭제됩니다. 표시할 검색 기록이 없으면 대체 뷰가 표시됩니다.
- 검색어가 입력되면 검색 필드에 플레이스홀더 대신 입력 중인 검색어가 표시됩니다. `전체`, `제목`, `내용`, `작성자` 중 검색할 영역을 선택하면 검색을 실행하고 데이터를 로드합니다.
- 검색된 데이터 중 검색어가 일치하는 텍스트는 오렌지 색상으로 강조됩니다. 대소문자를 구분하지 않고 강조됩니다.
- 검색 기록을 클릭하면 현재 게시판에서 해당 검색을 실행합니다. 동일한 검색 내용으로 검색 기록이 추가되진 않지만, 검색 시간이 업데이트되어 목록의 최상단으로 이동합니다.
</br></br>
## 3. 프로젝트 설명
### 3.1. 개요
MVVM 패턴을 적용하였고, RxSwift을 사용하여 구현하였습니다.
| UI | 아키텍처 | 반응형 프레임워크 |
| :---: | :---: | :---: |
| UIKit | MVVM | RxSwift |
다음과 같이 각 뷰는 뷰모델을 통해 DataManager 상호작용을 하는 방향으로 구현하였습니다.
앱 전역에서 관리되는 데이터는 뷰모델이 직접 관리하지 않고 DataManager를 통해 관리하였으며, 특정 화면에 일시적으로 종속되는 데이터는 뷰모델이 직접 관리하도록 구현하였습니다.
<img src="https://hackmd.io/_uploads/BkupBBazp." width=700>
### 3.2. 데이터베이스 프레임워크
앱 실행 중 게시글 검색 기록 데이터는 LocalDB에 저장됩니다.
LocalDB는 애플에서 제공하는 프레임워크인 `CoreData`와 외부 라이브러리 중 고민하였으며, 비교적 간단히 사용할 수 있는 외부 라이브러리인 `Realm`을 선택하여 사용하였습니다.
`CoreData`는 기본 타입의 자료형 외에 사용자 정의 타입을 저장하는 경우 추가 작업이 필요하기 때문입니다.
</br>
### 3.3. 도식화된 구조
주요 기능을 하는 타입들만 도식화하였습니다.
</br>

</br>
### 3.4. 주요 객체 기능
### 3.4.1. 공통
- `EmptyImagePresentableTableView`: `UITableView`를 상속받는 커스텀 TableView입니다. 표시할 셀 데이터가 없을 경우, 각 화면에 따라 다음의 이미지와 텍스트를 표시합니다.
<img src="https://hackmd.io/_uploads/B1eVqzCGa.png" height=150> <img src="https://hackmd.io/_uploads/HJiHqGAfa.png" height=150> <img src="https://hackmd.io/_uploads/r1S8cfAfT.png" height=150>
</br>
- `DataManager`: 앱 전체에 사용되는 모델 데이터를 관리합니다. 앱 전체에 유일한 인스턴스를 보장하기 위해 싱글톤 패턴을 적용하였습니다. 모델 데이터를 각각의 뷰모델에 observable하게 넘겨주기 위해 다음과 같은 프로퍼티를 소유합니다.
``` swift
final class DataManager {
static let shared = DataManager()
private let realmManager = RealmManager()
private let boardRelay = BehaviorRelay<Board?>(value: nil)
private let postRelay = BehaviorRelay<Post?>(value: nil)
private let searchHistoryRelay = BehaviorRelay<[SearchHistory]>(value: [])
private init() {
initiateDatabase()
}
...
}
```
</br>
### 3.4.2. 게시판 선택 화면
- `Board`: 게시판 데이터를 저장하는 모델입니다. 로드된 데이터는 `DataManager`에 반영됩니다.
</br>
- `BoardListViewController`: 게시판 데이터가 로드될 경우 tableView의 cell에 표시합니다. 게시판이 선택되면 게시글 리스트 화면에서 게시글 리스트를 요청하게 하기 위해 delegate 패턴을 적용하였습니다.
``` swift
final class BoardListViewController: UIViewController {
...
var delegate: PostRequestable?
...
}
protocol PostRequestable {
func requestPost(at indexPath: IndexPath)
}
final class PostListViewController: PostRequestable { ... }
```
</br>
- `BoardListViewModel`: 게시판 데이터를 요청합니다. 로드된 게시판 데이터를 `DataManager`에 반영하고, observable한 데이터를 `BoardListViewController`에 넘겨줍니다.
</br>
### 3.4.3. 게시글 리스트 화면
- `Post`: 게시글 데이터를 저장하는 모델입니다. 로드된 데이터는 `DataManager`에 반영됩니다.
</br>
- `PostListViewController`: 게시글 데이터가 로드될 경우 tableView의 cell에 표시합니다.
</br>
- `PostListViewModel`: 게시글 데이터를 요청합니다. 로드된 게시글 데이터를 `DataManager`에 반영하고, observable한 데이터를 `PostListViewController`에 넘겨줍니다.
tableView의 페이징을 구현하는 기능을 수행하며, 다음과 같은 프로퍼티를 소유합니다.
``` swift
final class PostListViewModel {
...
private let paginationUnit: Int = 30 // 페이징 단위
private var offset: Int = 0 // 데이터 로드 시 변경되는 오프셋
private(set) var postCount: Int? // 데이터 로드 시 변경되는 현재 포스트 개수
private(set) var noMoreData = false // 더이상 로드 할 데이터가 없을 경우 데이터 요청을 막기 위한 플래그
...
}
```
</br>
### 3.4.4. 검색 화면
- `Post`: 검색 결과 게시글 데이터를 나타내기 위한 모델입니다. 이 데이터는 일시적이며, 다른 화면과 공유되지 않고, 현재 화면을 종료하면 메모리에서 삭제될 데이터입니다. 따라서 로드된 데이터는 `DataManager`에 반영되지 않고 `PostSearchViewModel`이 관리합니다.
</br>
- `SearchHistory`: 검색 기록을 저장할 데이터입니다. 이 데이터가 추가/변경/삭제될 때 `DataManager`와 LocalDB를 관리하는 `RealmManager`에 반영됩니다.
</br>
- `SearchInfomation`: 현재 입력중인 검색 키워드를 저장하고, 검색할 영역에 따라 현재 앱에서는 4종의 enum case에 따라 4개의 데이터가 생성되어 배열로 관리됩니다.
<img src="https://hackmd.io/_uploads/B1ixfXRG6.png" width=250>
``` swift
struct SearchInfomation {
let category: SearchHistory.Category
var keyword: String
}
struct SearchHistory: DataTransferObject {
enum Category: String, CaseIterable, PersistableEnum {
case all
case title
case contents
case writer
...
}
...
}
```
</br>
- `PostSearchViewController`: 현재 사용자의 입력 상황에 따라, 동일한 tableView에 세 개 타입의 cell에 표시합니다.
</br>
- `PostSearchViewModel`: cell을 그리는 데 필요한 데이터를 관리합니다.
현재 `CellMode`에 따라 어떤 셀을 그려야할지 결정하고, observable한 데이터를 `PostSearchViewController`에 넘겨줍니다.
사용자의 검색 요청에 따라 검색 결과 데이터를 요청합니다. 로드된 검색 결과 데이터는 현재 화면에 종속된 임시 데이터이므로 DataManager에 반영하지 않습니다.
</br>
- `RealmManager`: LocalDB를 관리하기 위한 객체입니다. CRUD 기능을 수행합니다.
</br>
- `CellMode`: 현재 tableView에 어떤 cell을 표시할 지 결정하기 위한 모델입니다.
``` swift
enum CellMode {
case searchHistory
case searching
case searchResult
}
```
</br>
- `SearchHistoryTableViewCell`: 검색 기록을 표시하기 위한 cell입니다.
<img src="https://hackmd.io/_uploads/r1r3HmCz6.png" width=250>
</br>
- `SearchInformationTableViewCell`: 현재 입력중인 검색어를 각각 `전체`, `제목`, `내용`, `작성자` 데이터에 반영하여 표시합니다.
<img src="https://hackmd.io/_uploads/B1ixfXRG6.png" width=250>
</br>
- `SearchResultTableViewCell`: 검색 결과를 표시하기 위한 cell입니다. 게시글 리스트를 표시하는 cell과 UI가 유사하지만, 검색어 하이라이팅 기능을 추가하기 위해 `PostListTableViewCell`를 상속받는 타입입니다.
<img src="https://hackmd.io/_uploads/ByLlwQAfa.png" width=250>
``` swift
final class SearchResultTableViewCell: PostListTableViewCell {
func configure( ... ) { ... }
}
```