diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/Model/DisplayDayLog.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/Model/DisplayDayLog.swift
index a5fa9ea..fd6bece 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/Model/DisplayDayLog.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/Model/DisplayDayLog.swift
@@ -8,6 +8,7 @@
import Foundation
// MARK: - DisplayDayLog Model
+/// 하루 동안의 운동 기록을 표시하기 위한 모델
struct DisplayDayLog {
let date: Date
let locationName: String
@@ -20,11 +21,14 @@ struct DisplayDayLog {
let totalSteps: Int
}
-
// MARK: - DisplayDayLog 초기화 추가
extension DisplayDayLog {
+ /// DayLog 모델 데이터를 기반으로 DisplayDayLog를 초기화하는 생성자
+ /// - Parameter dayLog: 원본 DayLog 데이터
init(from dayLog: DayLog) {
self.date = dayLog.date
+
+ // 위치 문자열을 공백으로 분리 후 필요한 토큰만 사용
let splitted = dayLog.locationName.split(separator: " ").map { String($0) }
if splitted.isEmpty {
self.locationName = ""
@@ -34,7 +38,7 @@ extension DisplayDayLog {
// 첫 번째 토큰이 "시"로 끝나면 첫 번째 토큰만 사용
self.locationName = first
} else {
- // 그렇지 않으면, 두 번째 토큰까지 합쳐서 사용 (단, 두 번째 토큰이 없으면 첫 번째 토큰만 사용)
+ // 그렇지 않으면, 두 번째 토큰까지 합쳐서 사용 (두 번째 토큰이 없으면 첫 번째 토큰만 사용)
if splitted.count >= 2 {
self.locationName = "\(first) \(splitted[1])"
} else {
@@ -42,6 +46,8 @@ extension DisplayDayLog {
}
}
}
+
+ // 날씨 정보 및 온도, 제목, 난이도, 소요시간, 거리, 걸음수 설정
self.weather = Constants.WeatherCondition.from(dayLog.weather).description
self.temperature = dayLog.temperature
self.title = dayLog.title
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/Model/RecordDetail.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/Model/RecordDetail.swift
index bea973b..abe9fc4 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/Model/RecordDetail.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/Model/RecordDetail.swift
@@ -7,20 +7,24 @@
import Foundation
+/// 각 기록 섹션의 세부 정보를 담는 모델
struct RecordDetail {
let timeRange: String // 예: "06:12 - 06:18"
let distance: String // 예: "1.81km"
let steps: String // 예: "345"
- let route: [Point]
+ let route: [Point] // 해당 섹션의 경로 좌표 배열
}
extension RecordDetail {
- /// Section 데이터를 기반으로 RecordDetail 생성
+ /// Section 데이터를 기반으로 RecordDetail을 생성하는 생성자
+ /// - Parameter section: 원본 Section 데이터
init(from section: Section) {
- // route 배열을 timestamp 기준으로 정렬
+ // Section의 route 배열을 timestamp 기준으로 오름차순 정렬
let sortedRoute = section.route.sorted { $0.timestamp < $1.timestamp }
let startTime = sortedRoute.first?.timestamp
let endTime = sortedRoute.last?.timestamp
+
+ // 시작 시간과 종료 시간을 "HH:mm" 형식의 문자열로 변환하여 timeRange 생성
let timeRange: String
if let start = startTime, let end = endTime {
let formatter = DateFormatter()
@@ -31,9 +35,9 @@ extension RecordDetail {
}
self.timeRange = timeRange
+ // 거리와 걸음수는 형식에 맞게 문자열로 변환
self.distance = String(format: "%.2fkm", section.distance)
self.steps = "\(section.steps)"
self.route = section.route
}
}
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogView.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogView.swift
index e94c5d5..0f50cb4 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogView.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogView.swift
@@ -10,7 +10,6 @@ import MapKit
import SnapKit
import Then
-
final class DetailLogView: UIView {
// MARK: - UI Components 선언
@@ -26,33 +25,32 @@ final class DetailLogView: UIView {
/// 지도 영역을 표시하는 MKMapView
private let mapView = MKMapView().then {
- // 추가 delegate 설정 및 커스터마이징 가능
+ // 지도 영역에 둥근 모서리 효과 적용 및 클리핑 활성화
$0.layer.cornerRadius = DynamicSize.scaledSize(10)
$0.clipsToBounds = true
}
-
-
/// 지도 위에 오버레이될 무빙트랙 버튼
let movingTrackButton = UIButton(type: .system).then {
$0.configuration = UIButton.Configuration.plain()
+ // 버튼의 타이틀 설정
let attributedTitle = NSAttributedString.RLAttributedString(text: "무빙트랙", font: .Label2)
$0.setAttributedTitle(attributedTitle, for: .normal)
$0.backgroundColor = .Gray300
$0.layer.cornerRadius = DynamicSize.scaledSize(10)
+ // 버튼 아이콘 설정 (시스템 이미지 사용)
let config = UIImage.SymbolConfiguration(pointSize: DynamicSize.scaledSize(16), weight: .regular)
let icon = UIImage(systemName: RLIcon.play.name)?.withConfiguration(config)
$0.setImage(icon, for: .normal)
$0.tintColor = .Gray000
+ // 이미지와 타이틀 간의 여백 설정
$0.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: DynamicSize.scaledSize(8), bottom: 0, trailing: DynamicSize.scaledSize(8))
$0.configuration?.imagePadding = DynamicSize.scaledSize(8)
}
-
-
/// 타이틀 라벨
private let titleLabel = RLLabel(
text: "하트런",
@@ -120,6 +118,7 @@ final class DetailLogView: UIView {
font: .RLHeading1
)
+ /// 소요시간 관련 정보를 담는 수직 스택뷰
private lazy var timeStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [timeTitleLabel, timeValueLabel])
stack.axis = .vertical
@@ -144,6 +143,7 @@ final class DetailLogView: UIView {
font: .RLHeading1
)
+ /// 운동거리 관련 정보를 담는 수직 스택뷰
private lazy var distanceStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [distanceTitleLabel, distanceValueLabel])
stack.axis = .vertical
@@ -168,6 +168,7 @@ final class DetailLogView: UIView {
font: .RLHeading1
)
+ /// 걸음수 관련 정보를 담는 수직 스택뷰
private lazy var stepsStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [stepsTitleLabel, stepsValueLabel])
stack.axis = .vertical
@@ -176,6 +177,7 @@ final class DetailLogView: UIView {
return stack
}()
+ /// 소요시간, 운동거리, 걸음수 정보를 수평으로 나열하는 스택뷰
lazy var statsStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [timeStack, distanceStack, stepsStack])
stack.axis = .horizontal
@@ -196,8 +198,8 @@ final class DetailLogView: UIView {
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
- setupUI()
- setupLayout()
+ setupUI() // UI 요소들을 추가
+ setupLayout() // SnapKit을 통한 레이아웃 설정
print("LogView initialized")
}
@@ -207,19 +209,22 @@ final class DetailLogView: UIView {
}
// MARK: - Setup UI
+ /// 하위 뷰들을 계층 구조에 추가하는 메서드
private func setupUI() {
- // UI 요소 추가
backgroundColor = .Gray900
+ // 스크롤뷰와 내부 콘텐츠 뷰 추가
addSubview(scrollView)
scrollView.addSubview(contentView)
+ // 콘텐츠 뷰에 지도, 타이틀, 날씨, 구분선, 통계, 기록 상세 관련 뷰 추가
contentView.addSubviews(mapView, titleLabel, weatherStack, separatorView, statsStack, recordTitleLabel, recordDetailView)
+ // 지도 위에 무빙트랙 버튼 추가
mapView.addSubview(movingTrackButton)
-
}
// MARK: - Setup Layout
+ /// SnapKit을 활용하여 각 UI 요소들의 레이아웃 제약 조건을 설정하는 메서드
private func setupLayout() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(self)
@@ -230,43 +235,46 @@ final class DetailLogView: UIView {
make.width.equalTo(self.safeAreaLayoutGuide.snp.width)
}
+ // 지도 영역 설정: 상단 인셋과 좌우 인셋, 높이는 너비와 동일하게 설정
mapView.snp.makeConstraints { make in
make.top.equalToSuperview().inset(DynamicSize.scaledSize(16))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(24))
make.height.equalTo(mapView.snp.width)
}
+ // 무빙트랙 버튼 위치 설정: 지도 뷰의 우측 하단
movingTrackButton.snp.makeConstraints { make in
make.bottom.trailing.equalToSuperview().inset(DynamicSize.scaledSize(8))
make.width.equalTo(DynamicSize.scaledSize(95))
make.height.equalTo(DynamicSize.scaledSize(40))
}
- // 타이틀 라벨: 맵뷰 아래
+ // 타이틀 라벨 배치: 지도 하단에 위치
titleLabel.snp.makeConstraints { make in
make.top.equalTo(mapView.snp.bottom).offset(DynamicSize.scaledSize(24))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(24))
}
- // 날씨 스택: 타이틀 아래
+ // 날씨 스택 배치: 타이틀 라벨 아래에 위치
weatherStack.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(DynamicSize.scaledSize(8))
make.leading.equalToSuperview().inset(DynamicSize.scaledSize(24))
}
- // 구분선:날씨 스택 아래
+ // 구분선 배치: 날씨 스택 아래, 좌우 인셋과 높이 설정
separatorView.snp.makeConstraints { make in
make.top.equalTo(weatherStack.snp.bottom).offset(DynamicSize.scaledSize(8))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(24))
make.height.equalTo(DynamicSize.scaledSize(1))
}
- // 통계 스택: 구분선 아래
+ // 통계 스택 배치: 구분선 아래에 위치
statsStack.snp.makeConstraints { make in
make.top.equalTo(separatorView.snp.bottom).offset(DynamicSize.scaledSize(8))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(24))
}
+ // 각 정보 스택의 너비 비율 설정
timeStack.snp.makeConstraints { make in
make.width.equalTo(statsStack.snp.width).multipliedBy(0.334)
}
@@ -277,17 +285,17 @@ final class DetailLogView: UIView {
make.width.equalTo(statsStack.snp.width).multipliedBy(0.333)
}
- // 기록상세
+ // 기록 상세 타이틀 배치
recordTitleLabel.snp.makeConstraints { make in
make.top.equalTo(statsStack.snp.bottom).offset(DynamicSize.scaledSize(24))
make.leading.equalToSuperview().inset(DynamicSize.scaledSize(24))
}
- // “기록 상세” 테이블뷰
+ // “기록 상세” 테이블뷰 배치
recordDetailView.snp.makeConstraints { make in
- make.top.equalTo(recordTitleLabel.snp.bottom).offset(DynamicSize.scaledSize(16))
+ make.top.equalTo(recordTitleLabel.snp.bottom).offset(DynamicSize.scaledSize(8))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(16))
- make.bottom.equalToSuperview().offset(DynamicSize.scaledSize(-20))
+ make.bottom.lessThanOrEqualToSuperview().offset(DynamicSize.scaledSize(-20))
}
}
@@ -338,9 +346,8 @@ extension DetailLogView {
mapView.setRegion(region, animated: animated)
}
+ /// 맵뷰에 추가된 모든 오버레이 제거
func removeAllMapOverlays() {
mapView.removeOverlays(mapView.overlays)
}
}
-
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogViewController.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogViewController.swift
index 4249966..bb0c9a3 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogViewController.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/DetailLogViewController.swift
@@ -49,18 +49,22 @@ final class DetailLogViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("디버그: viewDidLoad 호출됨, 시각: \(Date())")
+
+ // 테이블뷰의 데이터소스와 delegate 설정
let tableView = detailLogView.recordDetailView.tableView
tableView.dataSource = self
tableView.delegate = self
+
setupUI()
setupNavigationBar()
bindGesture()
bindViewModel()
- // setupMapView()
+ // setupMapView() // 초기 맵뷰 설정은 뷰모델 바인딩 내에서 처리
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
+ // 뷰모델을 통해 DayLog 데이터를 새로고침하고 UI 업데이트
viewModel.refreshDayLog()
refreshUI()
}
@@ -70,15 +74,17 @@ final class DetailLogViewController: UIViewController {
}
// MARK: - Setup UI
+ /// 추가적인 UI 설정이 필요할 경우 여기에 구현 (현재는 기본 설정만 적용)
private func setupUI() {
- // UI 요소 추가
+ // UI 요소 추가 시 필요한 설정을 여기에 작성
}
// MARK: - Setup Navigation Bar
+ /// 네비게이션 바 스타일 및 오른쪽 메뉴 버튼 추가 설정
private func setupNavigationBar() {
- // 네비게이션바 디테일 설정
- navigationController?.setupAppearance() // 스타일 설정
+ navigationController?.setupAppearance() // 네비게이션 바 스타일 적용
navigationController?.navigationItem.backButtonTitle = "chevron.left"
+
navigationController?
.addRightMenuButton(menuItems: [
("수정하기", .init()),
@@ -92,8 +98,9 @@ final class DetailLogViewController: UIViewController {
}
// MARK: - Setup Gesture
+ /// 무빙트랙 버튼 및 통계 스택의 제스처를 바인딩하는 메서드
private func bindGesture() {
- // 제스처 추가
+ // 무빙트랙 버튼 터치 시 시트형태의 화면 표시
detailLogView.movingTrackButton.controlPublisher(for: .touchUpInside)
.sink { [weak self] _ in
guard let self = self else { return }
@@ -107,37 +114,33 @@ final class DetailLogViewController: UIViewController {
sheet.detents = [customDetent]
sheet.selectedDetentIdentifier = customDetent.identifier
- // Grabber 제거
+ // Grabber 비표시 설정 및 기타 시트 옵션 적용
sheet.prefersGrabberVisible = false
-
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
sheet.preferredCornerRadius = DynamicSize.scaledSize(16)
}
-
self.present(sheetVC, animated: true)
}
.store(in: &cancellables)
- // 통계 스택 탭 시 전체 경로 줌아웃 처리
+ // 통계 스택 탭 시 전체 경로를 보여주기 위해 맵 뷰를 줌 아웃
let statsTapGesture = UITapGestureRecognizer()
detailLogView.statsStack.addGestureRecognizer(statsTapGesture)
statsTapGesture.tapPublisher
.sink { [weak self] _ in
guard let self = self, let dayLog = self.currentDayLog else { return }
- self.zoomToAllPoints(dayLog: dayLog) // 전체 경로로 줌아웃
+ self.zoomToAllPoints(dayLog: dayLog)
}
.store(in: &cancellables)
-
}
-
// MARK: - Bind ViewModel
+ /// 뷰모델의 output 이벤트를 수신하여 UI를 업데이트하고 필요한 액션을 수행하는 메서드
private func bindViewModel() {
-
viewModel.output
.receive(on: DispatchQueue.main)
.sink { [weak self] output in
@@ -146,28 +149,27 @@ final class DetailLogViewController: UIViewController {
case .loadedDayLog(let dayLog):
self.currentDayLog = dayLog
- // 첫 지점의 timestamp 기준으로 각 섹션 정렬 (최신순: 내림차순)
+ // 각 섹션의 첫 timestamp 기준으로 내림차순 정렬
let sortedSections = dayLog.sections.sorted { lhsSection, rhsSection in
let lhsStartTime = lhsSection.route.sorted { $0.timestamp < $1.timestamp }
.first?.timestamp ?? Date.distantPast
let rhsStartTime = rhsSection.route.sorted { $0.timestamp < $1.timestamp }
.first?.timestamp ?? Date.distantPast
-
return lhsStartTime > rhsStartTime
}
self.recordDetails = sortedSections.map { RecordDetail(from: $0) }
-
+ // DayLog 데이터를 기반으로 뷰 업데이트
self.detailLogView.configure(with: DisplayDayLog(from: dayLog))
- self.recordDetails = dayLog.sections.map {
- RecordDetail(from: $0)
- }
+ self.recordDetails = dayLog.sections.map { RecordDetail(from: $0) }
self.detailLogView.recordDetailView.tableView.reloadData()
self.setupMapView(with: dayLog)
+
case .edit:
let editViewModel = EditLogInfoViewModel(date: viewModel.date)
self.navigationController?.pushViewController(EditLogInfoViewController(viewModel: editViewModel), animated: true)
+
case .share:
self.handleShare()
@@ -182,6 +184,7 @@ final class DetailLogViewController: UIViewController {
}
// MARK: - Action Handlers (함수 분리)
+ /// 공유 액션 처리: 현재 DayLog의 트랙 이미지를 공유
private func handleShare() {
guard let data = currentDayLog?.trackImage else { return }
let shareItems: [Any] = [UIImage(data: data)!]
@@ -189,6 +192,7 @@ final class DetailLogViewController: UIViewController {
self.present(activityVC, animated: true)
}
+ /// 삭제 액션 처리: 삭제 확인 Alert 후 삭제 처리
private func handleDelete(in targetVC: UIViewController, dateString: String) {
let alert = UIAlertController(
title: "기록 삭제하기",
@@ -200,7 +204,7 @@ final class DetailLogViewController: UIViewController {
Task {
do {
try await self.viewModel.deleteDayLog()
- // 삭제 성공 후 이전 화면으로 돌아가거나 추가 작업 수행
+ // 삭제 성공 후 이전 화면으로 이동
DispatchQueue.main.async {
self.navigationController?.popViewController(animated: true)
}
@@ -225,11 +229,12 @@ final class DetailLogViewController: UIViewController {
targetVC.present(alert, animated: true)
}
-
+ /// 네비게이션 타이틀을 날짜 정보로 업데이트
private func updateNavigationTitle(with date: Date) {
self.title = date.formattedString(.monthDay)
}
+ /// 현재 DayLog 데이터를 기반으로 전체 UI를 새로고침
private func refreshUI() {
guard let dayLog = currentDayLog else { return }
detailLogView.configure(with: DisplayDayLog(from: dayLog))
@@ -246,55 +251,48 @@ extension DetailLogViewController: UITableViewDataSource, UITableViewDelegate {
return 1
}
- // 헤더 행 1개 + 실제 데이터 수
+ // 실제 데이터 행 수만 반환
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return recordDetails.count + 1
+ return recordDetails.count
+ }
+
+ // 섹션 헤더 뷰 반환
+ func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ guard let header = tableView.dequeueReusableHeaderFooterView(
+ withIdentifier: RecordDetailHeaderView.identifier
+ ) as? RecordDetailHeaderView else {
+ return nil
+ }
+ return header
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
- if indexPath.row == 0 {
- // 헤더 셀
- guard let cell = tableView.dequeueReusableCell(
- withIdentifier: RecordDetailViewCell.identifier,
- for: indexPath
- ) as? RecordDetailViewCell else {
- return UITableViewCell()
- }
- cell.configureAsHeader()
- //print("디버그: 헤더 셀 생성됨, 시각: \(Date())")
- return cell
+ let record = recordDetails[indexPath.row]
+ guard let cell = tableView.dequeueReusableCell(
+ withIdentifier: RecordDetailViewCell.identifier,
+ for: indexPath
+ ) as? RecordDetailViewCell else {
+ return UITableViewCell()
+ }
+ // 선택된 셀은 폰트를 RLHeadline1, 그 외는 RLHeadline2로 설정
+ if indexPath.row == selectedSectionIndex {
+ cell.configure(with: record, font: .RLHeadline1)
} else {
- let record = recordDetails[indexPath.row - 1]
- guard let cell = tableView.dequeueReusableCell(
- withIdentifier: RecordDetailViewCell.identifier,
- for: indexPath
- ) as? RecordDetailViewCell else {
- return UITableViewCell()
- }
- // 선택된 셀이면 폰트를 RLHeadline1, 아니면 RLHeadline2로 설정
- if indexPath.row - 1 == selectedSectionIndex {
- cell.configure(with: record, font: .RLHeadline1)
- } else {
- cell.configure(with: record, font: .RLHeadline2)
- }
- return cell
+ cell.configure(with: record, font: .RLHeadline2)
}
+ return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- // 헤더 셀은 무시 (indexPath.row == 0)
- guard indexPath.row > 0 else { return }
-
- let newSelectionIndex = indexPath.row - 1
+ let newSelectionIndex = indexPath.row
let previousSelection = selectedSectionIndex
if let previous = previousSelection, previous != newSelectionIndex {
- // 다른 셀을 선택한 경우: 기존 선택 해제 후 전체 경로 줌 (줌 아웃)
+ // 다른 셀 선택 시: 이전 선택 해제 후 전체 경로 줌(줌 아웃)
selectedSectionIndex = nil
tableView.reloadData()
- // 줌 아웃 전에 오버레이 업데이트
+ // 줌 아웃 전 오버레이 업데이트
detailLogView.removeAllMapOverlays()
for polyline in polylineOverlays {
detailLogView.addMapOverlay(polyline)
@@ -304,13 +302,13 @@ extension DetailLogViewController: UITableViewDataSource, UITableViewDelegate {
zoomToAllPoints(dayLog: dayLog)
}
- // 약간의 딜레이 후 새 선택 셀 줌 (줌 인)
+ // 약간의 딜레이 후 새 선택 셀 줌(줌 인)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.selectedSectionIndex = newSelectionIndex
tableView.reloadData()
- // 줌 인 전에 오버레이 업데이트
+ // 줌 인 전 오버레이 업데이트
self.detailLogView.removeAllMapOverlays()
for polyline in self.polylineOverlays {
self.detailLogView.addMapOverlay(polyline)
@@ -323,11 +321,11 @@ extension DetailLogViewController: UITableViewDataSource, UITableViewDelegate {
}
}
} else {
- // 동일한 셀을 선택한 경우 즉시 줌 처리
+ // 동일 셀 선택 시: 즉시 줌 처리
selectedSectionIndex = newSelectionIndex
tableView.reloadData()
- // 줌 처리 전에 오버레이 업데이트
+ // 줌 처리 전 오버레이 업데이트
detailLogView.removeAllMapOverlays()
for polyline in polylineOverlays {
detailLogView.addMapOverlay(polyline)
@@ -342,29 +340,27 @@ extension DetailLogViewController: UITableViewDataSource, UITableViewDelegate {
}
}
-
// MARK: - Setup MapView & 폴리라인
extension DetailLogViewController {
- /// DayLog를 파라미터로 받아 맵뷰 초기설정 및 폴리라인 그리기
+ /// DayLog를 기반으로 맵뷰 초기 설정 및 폴리라인 그리기
private func setupMapView(with dayLog: DayLog) {
// 1) 맵뷰 델리게이트 설정
detailLogView.setMapViewDelegate(self)
- // 2) 데이터 세팅 (DisplayDayLog 생성 대신, dayLog 데이터 활용)
+ // 2) DayLog 데이터를 기반으로 뷰 업데이트
detailLogView.configure(with: DisplayDayLog(from: dayLog))
// 3) 폴리라인 그리기
drawPolyline(from: dayLog)
}
- /// dummyDayLog의 모든 Section을 순회하여 폴리라인을 그리고, 적절히 확대
+ /// 모든 섹션의 좌표를 순회하여 폴리라인을 그리고 전체 영역으로 줌 아웃 처리
private func drawPolyline(from dayLog: DayLog) {
// 기존 오버레이 제거
detailLogView.removeAllMapOverlays()
polylineOverlays.removeAll()
- // 각 section 별로 폴리라인 생성
+ // 각 섹션별 폴리라인 생성
for (index, section) in dayLog.sections.enumerated() {
- // timestamp 기준으로 정렬한 후 좌표 배열 생성
let sortedCoordinates = section.route
.sorted(by: { $0.timestamp < $1.timestamp })
.map { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) }
@@ -377,18 +373,16 @@ extension DetailLogViewController {
detailLogView.addMapOverlay(polyline)
}
- // 전체 영역이 보이도록 확대
+ // 전체 경로가 보이도록 맵 뷰 줌 아웃 처리
zoomToAllPoints(dayLog: dayLog)
-
}
- /// 모든 경로 점들을 순회하여 바운딩 박스(최소·최대 위도/경도) 구하기
+ /// 모든 좌표를 순회하여 바운딩 박스를 계산하고, 전체 영역으로 줌 아웃
private func zoomToAllPoints(dayLog: DayLog) {
- // 1) 모든 Point 추출
let allPoints = dayLog.sections.flatMap { $0.route }
guard !allPoints.isEmpty else { return }
- // 2) min/max lat, lon 구하기
+ // 최소/최대 위도 및 경도 계산
var minLat = Double.greatestFiniteMagnitude
var maxLat = -Double.greatestFiniteMagnitude
var minLon = Double.greatestFiniteMagnitude
@@ -401,25 +395,18 @@ extension DetailLogViewController {
maxLon = max(maxLon, point.longitude)
}
- // 3) 중심좌표 = (minLat ~ maxLat)의 중앙, (minLon ~ maxLon)의 중앙
+ // 중심 좌표 계산
let centerLat = (minLat + maxLat) / 2
let centerLon = (minLon + maxLon) / 2
let center = CLLocationCoordinate2D(latitude: centerLat, longitude: centerLon)
- // 4) 가장 멀리 떨어진 두 점 = (minLat, minLon) vs (maxLat, maxLon) 라고 가정
+ // 두 모서리 좌표 사이의 거리 계산 (여유 1.2배 적용)
let corner1 = CLLocation(latitude: minLat, longitude: minLon)
let corner2 = CLLocation(latitude: maxLat, longitude: maxLon)
-
- // 5) 두 지점 사이의 거리(미터)
var distance = corner1.distance(from: corner2)
- // 거리에 여유를 주고 싶다면 1.2배 등 곱해주기
- if distance == 0 {
- distance = 5000
- } else {
- distance *= 1.2
- }
+ distance = (distance == 0) ? 5000 : distance * 1.2
- // 6) region 설정
+ // MKCoordinateRegion 생성 후 맵 영역 설정
let region = MKCoordinateRegion(
center: center,
latitudinalMeters: distance,
@@ -429,8 +416,8 @@ extension DetailLogViewController {
detailLogView.setMapRegion(region, animated: true)
}
+ /// 선택된 섹션의 좌표를 기반으로 지도 영역을 줌 인
private func zoomToRoute(route: [Point]) {
- // 경로가 비어있으면 아무 작업도 하지 않음
guard !route.isEmpty else { return }
var minLat = Double.greatestFiniteMagnitude
@@ -438,7 +425,7 @@ extension DetailLogViewController {
var minLon = Double.greatestFiniteMagnitude
var maxLon = -Double.greatestFiniteMagnitude
- // 각 좌표의 최소, 최대 위도/경도 계산
+ // 각 좌표의 최소, 최대 위도 및 경도 계산
for point in route {
minLat = min(minLat, point.latitude)
maxLat = max(maxLat, point.latitude)
@@ -451,23 +438,20 @@ extension DetailLogViewController {
let centerLon = (minLon + maxLon) / 2
let center = CLLocationCoordinate2D(latitude: centerLat, longitude: centerLon)
- // 두 모서리 좌표 사이의 거리 계산 (여유를 위해 1.2배)
+ // 두 모서리 좌표 사이의 거리 계산 (여유 1.2배 적용)
let corner1 = CLLocation(latitude: minLat, longitude: minLon)
let corner2 = CLLocation(latitude: maxLat, longitude: maxLon)
var distance = corner1.distance(from: corner2)
distance = (distance == 0) ? 5000 : distance * 1.2
- // MKCoordinateRegion 생성 후 맵뷰 영역 설정
+ // MKCoordinateRegion 생성 후 맵 영역 설정
let region = MKCoordinateRegion(center: center,
latitudinalMeters: distance,
longitudinalMeters: distance)
detailLogView.setMapRegion(region, animated: true)
}
-
}
-
-
// MARK: - MKMapViewDelegate
extension DetailLogViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
@@ -477,7 +461,7 @@ extension DetailLogViewController: MKMapViewDelegate {
let renderer = MKPolylineRenderer(polyline: polyline)
if let title = polyline.title, let index = Int(title), index == selectedSectionIndex {
- renderer.strokeColor = .NormalGreen // 선택된 section이면 NormalGreen색으로 표시
+ renderer.strokeColor = .NormalGreen // 선택된 섹션이면 NormalGreen 색상 적용
renderer.lineWidth = 6
} else {
renderer.strokeColor = .LightGreen
@@ -486,5 +470,3 @@ extension DetailLogViewController: MKMapViewDelegate {
return renderer
}
}
-
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetView.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetView.swift
index ff1ec2c..3e94e56 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetView.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetView.swift
@@ -13,6 +13,7 @@ import Then
final class MovingTrackSheetView: UIView {
// MARK: - UI Components 선언
+ /// 날짜를 표시하는 라벨
private let dateLabel = RLLabel(
text: "날짜",
textColor: .Gray000,
@@ -21,6 +22,7 @@ final class MovingTrackSheetView: UIView {
font: .RLHeading1
)
+ /// 서브타이틀을 표시하는 라벨
private let subtitleLabel = RLLabel(
text: "이날의 동선을 영상으로 확인해 보세요!",
textColor: .Gray100,
@@ -29,23 +31,24 @@ final class MovingTrackSheetView: UIView {
font: .RLBody1
)
+ /// 닫기 버튼 (뷰를 종료하기 위한 버튼)
let closeButton = UIButton(type: .system).then {
$0.setImage(UIImage(systemName: "xmark"), for: .normal)
$0.tintColor = .white
}
+ /// 지도 영역을 표시하는 MKMapView
let mapView = MKMapView().then {
- // 추가 delegate 설정 및 커스터마이징 가능
+ // 지도에 둥근 모서리 효과 적용 및 클리핑 활성화
$0.layer.cornerRadius = DynamicSize.scaledSize(16)
$0.clipsToBounds = true
}
-
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
- setupUI()
- setupLayout()
+ setupUI() // UI 요소 추가
+ setupLayout() // SnapKit 레이아웃 제약조건 설정
}
required init?(coder: NSCoder) {
@@ -53,52 +56,51 @@ final class MovingTrackSheetView: UIView {
}
// MARK: - Setup UI
+ /// 하위 뷰들을 추가하고 기본 배경색을 설정하는 메서드
private func setupUI() {
- // UI 요소 추가
backgroundColor = .Gray700
-
self.addSubviews(dateLabel, subtitleLabel, closeButton, mapView)
}
// MARK: - Setup Layout
+ /// SnapKit을 사용하여 UI 요소들의 레이아웃을 설정하는 메서드
private func setupLayout() {
- // 레이아웃 설정
- // 상단 날짜 라벨
+ // 날짜 라벨: safeArea 상단에서 일정 간격 떨어져 중앙에 위치
dateLabel.snp.makeConstraints { make in
make.top.equalTo(safeAreaLayoutGuide).offset(DynamicSize.scaledSize(48))
make.centerX.equalToSuperview()
}
- // 닫기 버튼
+ // 닫기 버튼: 날짜 라벨의 수평 중앙에 맞추고, 오른쪽에 인셋 적용
closeButton.snp.makeConstraints { make in
make.centerY.equalTo(dateLabel)
make.trailing.equalToSuperview().inset(DynamicSize.scaledSize(32))
}
- // 서브타이틀
+ // 서브타이틀: 날짜 라벨 아래에 위치하며 중앙 정렬
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(dateLabel.snp.bottom).offset(DynamicSize.scaledSize(4))
make.centerX.equalToSuperview()
}
- // 지도
+ // 지도: 서브타이틀 아래에서 시작, 좌우 인셋 적용, 높이는 너비 비율에 따라 설정하고 safeArea 하단에 여백 적용
mapView.snp.makeConstraints { make in
make.top.equalTo(subtitleLabel.snp.bottom).offset(DynamicSize.scaledSize(48))
make.leading.trailing.equalToSuperview().inset(DynamicSize.scaledSize(24))
make.height.equalTo(mapView.snp.width).multipliedBy(498.0 / 392.0)
make.bottom.equalTo(safeAreaLayoutGuide).offset(DynamicSize.scaledSize(-60))
}
-
}
// MARK: - Configure
+ /// 추가적인 뷰 설정을 위한 메서드 (필요 시 구현)
private func configure() {
- // 뷰 설정
+ // 추가 설정이 필요하면 여기에 작성
}
}
extension MovingTrackSheetView {
- /// 날짜를 받아서 dateLabel에 반영
+ /// 날짜를 받아 dateLabel에 반영하는 메서드
func configure(with date: Date) {
let dateString = date.formattedString(.detailedFull)
print("MovingTrackSheetView configure 호출됨, 날짜: \(dateString)")
@@ -108,12 +110,12 @@ extension MovingTrackSheetView {
extension MovingTrackSheetView {
- // 메서드: MapOverlay 추가
+ /// MapView에 오버레이를 추가하는 메서드
func addMapOverlay(_ overlay: MKOverlay) {
mapView.addOverlay(overlay)
}
- // 메서드: 맵 영역 설정
+ /// MapView의 영역을 설정하는 메서드
func setMapRegion(_ region: MKCoordinateRegion, animated: Bool) {
mapView.setRegion(region, animated: animated)
}
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetViewController.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetViewController.swift
index f3483ec..025e4e2 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetViewController.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/MovingTrackSheetViewController.swift
@@ -29,7 +29,6 @@ final class MovingTrackSheetViewController: UIViewController {
private var currentCoordIndexInSection = 0
/// 카메라 이동을 위한 타이머
private var cameraTimer: Timer?
-
/// 진행 경로를 누적하는 배열 (현재 섹션 내)
private var accumulatedProgressCoordinates: [CLLocationCoordinate2D] = []
@@ -44,7 +43,9 @@ final class MovingTrackSheetViewController: UIViewController {
}
// MARK: - Lifecycle
- override func loadView() { view = sheetView }
+ override func loadView() {
+ view = sheetView
+ }
override func viewDidLoad() {
super.viewDidLoad()
@@ -53,7 +54,6 @@ final class MovingTrackSheetViewController: UIViewController {
setupData()
bindViewModel()
setupMapView()
-
prepareSectionsCoordinates()
}
@@ -64,57 +64,61 @@ final class MovingTrackSheetViewController: UIViewController {
}
// MARK: - Setup Navigation Bar
+ /// 네비게이션 바 설정 (추가 설정 필요 시 이곳에 구현)
private func setupNavigationBar() {
// 네비게이션바 디테일 설정
}
// MARK: - Setup Gesture
+ /// 닫기 버튼에 대한 제스처를 바인딩하는 메서드
private func bindGesture() {
- // 제스처 추가
sheetView.closeButton.controlPublisher(for: .touchUpInside)
.sink { [weak self] _ in
self?.dismiss(animated: true)
}
.store(in: &cancellables)
-
}
// MARK: - Setup Data
+ /// 초기 데이터 로드를 위한 메서드 (필요 시 구현)
private func setupData() {
// 초기 데이터 로드
}
// MARK: - Bind ViewModel
+ /// 뷰모델의 출력 이벤트를 구독하여 UI 업데이트 및 액션을 처리하는 메서드
private func bindViewModel() {
- // viewModel.output.something
- // .sink { [weak self] value in
- // // View 업데이트 로직
- // }
- // .store(in: &cancellables)
+ // 예시 코드 - 필요 시 구현
+ // viewModel.output.something
+ // .sink { [weak self] value in
+ // // View 업데이트 로직
+ // }
+ // .store(in: &cancellables)
}
// MARK: - Setup MapView
+ /// MapView의 설정 및 기본 옵션 적용, 뷰모델에서 전달된 날짜를 반영
private func setupMapView() {
let mapView = sheetView.mapView
- // 사용자 상호작용 모두 비활성화
+ // 사용자 상호작용 비활성화
mapView.isUserInteractionEnabled = false
- // 1) 건물 표시 (3D 데이터가 있는 지역에서만 보임)
+ // 1) 3D 건물 표시 활성화 (해당 지역에서만 적용)
mapView.showsBuildings = true
- // 2) 사용자가 핀치/드래그 제스처로 지도 기울이거나 회전할 수 있게
+ // 2) 지도 기울이기 및 회전 가능
mapView.isPitchEnabled = true
mapView.isRotateEnabled = true
- // 3) 나머지 옵션
+ // 3) 추가 옵션 적용: 나침반, 축척 표시
mapView.showsCompass = true
mapView.showsScale = true
- // (Delegate 설정)
+ // Delegate 설정
mapView.delegate = self
- // 뷰모델에서 받아온 날짜를 전달 (예: dayLog.date)
+ // 뷰모델의 dayLogPublisher를 통해 날짜 전달 및 뷰 업데이트
viewModel.dayLogPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] dayLog in
@@ -123,13 +127,13 @@ final class MovingTrackSheetViewController: UIViewController {
.store(in: &cancellables)
}
- /// 각 섹션의 route를 그대로 sectionsCoordinates에 저장
+ /// 각 섹션의 route 데이터를 sectionsCoordinates에 저장하고 애니메이션 시작 준비
private func prepareSectionsCoordinates() {
viewModel.dayLogPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] dayLog in
guard let self = self else { return }
- // 섹션별 route를 timestamp 순으로 정렬한 뒤 좌표 배열로 변환
+ // 각 섹션별 route를 timestamp 순으로 정렬 후 좌표 배열로 변환
self.sectionsCoordinates = dayLog.sections.map { section in
section.route
.sorted(by: { $0.timestamp < $1.timestamp })
@@ -139,20 +143,19 @@ final class MovingTrackSheetViewController: UIViewController {
// 전체 경로 오버레이 그리기
self.drawAllSectionOverlays()
- // 첫 섹션 카메라 애니메이션 시작
+ // 첫 섹션에 대해 카메라 애니메이션 시작
self.startCameraAnimationForCurrentSection()
}
.store(in: &cancellables)
}
- /// 각 섹션별로 오버레이를 그려 전체 경로를 표현 (섹션 간 구분)
+ /// 각 섹션별 전체 경로 오버레이를 지도에 그리는 메서드
private func drawAllSectionOverlays() {
let mapView = sheetView.mapView
mapView.removeOverlays(mapView.overlays)
for (index, sortedCoordinates) in sectionsCoordinates.enumerated() {
guard sortedCoordinates.count >= 2 else { continue }
-
let polyline = MKPolyline(coordinates: sortedCoordinates, count: sortedCoordinates.count)
polyline.title = "전체 경로-\(index)"
sheetView.addMapOverlay(polyline)
@@ -160,25 +163,25 @@ final class MovingTrackSheetViewController: UIViewController {
}
// MARK: - 3D 카메라 애니메이션 (섹션별)
-
- /// 현재 섹션의 애니메이션 시작
+ /// 현재 섹션에 대해 카메라 애니메이션을 시작하는 메서드
private func startCameraAnimationForCurrentSection() {
guard currentSectionIndex < sectionsCoordinates.count else { return }
let sectionCoords = sectionsCoordinates[currentSectionIndex]
guard sectionCoords.count > 1 else { return }
+ // 타이머 초기화 및 섹션 내 좌표 인덱스 재설정
cameraTimer?.invalidate()
cameraTimer = nil
currentCoordIndexInSection = 0
- // 누적 좌표 배열 초기화 및 첫 좌표 추가
+ // 누적 좌표 배열 초기화 후 첫 좌표 추가 및 카메라 초기 설정
accumulatedProgressCoordinates = []
if let firstCoord = sectionCoords.first {
accumulatedProgressCoordinates.append(firstCoord)
- // 초기 카메라 설정: 현재 섹션 첫 좌표, 현재 카메라 heading 유지
setCameraToCoordinate(firstCoord, heading: sheetView.mapView.camera.heading)
}
+ // 타이머 시작: 0.2초 간격으로 카메라 업데이트
cameraTimer = Timer.scheduledTimer(timeInterval: 0.2,
target: self,
selector: #selector(updateCameraForSection),
@@ -186,7 +189,7 @@ final class MovingTrackSheetViewController: UIViewController {
repeats: true)
}
- /// 섹션 내에서 카메라를 업데이트하고 진행 구간을 개별 세그먼트로 추가
+ /// 섹션 내에서 카메라 위치와 heading을 업데이트하고, 진행 구간 오버레이를 추가하는 메서드
@objc private func updateCameraForSection() {
let sectionCoords = sectionsCoordinates[currentSectionIndex]
guard currentCoordIndexInSection < sectionCoords.count - 1 else {
@@ -197,17 +200,17 @@ final class MovingTrackSheetViewController: UIViewController {
let currentCoord = sectionCoords[currentCoordIndexInSection]
let nextCoord = sectionCoords[currentCoordIndexInSection + 1]
- // 경로 진행 방향에 따른 heading 계산
+ // 현재 좌표와 다음 좌표 간의 heading 계산
let rawHeading = calculateHeading(from: currentCoord, to: nextCoord)
let targetHeading = rawHeading.truncatingRemainder(dividingBy: 360)
+ // 애니메이션을 통한 카메라 이동
UIView.animate(withDuration: 0.2) {
self.setCameraToCoordinate(nextCoord, heading: targetHeading)
}
- // 누적 좌표 배열에 새로운 좌표 추가
+ // 진행 좌표 누적 및 세그먼트 오버레이 추가
accumulatedProgressCoordinates.append(nextCoord)
- // 바로 직전 좌표와 연결하는 세그먼트 오버레이 추가 (기존 오버레이는 제거하지 않음)
if accumulatedProgressCoordinates.count >= 2 {
let lastTwoCoords = Array(accumulatedProgressCoordinates.suffix(2))
let segmentPolyline = MKPolyline(coordinates: lastTwoCoords, count: lastTwoCoords.count)
@@ -218,13 +221,11 @@ final class MovingTrackSheetViewController: UIViewController {
currentCoordIndexInSection += 1
}
- /// 현재 섹션이 끝났다면 다음 섹션으로 전환 (있을 경우)
+ /// 현재 섹션이 끝나면 다음 섹션으로 전환하는 메서드 (있을 경우)
private func moveToNextSectionIfAvailable() {
if currentSectionIndex < sectionsCoordinates.count - 1 {
currentSectionIndex += 1
currentCoordIndexInSection = 0
-
- // 새로운 섹션으로 넘어갈 때 누적 좌표 배열 초기화
accumulatedProgressCoordinates = []
let nextSectionCoords = sectionsCoordinates[currentSectionIndex]
if let firstCoord = nextSectionCoords.first {
@@ -238,7 +239,7 @@ final class MovingTrackSheetViewController: UIViewController {
}
}
- /// 카메라 설정 함수
+ /// 주어진 좌표와 heading으로 카메라를 설정하는 메서드
private func setCameraToCoordinate(_ coordinate: CLLocationCoordinate2D, heading: CLLocationDirection) {
let camera = MKMapCamera(
lookingAtCenter: coordinate,
@@ -249,7 +250,7 @@ final class MovingTrackSheetViewController: UIViewController {
sheetView.mapView.camera = camera
}
- /// 두 좌표 간 heading(방위각) 계산
+ /// 두 좌표 간의 heading(방위각)을 계산하는 메서드
private func calculateHeading(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> CLLocationDirection {
let fromLat = from.latitude.deg2rad
let fromLon = from.longitude.deg2rad
@@ -267,8 +268,11 @@ final class MovingTrackSheetViewController: UIViewController {
// MARK: - MKMapViewDelegate
extension MovingTrackSheetViewController: MKMapViewDelegate {
+ /// 오버레이 렌더러를 설정하여 진행 경로와 전체 경로를 구분하여 그린다.
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
- guard let polyline = overlay as? MKPolyline, let title = polyline.title else { return MKOverlayRenderer() }
+ guard let polyline = overlay as? MKPolyline, let title = polyline.title else {
+ return MKOverlayRenderer()
+ }
let renderer = MKPolylineRenderer(polyline: polyline)
if title == "진행 경로" {
renderer.strokeColor = .NormalGreen
@@ -280,4 +284,3 @@ extension MovingTrackSheetViewController: MKMapViewDelegate {
return renderer
}
}
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailHeaderView.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailHeaderView.swift
new file mode 100644
index 0000000..a06bb2a
--- /dev/null
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailHeaderView.swift
@@ -0,0 +1,74 @@
+//
+// RecordDetailHeaderView.swift
+// RunLog
+//
+// Created by 도민준 on 4/7/25.
+//
+
+import UIKit
+import SnapKit
+import Then
+
+final class RecordDetailHeaderView: UITableViewHeaderFooterView {
+
+ static let identifier = "RecordDetailHeaderView"
+
+ // MARK: - UI Components 선언
+ /// 시간대를 표시하는 라벨 (예: "시간대")
+ private let timeLabel = UILabel().then {
+ $0.font = .RLBody1
+ $0.textColor = .Gray000
+ $0.textAlignment = .left
+ $0.text = "시간대"
+ }
+
+ /// 운동거리를 표시하는 라벨 (예: "운동거리")
+ private let distanceLabel = UILabel().then {
+ $0.font = .RLBody1
+ $0.textColor = .Gray000
+ $0.textAlignment = .left
+ $0.text = "운동거리"
+ }
+
+ /// 걸음수를 표시하는 라벨 (예: "걸음수")
+ private let stepsLabel = UILabel().then {
+ $0.font = .RLBody1
+ $0.textColor = .Gray000
+ $0.textAlignment = .left
+ $0.text = "걸음수"
+ }
+
+ /// 3개의 라벨을 수평으로 배치하는 스택뷰 (각 라벨의 크기와 간격을 균등하게 배분)
+ private lazy var horizontalStack = UIStackView(arrangedSubviews: [timeLabel, distanceLabel, stepsLabel]).then {
+ $0.axis = .horizontal
+ $0.alignment = .center
+ $0.distribution = .fillEqually
+ $0.spacing = DynamicSize.scaledSize(8)
+ }
+
+ // MARK: - Init
+ override init(reuseIdentifier: String?) {
+ super.init(reuseIdentifier: reuseIdentifier)
+ setupUI() // UI 요소들을 뷰 계층 구조에 추가
+ setupLayout() // SnapKit을 이용한 레이아웃 제약 조건 설정
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Setup UI
+ /// contentView의 배경색을 설정하고 수평 스택뷰를 추가하는 메서드
+ private func setupUI() {
+ contentView.backgroundColor = .Gray900
+ contentView.addSubview(horizontalStack)
+ }
+
+ // MARK: - Setup Layout
+ /// horizontalStack의 위치와 여백을 SnapKit을 이용해 설정하는 메서드
+ private func setupLayout() {
+ horizontalStack.snp.makeConstraints { make in
+ make.edges.equalToSuperview().inset(DynamicSize.scaledSize(8))
+ }
+ }
+}
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailView.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailView.swift
index 9cd6e1e..d1617cb 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailView.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailView.swift
@@ -12,9 +12,12 @@ import Then
final class RecordDetailView: UIView {
// MARK: - UI Components 선언
+ /// 테이블뷰: 기록 상세 데이터를 표시하기 위한 테이블뷰
lazy var tableView = UITableView(frame: .zero, style: .plain).then {
// 셀 등록
$0.register(RecordDetailViewCell.self, forCellReuseIdentifier: RecordDetailViewCell.identifier)
+ // 헤더 뷰 등록
+ $0.register(RecordDetailHeaderView.self, forHeaderFooterViewReuseIdentifier: RecordDetailHeaderView.identifier)
// 테이블뷰 기본 설정
$0.isScrollEnabled = false
@@ -23,16 +26,20 @@ final class RecordDetailView: UIView {
$0.separatorColor = .Gray500
$0.rowHeight = UITableView.automaticDimension
$0.estimatedRowHeight = DynamicSize.scaledSize(32)
+ if #available(iOS 15.0, *) {
+ $0.sectionHeaderTopPadding = 0
+ }
}
- // KVO 관찰자 (contentSize 변경)
+
+ /// KVO 관찰자: 테이블뷰 contentSize 변경을 관찰하기 위한 변수
private var contentSizeObservation: NSKeyValueObservation?
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
- setupUI()
- setupLayout()
- configure()
+ setupUI() // UI 요소 추가
+ setupLayout() // 레이아웃 제약조건 설정
+ configure() // 추가 설정 (KVO 관찰 시작)
}
required init?(coder: NSCoder) {
@@ -40,55 +47,49 @@ final class RecordDetailView: UIView {
}
// MARK: - Setup UI
+ /// 배경색을 설정하고 테이블뷰를 서브뷰로 추가
private func setupUI() {
- // UI 요소 추가
backgroundColor = .Gray900
addSubview(tableView)
}
// MARK: - Setup Layout
+ /// SnapKit을 사용해 테이블뷰의 레이아웃 제약조건을 설정
private func setupLayout() {
- // 초기 제약조건은 임의 높이 (나중에 contentSize에 따라 업데이트됨
+ // 테이블뷰의 가장자리와 고정된 초기 높이 설정 (이후 contentSize 변경에 따라 업데이트됨)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
- make.height.equalTo(DynamicSize.scaledSize(300)) // 초기 높이 (나중에 KVO로 업데이트됨)
+ make.height.equalTo(DynamicSize.scaledSize(50)) // 초기 높이
}
+ // 셀 구분선 인셋 설정
tableView.separatorInset = UIEdgeInsets(top: 0, left: DynamicSize.scaledSize(8), bottom: 0, right: DynamicSize.scaledSize(8))
-
}
// MARK: - Configure
+ /// 테이블뷰의 contentSize를 관찰하여 높이 제약조건을 업데이트하는 설정 수행
private func configure() {
- // 뷰 설정
observeTableViewContentSize()
}
// MARK: - KVO: contentSize 관찰 및 높이 업데이트 + 디버그 출력
+ /// 테이블뷰의 contentSize 변화를 감지하여 제약조건을 업데이트하고 레이아웃을 새로 고침
private func observeTableViewContentSize() {
contentSizeObservation = tableView.observe(\.contentSize, options: [.new, .old]) { [weak self] tableView, change in
guard let self = self, let newSize = change.newValue else { return }
- //print("디버그: tableView의 contentSize가 \(change.oldValue ?? .zero)에서 \(newSize)로 변경됨, 시각: \(Date())")
-
// SnapKit을 통해 높이 제약조건 업데이트
self.tableView.snp.updateConstraints { make in
make.height.equalTo(newSize.height)
}
-
- // 레이아웃 업데이트 강제 실행
+ // 레이아웃 강제 업데이트
self.setNeedsLayout()
self.layoutIfNeeded()
-
- // 업데이트 후 실제 테이블뷰의 높이 출력
- //print("디버그: 업데이트 후 tableView의 frame.height: \(self.tableView.frame.height), 시각: \(Date())")
+ // 디버그 출력을 원할 경우 주석 해제하여 사용 가능
+ // print("디버그: tableView의 contentSize 변경됨: \(change.oldValue ?? .zero) -> \(newSize), 시각: \(Date())")
+ // print("디버그: 업데이트 후 tableView의 frame.height: \(self.tableView.frame.height), 시각: \(Date())")
}
}
deinit {
contentSizeObservation?.invalidate()
}
-
}
-
-
-
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailViewCell.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailViewCell.swift
index faf7dda..3e0fcb7 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailViewCell.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/View/RecordDetailViewCell.swift
@@ -14,23 +14,28 @@ final class RecordDetailViewCell: UITableViewCell {
static let identifier = "RecordDetailCell"
// MARK: - UI Components 선언
+ /// 시간 정보를 표시하는 라벨
private let timeLabel = UILabel().then {
$0.font = .RLHeadline2
$0.textColor = .LightOrange
$0.textAlignment = .left
}
+
+ /// 운동거리 정보를 표시하는 라벨
private let distanceLabel = UILabel().then {
$0.font = .RLHeadline2
$0.textColor = .LightPink
$0.textAlignment = .left
}
+
+ /// 걸음수 정보를 표시하는 라벨
private let stepsLabel = UILabel().then {
$0.font = .RLHeadline2
$0.textColor = .LightBlue
$0.textAlignment = .left
}
- // 수평 스택뷰로 3개 라벨 배치
+ /// 3개 라벨을 수평으로 배치하는 스택뷰
private lazy var horizontalStack = UIStackView(arrangedSubviews: [timeLabel, distanceLabel, stepsLabel]).then {
$0.axis = .horizontal
$0.alignment = .leading
@@ -41,8 +46,8 @@ final class RecordDetailViewCell: UITableViewCell {
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
- setupUI()
- setupLayout()
+ setupUI() // UI 요소들을 contentView에 추가
+ setupLayout() // SnapKit 레이아웃 제약조건 설정
}
required init?(coder: NSCoder) {
@@ -50,23 +55,23 @@ final class RecordDetailViewCell: UITableViewCell {
}
// MARK: - Setup UI
+ /// contentView의 배경색을 설정하고, 수평 스택뷰를 추가하는 메서드
private func setupUI() {
- // UI 요소 추가
contentView.backgroundColor = .Gray900
contentView.addSubview(horizontalStack)
- // 선택 스타일 제거
+ // 셀 선택 시 배경색 변화 없도록 설정
selectionStyle = .none
}
// MARK: - Setup Layout
+ /// 수평 스택뷰와 각 라벨의 제약조건을 SnapKit을 이용해 설정하는 메서드
private func setupLayout() {
- // 레이아웃 설정
horizontalStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(DynamicSize.scaledSize(8))
}
- // 라벨별로 width 비율 고정 (예: 0.4 : 0.3 : 0.3)
+ // 각 라벨의 너비를 전체 스택뷰 너비의 비율로 고정 (0.334, 0.333, 0.333)
timeLabel.snp.makeConstraints { make in
make.width.equalTo(horizontalStack.snp.width).multipliedBy(0.334)
}
@@ -79,26 +84,7 @@ final class RecordDetailViewCell: UITableViewCell {
}
// MARK: - Configure
- func configureAsHeader() {
- print("헤더 셀 구성 호출됨")
- // 뷰 설정
- timeLabel.font = .RLBody1
- timeLabel.textColor = .Gray000
- timeLabel.textAlignment = .left
- timeLabel.text = "시간대"
-
- distanceLabel.font = .RLBody1
- distanceLabel.textColor = .Gray000
- distanceLabel.textAlignment = .left
- distanceLabel.text = "운동거리"
-
- stepsLabel.font = .RLBody1
- stepsLabel.textColor = .Gray000
- stepsLabel.textAlignment = .left
- stepsLabel.text = "걸음수"
- }
-
- /// 폰트를 외부에서 지정할 수 있도록 수정한 configure 메서드
+ /// 레코드 데이터를 받아서 라벨에 설정하고, 폰트를 외부에서 지정할 수 있도록 한 메서드
func configure(with record: RecordDetail, font: UIFont) {
timeLabel.font = font
timeLabel.textColor = .LightOrange
@@ -116,10 +102,8 @@ final class RecordDetailViewCell: UITableViewCell {
stepsLabel.text = record.steps
}
- // 기존 메서드 유지 (디폴트로 headline2 적용)
+ /// 기본적으로 RLHeadline2 폰트를 적용하여 레코드 데이터를 설정하는 메서드
func configure(with record: RecordDetail) {
configure(with: record, font: .RLHeadline2)
}
}
-
-
diff --git a/RunLog/RunLog/Sources/Presentation/DetailLog/ViewModel/DetailLogViewModel.swift b/RunLog/RunLog/Sources/Presentation/DetailLog/ViewModel/DetailLogViewModel.swift
index cf368fa..add7027 100644
--- a/RunLog/RunLog/Sources/Presentation/DetailLog/ViewModel/DetailLogViewModel.swift
+++ b/RunLog/RunLog/Sources/Presentation/DetailLog/ViewModel/DetailLogViewModel.swift
@@ -12,9 +12,9 @@ import Combine
final class DetailLogViewModel {
// MARK: - Properties
+ /// 선택된 날짜
let date: Date
-
// MARK: - Input & Output
enum Input {
case menuSelected(String)
@@ -27,25 +27,28 @@ final class DetailLogViewModel {
case delete
}
+ /// 사용자 입력을 받기 위한 subject
let input = PassthroughSubject()
+ /// 뷰모델 출력 이벤트를 전달하기 위한 subject (초기값은 nil)
let output = CurrentValueSubject