From a7e8c741942fd686c365425b1e8ca3e537026efc Mon Sep 17 00:00:00 2001
From: youz2me <kynhun20@gachon.ac.kr>
Date: Fri, 19 Jul 2024 11:56:46 +0900
Subject: [PATCH 1/2] =?UTF-8?q?feat/#240=20=EC=A7=80=EA=B0=81=20=EA=BE=B8?=
 =?UTF-8?q?=EB=AC=BC=EC=9D=B4=20API=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../PagePromiseViewController.swift           |  2 +-
 .../Tardy/Cell/TardyCollectionViewCell.swift  |  4 +-
 .../Promise/Tardy/View/TardyEmptyView.swift   |  2 +
 .../Promise/Tardy/View/TardyPenaltyView.swift |  2 +-
 .../Source/Promise/Tardy/View/TardyView.swift |  6 +-
 .../ViewController/TardyViewController.swift  | 63 +++++++++++++------
 .../Tardy/ViewModel/TardyViewModel.swift      | 58 +++++++++++++++--
 7 files changed, 105 insertions(+), 32 deletions(-)

diff --git a/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift b/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift
index 28647264..1ca9c592 100644
--- a/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift
+++ b/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift
@@ -63,7 +63,7 @@ class PagePromiseViewController: BaseViewController {
         
         tardyViewController = TardyViewController(
             tardyViewModel: TardyViewModel(
-                tardyService: MockTardyService(),
+                tardyService: PromiseService(),
                 promiseID: promiseViewModel.promiseID
             )
         )
diff --git a/KkuMulKum/Source/Promise/Tardy/Cell/TardyCollectionViewCell.swift b/KkuMulKum/Source/Promise/Tardy/Cell/TardyCollectionViewCell.swift
index ebc2d60e..35642737 100644
--- a/KkuMulKum/Source/Promise/Tardy/Cell/TardyCollectionViewCell.swift
+++ b/KkuMulKum/Source/Promise/Tardy/Cell/TardyCollectionViewCell.swift
@@ -12,10 +12,10 @@ class TardyCollectionViewCell: BaseCollectionViewCell {
     
     // MARK: Property
 
-    private let profileImageView: UIImageView = UIImageView().then {
+    let profileImageView: UIImageView = UIImageView().then {
         $0.image = .imgProfile
         $0.contentMode = .scaleAspectFill
-        $0.layer.cornerRadius = 67 / 2
+        $0.layer.cornerRadius = Screen.height(67) / 2
         $0.clipsToBounds = true
     }
     
diff --git a/KkuMulKum/Source/Promise/Tardy/View/TardyEmptyView.swift b/KkuMulKum/Source/Promise/Tardy/View/TardyEmptyView.swift
index 84fba014..80f84356 100644
--- a/KkuMulKum/Source/Promise/Tardy/View/TardyEmptyView.swift
+++ b/KkuMulKum/Source/Promise/Tardy/View/TardyEmptyView.swift
@@ -19,6 +19,8 @@ class TardyEmptyView: BaseView {
     
     private let emptyContentLabel: UILabel = UILabel().then {
         $0.setText("꾸물이들이 도착하길\n기다리는 중이에요", style: .body05, color: .gray3)
+    }.then {
+        $0.textAlignment = .center
     }
     
     
diff --git a/KkuMulKum/Source/Promise/Tardy/View/TardyPenaltyView.swift b/KkuMulKum/Source/Promise/Tardy/View/TardyPenaltyView.swift
index 9d495b10..1eabdf48 100644
--- a/KkuMulKum/Source/Promise/Tardy/View/TardyPenaltyView.swift
+++ b/KkuMulKum/Source/Promise/Tardy/View/TardyPenaltyView.swift
@@ -21,7 +21,7 @@ class TardyPenaltyView: BaseView {
         $0.setText("벌칙", style: .caption02, color: .gray8)
     }
     
-    private let contentLabel: UILabel = UILabel().then {
+    let contentLabel: UILabel = UILabel().then {
         $0.setText("탕후루 릴스 찍기", style: .body03, color: .gray8)
     }
     
diff --git a/KkuMulKum/Source/Promise/Tardy/View/TardyView.swift b/KkuMulKum/Source/Promise/Tardy/View/TardyView.swift
index a788b7bb..eb2f9210 100644
--- a/KkuMulKum/Source/Promise/Tardy/View/TardyView.swift
+++ b/KkuMulKum/Source/Promise/Tardy/View/TardyView.swift
@@ -12,7 +12,7 @@ class TardyView: BaseView {
     
     // MARK: Property
 
-    private let tardyPenaltyView: TardyPenaltyView = TardyPenaltyView().then {
+    let tardyPenaltyView: TardyPenaltyView = TardyPenaltyView().then {
         $0.layer.cornerRadius = 8
     }
     
@@ -20,7 +20,7 @@ class TardyView: BaseView {
         $0.setText("이번 약속의 지각 꾸물이는?", style: .head01, color: .gray8)
     }
     
-    private let tardyEmptyView: TardyEmptyView = TardyEmptyView()
+    let tardyEmptyView: TardyEmptyView = TardyEmptyView()
     
     let tardyCollectionView: UICollectionView = UICollectionView(
         frame: .zero,
@@ -74,7 +74,7 @@ class TardyView: BaseView {
         tardyEmptyView.snp.makeConstraints {
             $0.top.equalTo(titleLabel.snp.bottom).offset(108)
             $0.centerX.equalToSuperview()
-            $0.height.equalTo(Screen.height(192))
+            $0.height.equalTo(Screen.height(210))
             $0.width.equalTo(Screen.width(112))
         }
         
diff --git a/KkuMulKum/Source/Promise/Tardy/ViewController/TardyViewController.swift b/KkuMulKum/Source/Promise/Tardy/ViewController/TardyViewController.swift
index 6ca53d45..41c62287 100644
--- a/KkuMulKum/Source/Promise/Tardy/ViewController/TardyViewController.swift
+++ b/KkuMulKum/Source/Promise/Tardy/ViewController/TardyViewController.swift
@@ -33,14 +33,14 @@ class TardyViewController: BaseViewController {
     // MARK: - Setup
 
     override func loadView() {
-        let state = !tardyViewModel.hasTardy.value && tardyViewModel.isPastDue.value
-        view = state ? tardyView : arriveView
+        view = tardyViewModel.isPastDue.value ? arriveView : tardyView
     }
     
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         
-        // TODO: 서버 통신하고 데이터 바인딩
+        tardyViewModel.fetchTardyInfo()
+        tardyViewModel.updatePromiseCompletion()
     }
     
     override func viewDidLoad() {
@@ -50,7 +50,6 @@ class TardyViewController: BaseViewController {
     }
     
     override func setupDelegate() {
-        tardyView.tardyCollectionView.delegate = self
         tardyView.tardyCollectionView.dataSource = self
     }
 }
@@ -61,25 +60,40 @@ class TardyViewController: BaseViewController {
 private extension TardyViewController {
     func setupBinding() {
         /// 시간이 지나고 지각자가 없을 때 arriveView로 띄워짐
-        tardyViewModel.hasTardy.bind(with: self) { owner, flag in
-            let state = !flag && owner.tardyViewModel.isPastDue.value
-            owner.view = state ? owner.tardyView : owner.arriveView
+        tardyViewModel.isPastDue.bind(with: self) { owner, isPastDue in
+            DispatchQueue.main.async {
+                owner.tardyView.tardyCollectionView.isHidden = !isPastDue
+                owner.tardyView.tardyEmptyView.isHidden = isPastDue
+                owner.tardyView.finishMeetingButton.isEnabled = isPastDue
+            }
         }
         
-        /// isFinishButtonEnabled에 따라서 버튼 활성화 상태 변경
-        tardyViewModel.isFinishButtonEnabled.bind(with: self) { owner, flag in
-            self.tardyView.finishMeetingButton.isEnabled = flag
+        tardyViewModel.penalty.bind(with: self) {
+            owner,
+            penalty in
+            DispatchQueue.main.async {
+                owner.tardyView.tardyPenaltyView.contentLabel.setText(
+                    penalty,
+                    style: .body03,
+                    color: .gray8
+                )
+            }
+        }
+        
+        tardyViewModel.hasTardy.bind(with: self) { owner, hasTardy in
+            DispatchQueue.main.async {
+                owner.view = hasTardy && owner.tardyViewModel.isPastDue.value ? owner.arriveView : owner.tardyView
+            }
+        }
+        
+        tardyViewModel.comers.bind(with: self) { owner, comers in
+            DispatchQueue.main.async {
+                owner.tardyView.tardyCollectionView.reloadData()
+            }
         }
     }
 }
 
-// MARK: UICollectionViewDelegate
-
-extension TardyViewController: UICollectionViewDelegate {
-    
-}
-
-
 // MARK: UICollectionViewDataSource
 
 extension TardyViewController: UICollectionViewDataSource {
@@ -87,8 +101,7 @@ extension TardyViewController: UICollectionViewDataSource {
         _ collectionView: UICollectionView,
         numberOfItemsInSection section: Int
     ) -> Int {
-        // TODO: 데이터 바인딩
-        return 10
+        return tardyViewModel.comers.value?.count ?? 0
     }
     
     func collectionView(
@@ -100,6 +113,18 @@ extension TardyViewController: UICollectionViewDataSource {
             for: indexPath
         ) as? TardyCollectionViewCell else { return UICollectionViewCell() }
         
+        guard let data = tardyViewModel.comers.value?[indexPath.row] else { return cell }
+        
+        cell.nameLabel.setText(data.name, style: .body06, color: .gray6)
+        
+        guard let image = URL(string: data.profileImageURL) else {
+            cell.profileImageView.image = .imgProfile
+            
+            return cell
+        }
+        
+        cell.profileImageView.kf.setImage(with: image)
+        
         return cell
     }
 }
diff --git a/KkuMulKum/Source/Promise/Tardy/ViewModel/TardyViewModel.swift b/KkuMulKum/Source/Promise/Tardy/ViewModel/TardyViewModel.swift
index c03ecb79..55867fea 100644
--- a/KkuMulKum/Source/Promise/Tardy/ViewModel/TardyViewModel.swift
+++ b/KkuMulKum/Source/Promise/Tardy/ViewModel/TardyViewModel.swift
@@ -12,11 +12,13 @@ class TardyViewModel {
     
     // MARK: Property
 
-    let tardyService: TardyServiceType
-    var isPastDue: ObservablePattern<Bool> = ObservablePattern<Bool>(true)
-    var hasTardy: ObservablePattern<Bool> = ObservablePattern<Bool>(true)
-    var isFinishButtonEnabled: ObservablePattern<Bool> = ObservablePattern<Bool>(true)
-    let promiseID: ObservablePattern<Int>
+    private let tardyService: TardyServiceType
+    let promiseID: Int
+    
+    var hasTardy: ObservablePattern<Bool> = ObservablePattern<Bool>(false)
+    var isPastDue: ObservablePattern<Bool> = ObservablePattern<Bool>(false)
+    var penalty: ObservablePattern<String> = ObservablePattern<String>("")
+    var comers: ObservablePattern<[Comer]?> = ObservablePattern<[Comer]?>(nil)
     
     
     // MARK: Initialize
@@ -26,6 +28,50 @@ class TardyViewModel {
         promiseID: Int
     ) {
         self.tardyService = tardyService
-        self.promiseID = ObservablePattern<Int>(promiseID)
+        self.promiseID = promiseID
+    }
+}
+
+extension TardyViewModel {
+    func fetchTardyInfo() {
+        Task {
+            do {
+                let responseBody = try await
+                tardyService.fetchTardyInfo(with: promiseID)
+                
+                guard let success = responseBody?.success, success == true
+                else {
+                    return
+                }
+                
+                guard let data = responseBody?.data else {
+                    return
+                }
+                
+                hasTardy.value = data.lateComers.isEmpty
+                isPastDue.value = data.isPastDue
+                penalty.value = data.penalty
+                comers.value = data.lateComers
+                
+            } catch {
+                print(">>>>> \(error.localizedDescription) : \(#function)")
+            }
+        }
+    }
+    
+    func updatePromiseCompletion() {
+        Task {
+            do {
+                let responseBody = try await
+                tardyService.updatePromiseCompletion(with: promiseID)
+                
+                guard let success = responseBody?.success, success == true
+                else {
+                    return
+                }
+            } catch {
+                print(">>>>> \(error.localizedDescription) : \(#function)")
+            }
+        }
     }
 }

From b292e1d0335c8e0dee15f2e8032163b4fa47d708 Mon Sep 17 00:00:00 2001
From: youz2me <kynhun20@gachon.ac.kr>
Date: Fri, 19 Jul 2024 14:31:43 +0900
Subject: [PATCH 2/2] =?UTF-8?q?feat/#240=20=EC=A4=80=EB=B9=84=20=EC=A0=95?=
 =?UTF-8?q?=EB=B3=B4=20=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EA=B8=B0?=
 =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ReadyStatus/View/ReadyPlanInfoView.swift  | 10 +--
 .../ReadyStatusViewController.swift           | 30 +++++++--
 .../ViewModel/ReadyStatusViewModel.swift      | 64 ++++++++++++++++++-
 3 files changed, 90 insertions(+), 14 deletions(-)

diff --git a/KkuMulKum/Source/Promise/ReadyStatus/View/ReadyPlanInfoView.swift b/KkuMulKum/Source/Promise/ReadyStatus/View/ReadyPlanInfoView.swift
index 06336de2..879277fe 100644
--- a/KkuMulKum/Source/Promise/ReadyStatus/View/ReadyPlanInfoView.swift
+++ b/KkuMulKum/Source/Promise/ReadyStatus/View/ReadyPlanInfoView.swift
@@ -12,16 +12,16 @@ class ReadyPlanInfoView: BaseView {
     
     // MARK: Property
 
-    private let readyTimeLabel: UILabel = UILabel().then {
+    let readyTimeLabel: UILabel = UILabel().then {
         $0.setText("12시 30분에 준비하고,\n1시에 이동을 시작해야 해요", style: .body03)
         $0.setHighlightText("12시 30분", "1시", style: .body03, color: .maincolor)
     }
     
-    private let requestReadyTimeLabel: UILabel = UILabel().then {
+    let requestReadyTimeLabel: UILabel = UILabel().then {
         $0.setText("준비 소요 시간: 30분", style: .label02, color: .gray8)
     }
     
-    private let requestMoveTimeLabel: UILabel = UILabel().then {
+    let requestMoveTimeLabel: UILabel = UILabel().then {
         $0.setText("이동 소요 시간: 1시간 30분", style: .label02, color: .gray8)
     }
     
@@ -69,7 +69,3 @@ class ReadyPlanInfoView: BaseView {
         }
     }
 }
-
-extension ReadyPlanInfoView {
-    func configure() {}
-}
diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewController/ReadyStatusViewController.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/ReadyStatusViewController.swift
index 78a267df..9208109c 100644
--- a/KkuMulKum/Source/Promise/ReadyStatus/ViewController/ReadyStatusViewController.swift
+++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/ReadyStatusViewController.swift
@@ -73,7 +73,6 @@ class ReadyStatusViewController: BaseViewController {
     
     @objc
     func readyStartButtonDidTapped() {
-        // TODO: 늦었을 때 꾸물거릴 시간이 없어요 팝업 뜨도록 설정
         readyStatusViewModel.myReadyProgressStatus.value = .ready
         rootView.myReadyStatusProgressView.readyStartButton.isEnabled.toggle()
     }
@@ -174,9 +173,32 @@ private extension ReadyStatusViewController {
                     owner.updateReadyInfoView(flag: false)
                     return
                 }
-                // TODO: 시간 계산 로직 필요..
+                
                 owner.updateReadyInfoView(flag: true)
-                owner.rootView.readyPlanInfoView.configure()
+                owner.readyStatusViewModel.calculatePrepareTime()
+                owner.readyStatusViewModel.convertMinute()
+            }
+        }
+        
+        readyStatusViewModel.moveTime.bind(with: self) { owner, moveTime in
+            owner.rootView.readyPlanInfoView.requestMoveTimeLabel.setText("이동 소요 시간: \(moveTime)", style: .label02, color: .gray8)
+        }
+        
+        readyStatusViewModel.readyTime.bind(with: self) { owner, readyTime in
+            owner.rootView.readyPlanInfoView.requestReadyTimeLabel.setText("준비 소요 시간: \(readyTime)", style: .label02, color: .gray8)
+        }
+        
+        readyStatusViewModel.readyStartTime.bind(with: self) { owner, readyStartTime in
+            DispatchQueue.main.async {
+                owner.rootView.readyPlanInfoView.readyTimeLabel.setText("\(readyStartTime)에 준비하고,\n\(owner.readyStatusViewModel.moveStartTime.value)에 이동을 시작해야 해요", style: .body03)
+                owner.rootView.readyPlanInfoView.readyTimeLabel.setHighlightText(readyStartTime, owner.readyStatusViewModel.moveStartTime.value, style: .body03, color: .maincolor)
+            }
+        }
+        
+        readyStatusViewModel.moveStartTime.bind(with: self) { owner, moveStartTime in
+            DispatchQueue.main.async {
+                owner.rootView.readyPlanInfoView.readyTimeLabel.setText("\(owner.readyStatusViewModel.readyStartTime.value)에 준비하고,\n\(moveStartTime)에 이동을 시작해야 해요", style: .body03)
+                owner.rootView.readyPlanInfoView.readyTimeLabel.setHighlightText(owner.readyStatusViewModel.readyStartTime.value, moveStartTime, style: .body03, color: .maincolor)
             }
         }
         
@@ -211,7 +233,7 @@ private extension ReadyStatusViewController {
     }
     
     func updateReadyStartButton(status: ReadyProgressStatus) {
-        /// 버튼 누를 때 서버 통신하게 설정
+        // TODO: 버튼 누를 때 서버 통신하게 설정
         switch status {
         case .none:
             rootView.myReadyStatusProgressView.readyStartButton.setupButton(
diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/ReadyStatusViewModel.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/ReadyStatusViewModel.swift
index f0e82c47..2a64ce30 100644
--- a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/ReadyStatusViewModel.swift
+++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/ReadyStatusViewModel.swift
@@ -17,13 +17,22 @@ class ReadyStatusViewModel {
     /// bind를 사용하라는 말이 아님
     let promiseName = ObservablePattern<String>("")
     
-    // 준비 정보가 입력되었는지 여부
-//    let isReadyInfoEntered = ObservablePattern<Bool>(false)
-    
     /// 나의 준비현황이 담긴 정보
     /// 설령 데이터가 없다하더라도 약속 시간은 담겨있음.
     let myReadyStatus = ObservablePattern<MyReadyStatusModel?>(nil)
     
+    // 준비 시작 시간
+    var readyStartTime = ObservablePattern<String>("")
+    
+    // 준비 소요 시간
+    var readyTime = ObservablePattern<String>("")
+    
+    // 이동 시작 시간
+    var moveStartTime = ObservablePattern<String>("")
+    
+    // 이동 소요 시간
+    var moveTime = ObservablePattern<String>("")
+    
     // 현재 준비 상태에 대한 버튼 처리
     let myReadyProgressStatus = ObservablePattern<ReadyProgressStatus>(.none)
     
@@ -63,6 +72,55 @@ class ReadyStatusViewModel {
 }
 
 extension ReadyStatusViewModel {
+    func convertMinute() {
+        let preparationHours = (self.myReadyStatus.value?.preparationTime ?? 0) / 60
+        let preparationMinutes = (self.myReadyStatus.value?.preparationTime ?? 0) % 60
+        
+        readyTime.value = preparationHours == 0 ? "\(preparationMinutes)분" : "\(preparationHours)시간 \(preparationMinutes)분"
+        
+        let travelHours = (self.myReadyStatus.value?.travelTime ?? 0) / 60
+        let travelMinutes = (self.myReadyStatus.value?.travelTime ?? 0) % 60
+        
+        moveTime.value = travelHours == 0 ? "\(travelMinutes)분" : "\(travelHours)시간 \(travelMinutes)분"
+    }
+    
+    func calculatePrepareTime() {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+        dateFormatter.locale = Locale(identifier: "ko_KR")
+        dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
+        
+        let promiseTime = self.myReadyStatus.value?.promiseTime ?? ""
+        let readyTime = self.myReadyStatus.value?.preparationTime ?? 0
+        let moveTime = self.myReadyStatus.value?.travelTime ?? 0
+        
+        
+        guard let promiseDate = dateFormatter.date(from: promiseTime) else {
+            print("Invalid date format: \(promiseTime)")
+            return
+        }
+        
+        let totalPrepTime = TimeInterval((readyTime + moveTime) * 60)
+        
+        let timeFormatter = DateFormatter()
+        timeFormatter.dateFormat = "HH시 mm분"
+        timeFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
+        
+        print("약속 시간: \(timeFormatter.string(from: promiseDate))")
+        print("준비 시간: \(readyTime) 분")
+        print("이동 시간: \(moveTime) 분")
+        print("총 준비 시간: \(totalPrepTime / 60) 분")
+        
+        let readyStartTime = promiseDate.addingTimeInterval(-TimeInterval(readyTime + moveTime) * 60)
+        let moveStartTime = promiseDate.addingTimeInterval(-TimeInterval(moveTime) * 60)
+        
+        self.readyStartTime.value = timeFormatter.string(from: readyStartTime)
+        print("준비 시작 시간: \(self.readyStartTime.value)")
+
+        self.moveStartTime.value = timeFormatter.string(from: moveStartTime)
+        print("이동 시작 시간: \(self.moveStartTime.value)")
+    }
+    
     func fetchMyReadyStatus() {
         Task {
             do {