Feature/mission result view#6#21
Conversation
# Conflicts: # RocketCall/View/ViewController.swift
There was a problem hiding this comment.
Code Review
This pull request introduces a new Mission Result View, comprising MissionResultView, MissionResultViewController, InfoPairView, and StateRowView, designed to display mission outcomes and details. It also extends LabelConfiguration and StateLabelConfiguration with new static properties and refactors AlarmRingView to manage animations more robustly across app lifecycle events, including a rename of AlarmImageView. Key feedback points include addressing a type mismatch in MissionResultView's configuration, refactoring duplicated LabelConfiguration static properties into factory methods for better maintainability, improving testability in AlarmRingView by injecting NotificationCenter, correcting naming convention violations and typos (e.g., SeperaterView) in the new Mission Result View components, and implementing more robust error handling for CoreData fetches in MissionResultViewController instead of using try?.
| let SuccessData = MissionResultData( | ||
| state: true, | ||
| missionName: "성공 미션", | ||
| route: "지구 → 달", | ||
| duration: "00:14", | ||
| type: "계획된 미션", | ||
| statusText: "완료", | ||
| completedDate: "2026. 3. 20. 오후 5:41:08" | ||
| ) | ||
|
|
||
| let failData = MissionResultData( | ||
| state: false, | ||
| missionName: "실패 미션", | ||
| route: "지구 → 화성", | ||
| duration: "00:08", | ||
| type: "즉시 미션", | ||
| statusText: "실패", | ||
| completedDate: "2026. 3. 25. 오후 9:10:00" | ||
| ) |
There was a problem hiding this comment.
MissionResultView.configure(with:) 메서드는 MissionResultPayload를 인자로 받지만, 여기서는 정의되지 않은 MissionResultData 타입을 사용하고 있습니다. 컴파일 오류를 수정하기 위해 MissionResultPayload 타입의 목(mock) 데이터를 생성해야 합니다.
let successPayload = MissionResultPayload(
id: UUID(),
title: "성공 미션",
start: Date().addingTimeInterval(-3600),
end: Date(),
studyTime: 50,
isCompleted: true
)
let failPayload = MissionResultPayload(
id: UUID(),
title: "실패 미션",
start: Date().addingTimeInterval(-1800),
end: Date(),
studyTime: 20,
isCompleted: false
)| test.snp.makeConstraints { | ||
| $0.edges.equalTo(view.safeAreaLayoutGuide) | ||
| } | ||
| test.configure(with: SuccessData) |
| extension LabelConfiguration { | ||
|
|
||
| // 크기: 12~18, 색: subLabel, weight: medium | ||
| static let sub12 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 12, weight: .medium), | ||
| color: .subLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let sub14 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 14, weight: .medium), | ||
| color: .subLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let sub16 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 16, weight: .medium), | ||
| color: .subLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let sub18 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 18, weight: .medium), | ||
| color: .subLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| // 크기: 18~30, 색: mainLabel, weight: medium/bold | ||
| static let main18 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 18, weight: .medium), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main20 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 20, weight: .medium), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main22 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 22, weight: .medium), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main24 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 24, weight: .medium), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main30 = LabelConfiguration( | ||
| font: .systemFont(ofSize: 30, weight: .medium), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main18Bold = LabelConfiguration( | ||
| font: .systemFont(ofSize: 18, weight: .bold), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main20Bold = LabelConfiguration( | ||
| font: .systemFont(ofSize: 20, weight: .bold), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main22Bold = LabelConfiguration( | ||
| font: .systemFont(ofSize: 22, weight: .bold), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main24Bold = LabelConfiguration( | ||
| font: .systemFont(ofSize: 24, weight: .bold), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
|
|
||
| static let main30Bold = LabelConfiguration( | ||
| font: .systemFont(ofSize: 30, weight: .bold), | ||
| color: .mainLabel, | ||
| lines: 1 | ||
| ) | ||
| } |
There was a problem hiding this comment.
LabelConfiguration을 생성하기 위해 많은 수의 정적 프로퍼티가 선언되어 있어 코드 중복이 많고 유지보수가 어렵습니다.
이를 개선하기 위해 정적 팩토리 메서드를 사용하는 것을 제안합니다. 이렇게 하면 코드의 양을 크게 줄이고 유연성을 높일 수 있습니다.
사용 예시:
let subLabel = UILabel(config: .sub(size: 12))
let mainLabel = UILabel(config: .main(size: 24, weight: .bold))extension LabelConfiguration {
static func sub(size: CGFloat) -> LabelConfiguration {
return LabelConfiguration(
font: .systemFont(ofSize: size, weight: .medium),
color: .subLabel,
lines: 1
)
}
static func main(size: CGFloat, weight: UIFont.Weight = .medium) -> LabelConfiguration {
return LabelConfiguration(
font: .systemFont(ofSize: size, weight: weight),
color: .mainLabel,
lines: 1
)
}
}There was a problem hiding this comment.
미리 만들어진 config는 최소한의 것만 남기고 함수 추가완료
There was a problem hiding this comment.
extension LabelConfiguration {
static let sub12 = LabelConfiguration(
font: .systemFont(ofSize: 12, weight: .medium),
color: .subLabel,
lines: 1
)
static let sub14 = LabelConfiguration(
font: .systemFont(ofSize: 14, weight: .medium),
color: .subLabel,
lines: 1
)
static let sub16 = LabelConfiguration(
font: .systemFont(ofSize: 16, weight: .medium),
color: .subLabel,
lines: 1
)
static let main24 = LabelConfiguration(
font: .systemFont(ofSize: 24, weight: .medium),
color: .mainLabel,
lines: 1
)
static let main30 = LabelConfiguration(
font: .systemFont(ofSize: 30, weight: .medium),
color: .mainLabel,
lines: 1
)
static let main24Bold = LabelConfiguration(
font: .systemFont(ofSize: 24, weight: .bold),
color: .mainLabel,
lines: 1
)
static let main30Bold = LabelConfiguration(
font: .systemFont(ofSize: 30, weight: .bold),
color: .mainLabel,
lines: 1
)
}
/*
사용예시
let subLabel = UILabel(config: .sub(size: 12))
let mainLabel = UILabel(config: .main(size: 24, weight: .bold))
*/
extension LabelConfiguration {
static func sub(size: CGFloat) -> LabelConfiguration {
return LabelConfiguration(
font: .systemFont(ofSize: size, weight: .medium),
color: .subLabel,
lines: 1
)
}
static func main(size: CGFloat, weight: UIFont.Weight = .medium) -> LabelConfiguration {
return LabelConfiguration(
font: .systemFont(ofSize: size, weight: weight),
color: .mainLabel,
lines: 1
)
}
}
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(handleDidBecomeActive), | ||
| name: UIApplication.didBecomeActiveNotification, | ||
| object: nil | ||
| ) |
There was a problem hiding this comment.
NotificationCenter.default를 직접 사용하면 뷰와 NotificationCenter 사이에 강한 결합이 생겨 단위 테스트가 어려워집니다. 의존성 주입(Dependency Injection)을 통해 테스트 용이성을 높이는 것을 권장합니다.
init을 통해 NotificationCenter 인스턴스를 주입받도록 수정하면, 테스트 시에는 mock 객체를 주입하여 뷰의 동작을 독립적으로 검증할 수 있습니다.
// 수정 제안
private let notificationCenter: NotificationCenter
init(
time: String,
date: String,
title: String,
notificationCenter: NotificationCenter = .default
) {
self.time = time
self.date = date
self.title = title
self.notificationCenter = notificationCenter
super.init(frame: .zero)
// ...
// self.notificationCenter 사용
notificationCenter.addObserver(...)
}| private let Seperater1 = SeperaterView() | ||
| private let Seperater2 = SeperaterView() | ||
| private let Seperater3 = SeperaterView() | ||
| private let Seperater4 = SeperaterView() | ||
| private let Seperater5 = SeperaterView() |
There was a problem hiding this comment.
프로퍼티 이름이 Swift API 디자인 가이드라인의 네이밍 컨벤션(lowerCamelCase)을 따르지 않습니다. Seperater1 대신 separator1과 같이 소문자로 시작해야 합니다. 또한, 클래스 이름에 'Seperater'라는 오타가 있으니 함께 수정하는 것을 권장합니다 (315라인 참고).
| private let Seperater1 = SeperaterView() | |
| private let Seperater2 = SeperaterView() | |
| private let Seperater3 = SeperaterView() | |
| private let Seperater4 = SeperaterView() | |
| private let Seperater5 = SeperaterView() | |
| private let separator1 = SeperaterView() | |
| private let separator2 = SeperaterView() | |
| private let separator3 = SeperaterView() | |
| private let separator4 = SeperaterView() | |
| private let separator5 = SeperaterView() |
| } | ||
| } | ||
|
|
||
| class SeperaterView: UIView { |
| if let resultId, | ||
| let payload = try? coreDataManager.fetchMissionResult(of: resultId) { | ||
| missionResultView.configure(with: payload) | ||
| } else { | ||
| missionResultView.configure(with: samplePayload) | ||
| } |
There was a problem hiding this comment.
try?를 사용하여 CoreData fetch 중 발생하는 오류를 무시하고 있습니다. 이는 데이터 로드 실패 시 원인을 파악하기 어렵게 만듭니다.
do-catch 구문을 사용하여 오류를 명시적으로 처리하는 것이 좋습니다. 예를 들어, 오류를 로깅하거나 사용자에게 알림을 표시할 수 있습니다.
if let resultId {
do {
let payload = try coreDataManager.fetchMissionResult(of: resultId)
missionResultView.configure(with: payload)
} catch {
// TODO: 오류 처리 로직 구현 (예: 로깅, 사용자에게 알림)
print("Failed to fetch mission result: \(error)")
missionResultView.configure(with: samplePayload) // fallback
}
} else {
missionResultView.configure(with: samplePayload)
}There was a problem hiding this comment.
오류 시 처리 할 수 있게 수정 완료
devBambu
left a comment
There was a problem hiding this comment.
LabelConfiguration 확인했고, 저는 이상 없는 것으로 보입니다. 고생하셨습니다!
| Separator1.snp.makeConstraints { | ||
| $0.top.equalTo(durationRow.snp.bottom).offset(15) | ||
| $0.leading.trailing.equalToSuperview().inset(30) | ||
| $0.height.equalTo(1) | ||
| } |
There was a problem hiding this comment.
Separator마다 $0.height.equalTo(1)로 설정해주고 있는데, SepearatorView를 생성할 때 height를 1로 설정해서 생성해주면 중복 코드를 줄일 수 있지 않을까 싶습니다!
🎫 관련 이슈 (Linked Issues)
Closes #6
🛠 작업 내용 (What I did)
MissionResultView는 미션 수행 결과를 보여주는 결과 화면용 커스텀 UIView
미션 이름, 총 소요 시간, 집중 시간, 시작 시간, 끝난 시간, 완료 일시, 성공/실패 상태를 카드 형태로 보여주고, 상단 헤더에서는 결과에 따라 아이콘과 문구가 바뀌도록 구성되어 있습니다.
Label config, StateLabel config 추가했습니다.
subLabel 크기: 12~18, weight: medium
mainLabel 크기: 18~30, weight: medium/bold
StateLabel - 성공, 실패
💻 구현 상세 (Implementation Details)
📸 스크린샷 (Screenshots)
🧐 리뷰 포인트 (Review Points)
✅ 자가 점검 (Self Checklist)