Search

Clean Swift 적용해보기(2) - 상세 화면 만들어보기

하단 북마크 The Clean Swift Handbook에 나오는 Example 코드를 참고하여 Image Collection Example를 만들었습니다.
Clean Swift 개념은 The Clean Swift 에 작성해두었습니다.

 샘플 프로젝트 만들기 시작

이전에 MVP, MVVM 패턴으로 만들어 두었던 Image Example 프로젝트를 Clean Swift를 적용한 방식으로 수정해보려고 합니다.
기존 프로젝트에는 이미지를 가져와서 보여주는 메인 화면만 존재합니다. 이번에 Clean Swift를 적용하면서 상세 화면과 인덱스 변경 화면을 추가로 만들 예정입니다.
메인 화면에 이어 이번에는 상세 화면을 만들어 보려 합니다.
*메인 화면 구현은 Clean Swift 적용해보기(1) - 메인 화면 만들어보기 에서 보실 수 있습니다.
상세 화면에서 할 수 있는 작업들은 아래와 같습니다.
이전 화면에서 선택한 사진을 노출합니다.
상단 Edit 버튼을 누르면 Index를 변경할 수 있는 반모달이 노출됩니다.
반모달 화면에서 Index를 변경하고 저장 버튼을 누르면 Index가 저장됩니다.
이전 화면으로 돌아가면 변경된 Index로 사진 순서가 변경됩니다.
위의 모든 작업들을 시작하기 전에 메인 화면에서 상세 화면으로 넘어가는 기능부터 구현해보도록 하겠습니다.
*화면 UI 로직에 대한 설명은 따로 넣어두지 않았습니다. 화면 구현 방식이 궁금하시다면 레포지토리에서 코드를 참고해주세요.

 메인 화면 연결하기

이전에 만든 메인 화면은 아래와 같은 화면입니다.
해당 화면에서 위에 노출된 이미지를 누르면 상세 화면이 노출되도록 코드를 작성해보려고 합니다.
ViewController에서 화면 이동을 구현할 수도 있겠지만 Router를 사용해서 화면 이동을 하도록 코드를 작성해보겠습니다.

⓵ Router 연결

Router는 RoutingLogic protocol를 conform하고 있습니다. 해당 protocol에 routeToImageDetail 메서드를 추가합니다.
protocol ImageCollectionRoutingLogic { func routeToImageDetail() }
Swift
복사
routeToImageDetail 메서드 내에 상세 화면으로 이동하는 코드를 추가합니다.
네비게이션 push 방식을 통해서 상세 화면으로 넘어갈 겁니다.
final class ImageCollectionRouter: ImageCollectionRoutingLogic, ImageCollectionDataPassing { weak var viewController: ImageCollectionViewController? // MARK: - route - func public func routeToImageDetail() { let destinationVC = ImageDetailViewController() navigateToImageDetail(source: viewController!, destination: destinationVC) } // MARK: - navigate - func private func navigateToImageDetail(source: ImageCollectionViewController, destination: ImageDetailViewController) { source.navigationController?.pushViewController(destination, animated: true) } }
Swift
복사
Router를 사용해서 화면 이동을 하는 코드를 완성했다면, 이번엔 셀을 눌러서 다음 화면으로 이동하는 트리거를 구현해봅시다.
셀을 눌러서UICollectionViewDelegate 내에 있는 didSelectItemAt 메서드를 사용해봅시다.
extension ImageCollectionView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { print("Selected item at index: \(indexPath.item)") } }
Swift
복사
이전에 만들어둔 ImageCollection enum 타입 내부에 PhotoSelection 이라는 타입을 하나 생성해줍니다.
해당 타입 내에 Request, Response, ViewModel 구조체 타입을 생성합니다.
enum PhotoSelection { struct Request { let row: Int } struct Response { } struct ViewModel { } }
Swift
복사
선택한 row index를 받아올 수 있는 row 프로퍼티를 Request 구조체 내부에 만들어 줍니다.
BusinessLogic 프로퍼티 내부에 didSelectPhoto 메서드를 생성해주고,
protocol ImageCollectionBusinessLogic { func start() func changeCount(request: ImageCollection.PhotoCollectionCount.Request) func fetchPhotoCollection(request: ImageCollection.PhotoCollection.Request) func changeToPage(request: ImageCollection.PhotoCollectionPage.Request) func didSelectPhoto(request: ImageCollection.PhotoSection.Request) // 새로 추가 }
Swift
복사
ViewController에서 해당 메서드를 호출하는 코드를 추가해줍니다.
func didSelectCell(with rowIndex: Int) { let request = ImageCollection.PhotoSection.Request(row: rowIndex) interactor?.didSelectPhoto(request: request) }
Swift
복사
interactor 내부에 있는 didSelectPhoto 메서드를 구현해봅시다.
didSelectPhoto 내부엔 아래 주석에 써둔 기능이 있어야 합니다.
public func didSelectPhoto(request: ImageCollection.PhotoSection.Request) { /// ImageURLs에 값이 있다면, 내가 선택 row의 index로 imageURL를 설정해준다. /// presenter 호출 }
Swift
복사
ImageURLs에 있는 값 중 Row에 해당 하는 URL를 가지고 오려면, API 통신을 통해서 가져온 URL값을 저장해 둘 공간이 필요합니다.
그 공간을 imageURLs라는 이름으로 만들었습니다.
var imageURLs: [String] = []
Swift
복사
이전에 CollectionView에 이미지를 올리는 기능을 구현할 때 imageURLs를 가져오는 부분이 있었는데, 해당 부분에서 imageURLs 값을 설정할 수 있도록 코드를 수정하겠습니다.
public func fetchPhotoCollection(request: ImageCollection.PhotoCollection.Request) { Task { [weak self] in guard let self else { return } let urls = await self.photosWorker.fetchRandomPhotoURLs(count: self.count) let response = ImageCollection.PhotoCollection.Response(photoURLs: urls) self.imageURLs = urls // 추가 self.presenter?.presentPhotoCollection(response: response) } }
Swift
복사
API에서 값을 제대로 가져왔다면 imageURLs에 값이 들어올 겁니다.
아니라면 imageURLs에 값이 없겠죠. 그런 경우에는 더이상 진행되지 않도록 guard문을 추가해줍니다.
public func didSelectPhoto(request: ImageCollection.PhotoSection.Request) { guard imageURLs.isNotEmpty(), imageURLs.count > request.row else { return } }
Swift
복사
imageURLs에 값이 있고, 들어온 Row보다 배열의 count가 크면 원하는 imageURL를 찾을 수 있는 상태이기 때문에 그 다음 코드를 실행할 수 있습니다.
하지만, 반대의 경우는 더이상 진행하면 안됩니다. outOfRange 에러가 발생할 수 있기 때문이죠.
imageURL를 찾을 수 있는 상태라면 imageURL를 어딘가에 저장해두어야 합니다.
이전에 만들어뒀던 DataStore에 imageURL 프로퍼티를 만들고 값을 그 곳에 저장하려 합니다.
protocol ImageCollectionDataStore { var count: Int { get set } var imageURL: String? { get set } }
Swift
복사
imageURL에 원하는 URL를 저장하고 presenter로 Response를 전달합니다.
화면에 present해야 하는 값이 없기 때문에 Response에 아무런 값을 담지 않아도 됩니다.
public func didSelectPhoto(request: ImageCollection.PhotoSection.Request) { guard imageURLs.isNotEmpty(), imageURLs.count > request.row else { return } imageURL = imageURLs[request.row] let response = ImageCollection.PhotoSection.Response() presenter?.presentSelectedPhoto(response: response) }
Swift
복사
presenter도 viewController로 아무런 값을 보내지 않습니다.
대신 ViewController에서는 메서드가 호출이 되었을 때, router를 통해서 화면을 띄워주는 로직을 구현합니다.
public func displaySelectedPhoto(viewModel: ImageCollection.PhotoSection.ViewModel) { router?.routeToImageDetail() }
Swift
복사
이렇게 연결을 마무리하고 나서 프로젝트를 빌드해보면,
상세 화면으로 이동하는 모습을 볼 수 있습니다.
하지만, 상세 화면으로 ImageURL를 보내주는 코드가 없기 때문에 상세 화면에 아무런 이미지가 뜨지 않는 것을 볼 수 있습니다.
상세 화면으로 ImageURL 값을 전달해줍시다!!!!

⓶ Image Data Passing

이전에 ImageCollectionDataStoreimageURL를 추가했습니다.
protocol ImageCollectionDataStore { var count: Int { get set } var imageURL: String? { get set } }
Swift
복사
상세 화면에도 imageURL를 받을 수 있는 영역을 만들어 줍시다.
protocol ImageDetailDataStore { var imageURL: String? { get set } }
Swift
복사
그리고 이전에 만들어둔 Router 메서드에 데이터를 전달하는 메서드를 추가해줍니다.
public func routeToImageDetail() { let destinationVC = ImageDetailViewController() var destinationDS = destinationVC.router!.dataStore! passDataToImageDetail(source: dataStore!, destination: &destinationDS) navigateToImageDetail(source: viewController!, destination: destinationVC) } private func passDataToImageDetail(source: ImageCollectionDataStore, destination: inout ImageDetailDataStore) { destination.imageURL = source.imageURL }
Swift
복사
이젠 화면 이동 시에 imageURL 데이터가 상세 화면으로 넘어갈 겁니다.
하지만, 해당 값을 보기 위해선 상세 화면이 노출될 때 ImageView에 Image를 설정해주는 코드가 필요합니다.
이전에 메인 화면에서 Count Label의 Text 값을 0으로 초기 설정할 때, 우리는 viewDidLoad에서 interactor로 start 트리거를 보내는 방식으로 구현했었습니다.
동일한 방식으로 구현해볼게요.
상세 화면의 viewDidLoad에서 start 메서드를 호출합니다. 해당 메서드 내부에는 interactor의 fetchImage 메서드를 호출하는 코드가 들어 있습니다.
override func viewDidLoad() { super.viewDidLoad() start() } // MARK: - func private func start() { let request = ImageDetail.Image.Request() interactor?.fetchImage(request: request) }
Swift
복사
fetchImage 내부에선 ImageDetailDataStore에 있는 ImageURL 값을 presenter로 전달해주는 코드가 들어 있습니다. 해당 코드를 통해서 이전 화면에서 전달했던 ImageURL이 상세 화면에 띄워질 준비를 하게 됩니다.
public func fetchImage(request: ImageDetail.Image.Request) { let response = ImageDetail.Image.Response(imageURL: imageURL) presenter?.presentSomething(response: response) }
Swift
복사
따로, formatting이 필요하지 않기 때문에 그 값 그대로 ViewController로 전달해줍니다.
ViewController에서는 전달받은 imageURL 값을 그대로 사용하지 않습니다.
해당 값이 nil로 올 수도 있기에 해당 값이 nil이라면 이전 화면으로 pop 되도록 합니다. 아니라면 해당 imageURL이 화면에 노출될 수 있도록 합니다.
public func displayImage(viewModel: ImageDetail.Image.ViewModel) { guard let imageURL = viewModel.imageURL else { router?.routeToImageCollection() return } contentView.updateImage(imageURL) }
Swift
복사
routeToImageCollection 코드를 살짝 보자면 말그대로 이전 화면으로 pop 해주는 코드를 담고 있습니다. 아직 전달할 값이 없기 때문에 데이터 패싱 코드는 작성하지 않습니다.
public func routeToImageCollection() { let destinationVC = ImageCollectionViewController() navigateToImageCollection(source: viewController!, destination: destinationVC) } private func navigateToImageCollection(source: ImageDetailViewController, destination: ImageCollectionViewController) { source.navigationController?.popViewController(animated: true) }
Swift
복사
위의 기능 구현을 완료하고 나면 이런 화면을 볼 수 있습니다.

 상세 화면 만들어보기

메인 화면에서 상세 화면을 띄우는 기능을 구현하면서 선택한 사진을 노출시키는 기능은 구현을 완료했습니다.
그럼 그 외의 기능들을 구현해 봅시다.

⓵ Edit 기능 구현

[ 상단 Edit 버튼을 누르면 Index를 변경할 수 있는 반모달이 노출됩니다. ]
상단 Edit 버튼을 누르기 위해선 상단 Edit 버튼부터 구현이 되어야 합니다. 현재 화면엔 없거등요.
Navigation bar에 Edit 버튼을 추가해줍니다.
private func configureUI() { navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .edit, target: self, action: #selector(didTapEdit) ) }
Swift
복사
그리고 상세 화면 Router에서 반모달을 노출시키는 코드를 추가해줍니다.
protocol ImageDetailRoutingLogic { func routeToImageCollection() func routeToSheetView() // 추가 } final class ImageDetailRouter: ImageDetailRoutingLogic, ImageDetailDataPassing { weak var viewController: ImageDetailViewController? var dataStore: ImageDetailDataStore? // MARK: - route - func public func routeToSheetView() { let destinationVC = ImageIndexEditViewController(delegate: self) if let sheet = destinationVC.sheetPresentationController { sheet.detents = [.medium()] sheet.largestUndimmedDetentIdentifier = .medium sheet.prefersScrollingExpandsWhenScrolledToEdge = false sheet.prefersEdgeAttachedInCompactHeight = true sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true } navigationToSheetView(source: viewController!, destination: destinationVC) } private func navigationToSheetView(source: ImageDetailViewController, destination: ImageIndexEditViewController) { source.present(destination, animated: true) } }
Swift
복사
Router 코드를 완성했으니 ViewController에서 호출될 수 있도록 합시다.
@objc private func didTapEdit() { router?.routeToSheetView() }
Swift
복사
Router만 연결하게 되면 이런 화면이 완성됩니다. 아직 데이터가 올라가지 않았죠.
이번엔 데이터가 올라가도록 코드를 수정해봅시다.
반모달에 현재 Index와 전체 Image Index를 올리려고 합니다.
하지만, 현재는 상세 화면에 imageURL만 전달되기 때문에 두 가지 값을 보여줄 수 없는 상황입니다. 따라서, 이전에 작성한 코드를 수정해야 합니다.
ImageDetailDataStore를 수정해봅시다.
imageURL이 아닌 imageURLs와 index를 가져오도록 수정했습니다.
protocol ImageDetailDataStore { var imageURLs: [String] { get set } var index: Int? { get set } }
Swift
복사
메인 화면이 해당 값을 넘겨줄 수 있도록 ImageCollectionDataStore 코드를 변경합시다.
protocol ImageCollectionDataStore { var count: Int { get set } var index: Int? { get set } var imageURLs: [String] { get } }
Swift
복사
그리고 이전에 imageURL를 설정해주던 코드를 삭제하고, index를 설정하도록 코드를 수정합니다.
public func didSelectPhoto(request: ImageCollection.PhotoSelection.Request) { guard imageURLs.isNotEmpty(), imageURLs.count > request.row else { return } index = request.row // 수정 let response = ImageCollection.PhotoSelection.Response() presenter?.presentSelectedPhoto(response: response) }
Swift
복사
Router에서 Data Passing 시에 imageURL를 넘겨주던 부분을 imageURLsindex를 넘겨주도록 코드를 수정합니다.
private func passDataToImageDetail(source: ImageCollectionDataStore, destination: inout ImageDetailDataStore) { destination.index = source.index destination.imageURLs = source.imageURLs }
Swift
복사
위의 코드로 바뀌면서 상세 화면에서 ImageURL 값을 설정해주는 코드가 조금 바뀌어야 하겠지만, 큰 작업 아니니 잘 수정해주십셔.
값을 받아서 사용해야 하는 인덱스 변경 화면에도 DataStore를 생성해줍니다.
protocol ImageIndexEditDataStore { var indexs: [Int] { get set } var currentIndex: Int { get set } }
Swift
복사
그리고 상세 화면 Router에서 인덱스 변경 화면으로 값을 넘겨줍시다. 이전에 만들어둔 Router 메서드에 데이터 패싱 코드만 추가하면 될 거 같아요.
public func routeToSheetView() { let destinationVC = ImageIndexEditViewController(delegate: self) var destinationDS = destinationVC.router!.dataStore! if let sheet = destinationVC.sheetPresentationController { sheet.detents = [.medium()] sheet.largestUndimmedDetentIdentifier = .medium sheet.prefersScrollingExpandsWhenScrolledToEdge = false sheet.prefersEdgeAttachedInCompactHeight = true sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true } passDataToSheetView(source: dataStore!, destination: &destinationDS) // 추가 navigationToSheetView(source: viewController!, destination: destinationVC) } private func navigationToSheetView(source: ImageDetailViewController, destination: ImageIndexEditViewController) { source.present(destination, animated: true) } private func passDataToSheetView(source: ImageDetailDataStore, destination: inout ImageIndexEditDataStore) { destination.indexs = Array(source.imageURLs.indices) destination.currentIndex = source.index ?? 0 }
Swift
복사
하지만, 데이터가 전달되었다고 끝난 것이 아니기에 화면에 얹어주는 코드가 필요합니다.
이 부분은 이전 화면들과 동일하게 viewDidLoad에서 start 메서드를 호출하는 방식으로 구현해볼게요.
override func viewDidLoad() { super.viewDidLoad() start() } private func start() { let request = ImageIndexEdit.Indexs.Request() interactor?.start(request: request) }
Swift
복사
interactor에서는 DataStore에 담긴 값을 가져와서 Presenter로 넘겨줍니다.
public func start(request: ImageIndexEdit.Indexs.Request) { let response = ImageIndexEdit.Indexs.Response(indexs: indexs, currentIndex: currentIndex) presenter?.presentIndexs(response: response) }
Swift
복사
Presenter에서는 indexs 값을 String 타입으로 변환해주는 작업을 한 후에 ViewController로 전달해줍니다.
public func presentIndexs(response: ImageIndexEdit.Indexs.Response) { let viewModel = ImageIndexEdit.Indexs.ViewModel( indexs: response.indexs.compactMap { String($0) }, currentIndex: response.currentIndex ) viewController?.displayImageIndex(viewModel: viewModel) }
Swift
복사
ViewController에서 해당 값을 가지고 화면 세팅을 완료하면,
public func displayImageIndex(viewModel: ImageIndexEdit.Indexs.ViewModel) { contentView.setCounts(viewModel.indexs) contentView.setCurrentIndex(viewModel.currentIndex) }
Swift
복사
아래와 같이 이미지 배열 Element 수 만큼 Picker Title이 설정되고 선택한 셀의 Index로 현재 Picker Index가 설정됩니다.

⓶ 저장 기능 구현

[ 반모달 화면에서 Index를 변경하고 저장 버튼을 누르면 Index가 저장됩니다. ]
저장 버튼을 누르면 Index가 저장됩니다. 해당 동작 시, 실행되는 메서드를 ViewController에 만들어 봅시다.
해당 메서드는 interactor save 메서드로 선택된 index를 담고 있는 Request 모델을 전달합니다.
func save(index: Int?) { let request = ImageIndexEdit.SaveIndex.Request(index: index) interactor?.save(request: request) }
Swift
복사
save 는 선택한 Index를 담아서 Presenter로 보내고, Presenter는 ViewController로 보내줍니다.
선택한 index 값은 DataStore에 있는 currentIndex에 저장해줍니다.
public func save(request: ImageIndexEdit.SaveIndex.Request) { let response = ImageIndexEdit.SaveIndex.Response(index: request.index) currentIndex = request.index ?? currentIndex presenter?.presentSaveState(response: response) }
Swift
복사
선택한 Index는 화면에 띄워줄 필요가 없으니 Model로 전달을 안해도 되지 않나요?
사실 그렇긴 합니다.
선택한 데이터를 저장하기 위해서 Interactor로 전달할 필요는 있지만, Response, ViewModel 값엔 담을 필요가 없습니다. 화면에 띄울 필요가 없기 때문이죠.
하지만, 전달을 했던 이유는 ViewController에서 해당 값이 nil인 경우 알럿창을 띄워주기 위해서 입니다.
전달된 값이 nil이라면 저장하면 안되기 때문이죠.
지금 생각해보니 애초에 값을 넘겨줄 때, 확인해도 되지 않았나 싶긴한데
아무튼 잘못된 값이 이전 화면으로 넘어가지 않도록 하기 위한 조치였습니다.
ViewModel 형식으로 값이 전달되고 해당 값이 nil이 아니라면 routeToImageDetail 메서드를 호출합니다.
public func displaySaveIndex(viewModel: ImageIndexEdit.SaveIndex.ViewModel) { guard let index = viewModel.index else { showAlert(title: "에러 발생", message: "잘못된 값이 전달되었습니다.") return } router?.routeToImageDetail() }
Swift
복사
routeToImageDetail에서 반모달을 내리고 이전 상세 화면으로 데이터를 전달해주도록 코드를 작성합니다.
public func routeToImageDetail() { let destinationVC = ImageDetailViewController() var destinationDS = destinationVC.router!.dataStore! passDataToImageDetail(source: dataStore!, destination: &destinationDS) navigateToImageDetail(source: viewController!, destination: destinationVC) } private func navigateToImageDetail(source: ImageIndexEditViewController, destination: ImageDetailViewController) { source.dismiss(animated: true) } private func passDataToImageDetail(source: ImageIndexEditDataStore, destination: inout ImageDetailDataStore) { destination.index = source.currentIndex }
Swift
복사
상세 화면에서도 화면 이동 시에 변경된 Index 값을 메인 화면으로 넘겨줄 수 있도록 코드를 작성해줍니다.
public func routeToImageCollection() { let destinationVC = ImageCollectionViewController() var destinationDS = destinationVC.router!.dataStore! passDataToImageCollection(source: dataStore!, destination: &destinationDS) navigateToImageCollection(source: viewController!, destination: destinationVC) } private func navigateToImageCollection(source: ImageDetailViewController, destination: ImageCollectionViewController) { source.navigationController?.popViewController(animated: true) } private func passDataToImageCollection(source: ImageDetailDataStore, destination: inout ImageCollectionDataStore) { destination.changedIndex = source.index }
Swift
복사
변경된 Index 값은 changedIndex 라는 프로퍼티를 만들어서 해당 프로퍼티에 저장하도록 구현했습니다.
해당 메서드는 상세 화면의 ViewDidDisappear에서 호출했습니다.
private func isMovingFromParent() { if isMovingFromParent { router?.routeToImageCollection() } }
Swift
복사
마지막으로 메인 화면에 다시 진입했을 때, 변경된 Index 순서대로 CollectionView가 노출되도록 코드를 수정해보겠습니다.
화면에 다시 진입했을 때ViewWillAppearupdateImages라는 메서드를 추가했습니다.
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateImages() } private func updateImages() { let request = ImageCollection.PhotoCollection.Request() interactor?.changeIndexs(request: request) }
Swift
복사
updateImages는 interactor에 있는 changeIndexs 메서드를 호출합니다.
해당 메서드는 선택한 셀 인덱스(index), 변경된 셀 인덱스(changedIndex) 값이 nil이 아닌지 확인하고, imageURLs 배열이 빈 값은 아닌지 먼저 확인합니다. 잘못된 값이 나올 수 있으니깐요.
public func changeIndexs(request: ImageCollection.PhotoCollection.Request) { guard let index, let changedIndex, imageURLs.isNotEmpty() else { return } }
Swift
복사
또한, 이전 값과 변경된 값이 다른지 확인합니다. 같다면 굳이 CollectionView를 Reload할 필요가 없으니깐요.
public func changeIndexs(request: ImageCollection.PhotoCollection.Request) { guard let index, let changedIndex, imageURLs.isNotEmpty() else { return } guard index != changedIndex else { return } }
Swift
복사
위의 조건을 모두 충족한다면 현재 Index에 있는 imageURL를 제거하고, 변경된 Index에 넣어줍니다.
순서가 변경된 Array 데이터는 이전에 PhotoCollection 설정 시 사용했던 presenter 메서드로 보내줍니다.
public func changeIndexs(request: ImageCollection.PhotoCollection.Request) { guard let index, let changedIndex, imageURLs.isNotEmpty() else { return } guard index != changedIndex else { return } let element = imageURLs.remove(at: index) imageURLs.insert(element, at: changedIndex) let response = ImageCollection.PhotoCollection.Response(photoURLs: imageURLs) presenter?.presentPhotoCollection(response: response) }
Swift
복사
데이터가 ViewController에 잘 도착했다면, 변경된 순서로 화면에 노출될 겁니다.
확인해볼까요?
변경이 안됩니다. 수정이 필요하겠네요.

 Next Step

마지막으로 이전 화면으로 값을 제대로 전달할 수 있게 수정해보려고 합니다.
왜 문제가 발생했고, 어떤 방식으로 수정을 진행했는지 해당 게시물에서 다룰 거 같습니다.