From af072b9e99d649c9427f1a73ed517854c66a4a8b Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 19:40:28 +0900 Subject: [PATCH 01/27] =?UTF-8?q?FEAT:=20remainingTimeText=EC=97=90=2012?= =?UTF-8?q?=EC=8B=9C=EA=B0=84/30=EC=9D=BC/365=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=EB=B0=8F=20=EC=A7=80=EB=82=9C=20=EC=95=BD=EC=86=8D?= =?UTF-8?q?=20nil=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extension/Date+RemainingTime.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift index 0b85c14..66399c6 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift @@ -8,17 +8,30 @@ import Foundation // MARK: - 현재 시각 기준 남은 시간을 화면에 표시할 문자열로 변환 -// - 순수 포맷 변환이 아니라 "지금"을 참조하는 프레젠테이션 로직이라 Core가 아닌 Presentation에 위치 +// - 약속 시간이 지났다면 nil을 반환해 "남은 시간 표시 안 함" 규칙을 표현 +// - 경계 판정과 단위 숫자 계산 모두 시간(hour) 차이 하나만 기준으로 함 (달력 월/년 컴포넌트 미사용) +// - 12시간 이하 → 시간 단위 +// - 30일(24*30시간) 이하 → 일 단위 +// - 365일(24*365시간) 이하 → 개월 단위 (30일 = 1개월로 근사) +// - 그 외 → 연 단위 (365일 = 1년으로 근사) extension Date { - var remainingTimeText: String { + var remainingTimeText: String? { let hours = Calendar.current.dateComponents([.hour], from: Date(), to: self).hour ?? 0 - if hours >= 24 { + guard hours >= 0 else { return nil } + + if hours <= 12 { + return "\(hours)시간 남음" + } + if hours <= 24 * 30 { return "\(hours / 24)일 남음" } - return "\(hours)시간 남음" + if hours <= 24 * 365 { + return "\(hours / 24 / 30)개월 남음" + } + return "\(hours / 24 / 365)년 남음" } } From d8fe8f287d8e8b4f9eafe8af864f121e77679f8b Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 19:46:08 +0900 Subject: [PATCH 02/27] =?UTF-8?q?FEAT:=20=EC=95=BD=EC=86=8D=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=EA=B0=80=20=EC=98=AC=ED=95=B4=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EB=A9=B4=20=EC=97=B0=EB=8F=84=EB=A5=BC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=ED=95=98=EB=8A=94=20appointmentDateTimeText=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WhereAreYou/Core/Date+Format.swift | 11 +++++++++ .../Extension/Date+AppointmentDisplay.swift | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/Extension/Date+AppointmentDisplay.swift diff --git a/WhereAreYou/WhereAreYou/Core/Date+Format.swift b/WhereAreYou/WhereAreYou/Core/Date+Format.swift index 9b7f697..478baae 100644 --- a/WhereAreYou/WhereAreYou/Core/Date+Format.swift +++ b/WhereAreYou/WhereAreYou/Core/Date+Format.swift @@ -17,6 +17,10 @@ extension Date { Self.koreanDateTimeFormatter.string(from: self) } + var monthDayTimeString: String { + Self.monthDayTimeFormatter.string(from: self) + } + private static let koreanDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale(identifier: "ko_KR") @@ -31,4 +35,11 @@ extension Date { return formatter }() + private static let monthDayTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "MM월 dd일 HH:mm" + return formatter + }() + } diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/Date+AppointmentDisplay.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+AppointmentDisplay.swift new file mode 100644 index 0000000..eb5868f --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+AppointmentDisplay.swift @@ -0,0 +1,23 @@ +// +// Date+AppointmentDisplay.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import Foundation + +// MARK: - 약속 날짜/시간을 화면에 표시할 문자열로 변환 +// - 올해면 연도를 생략하고, 올해가 아니면 연도를 포함해 표시 + +extension Date { + + var appointmentDateTimeText: String { + isThisYear ? monthDayTimeString : koreanDateTimeString + } + + private var isThisYear: Bool { + Calendar.current.component(.year, from: self) == Calendar.current.component(.year, from: Date()) + } + +} From 91910eb08c2e3b5031d89a7ebe1cb381a5c24ed5 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 19:48:06 +0900 Subject: [PATCH 03/27] =?UTF-8?q?FIX:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=95=BD=EC=86=8D=20=EC=B9=B4=EB=93=9C=EB=8F=84=20=EC=98=AC?= =?UTF-8?q?=ED=95=B4=EA=B0=80=20=EC=95=84=EB=8B=90=20=EB=95=8C=EB=A7=8C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=84=EB=A5=BC=20=ED=91=9C=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift index edfb5e6..2539f6e 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift @@ -94,7 +94,7 @@ final class UpcomingAppointmentCard: UIView { remainingTimeLabel.text = appointment.date?.remainingTimeText participantRow.text = "\(appointment.participantCount)명" placeRow.text = appointment.location?.title ?? "미정" - dateRow.text = appointment.date?.koreanDateTimeString ?? "미정" + dateRow.text = appointment.date?.appointmentDateTimeText ?? "미정" } @objc private func cardTapped() { From e9f9594f570a335a227941b6fe93bfc6233e7ca1 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 19:52:08 +0900 Subject: [PATCH 04/27] =?UTF-8?q?FEAT:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=EC=9A=A9=20AppointmentListItem?= =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListItem.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift new file mode 100644 index 0000000..1913209 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift @@ -0,0 +1,19 @@ +// +// AppointmentListItem.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import Foundation + +// MARK: - 약속 목록 / 지난 약속 화면의 카드 하나에 대응하는 표시 전용 모델 + +struct AppointmentListItem { + let id: String + let title: String + let participantCount: Int + let location: AppointmentLocation? + let date: Date? + let isNotificationEnabled: Bool +} From a97ec102c2d473f9a08ae92ea609416d77d0fc81 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:13:09 +0900 Subject: [PATCH 05/27] =?UTF-8?q?REFACTOR:=20=EC=95=BD=EC=86=8D=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20AppointmentCardBase=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20AppointmentListCard=20=EC=8B=A0=EA=B7=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 62 ++++++++++++ .../Component/AppointmentCardBase.swift | 92 +++++++++++++++++ .../Home/UpcomingAppointmentCard.swift | 98 +++++-------------- 3 files changed, 177 insertions(+), 75 deletions(-) create mode 100644 WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift create mode 100644 WhereAreYou/WhereAreYou/Presentation/Component/AppointmentCardBase.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift new file mode 100644 index 0000000..7d693a1 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -0,0 +1,62 @@ +// +// AppointmentListCard.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import UIKit + +// MARK: - 약속 목록 / 지난 약속 화면의 약속 카드 +// - 제목 + 인원/장소/날짜 3줄 + 대화 열기·지도 열기 버튼으로 구성 +// - 짧게 탭했을 땐 카드 자체는 반응하지 않고 대화/지도 버튼만 반응 +// - 꾹 눌렀을 땐 카드 전체가 반응해 컨텍스트 메뉴(알림 토글/약속 나가기)를 표시 +// - 오른쪽 상단 액세서리(남은 시간 라벨 / 휴지통 버튼 등)는 외부에서 주입 + +final class AppointmentListCard: AppointmentCardBase { + + var onChatTap: (() -> Void)? + var onMapTap: (() -> Void)? + + private lazy var chatButton = UIButton.filled( + title: "대화 열기", background: .systemGray6, tint: .label, font: .preferredFont(forTextStyle: .footnote) + ) + private lazy var mapButton = UIButton.filled( + title: "지도 열기", background: .systemGray6, tint: .label, font: .preferredFont(forTextStyle: .footnote) + ) + + init(item: AppointmentListItem, accessoryView: UIView?) { + super.init( + participantCount: item.participantCount, + placeText: item.location?.title ?? "미정", + dateText: item.date?.appointmentDateTimeText ?? "미정", + title: item.title, + accessoryView: accessoryView + ) + setUpButtons() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented — use init(item:accessoryView:)") + } + + private func setUpButtons() { + let buttonStack = UIStackView(arrangedSubviews: [chatButton, mapButton]) + buttonStack.axis = .horizontal + buttonStack.spacing = 8 + buttonStack.distribution = .fillEqually + buttonStack.translatesAutoresizingMaskIntoConstraints = false + addSubview(buttonStack) + + NSLayoutConstraint.activate([ + buttonStack.topAnchor.constraint(equalTo: rowStack.bottomAnchor, constant: 16), + buttonStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), + buttonStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), + buttonStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14) + ]) + + chatButton.addAction(UIAction { [weak self] _ in self?.onChatTap?() }, for: .touchUpInside) + mapButton.addAction(UIAction { [weak self] _ in self?.onMapTap?() }, for: .touchUpInside) + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentCardBase.swift b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentCardBase.swift new file mode 100644 index 0000000..51f9d35 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentCardBase.swift @@ -0,0 +1,92 @@ +// +// AppointmentCardBase.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import UIKit + +// MARK: - 약속 카드의 공통 뼈대 +// - 배경/그림자/코너radius + 제목 + 인원/장소/날짜 3줄을 담당 +// - 제목 오른쪽 액세서리(남은 시간 라벨, 휴지통 버튼 등)는 하위 클래스가 넘겨줌 (없으면 nil) +// - 하위 클래스는 rowStack.bottomAnchor 아래에 자신만의 콘텐츠(버튼, 탭 제스처 등)를 추가 + +class AppointmentCardBase: UIView { + + let rowStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 3 + return stack + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .headline) + label.lineBreakMode = .byTruncatingTail + label.numberOfLines = 1 + return label + }() + + private let participantRow = IconLabel(iconName: "person.2.fill") + private let placeRow = IconLabel(iconName: "location.fill") + private let dateRow = IconLabel(iconName: "calendar.badge.clock") + + init(participantCount: Int, placeText: String, dateText: String, title: String, accessoryView: UIView?) { + super.init(frame: .zero) + setUp(accessoryView: accessoryView) + configure(title: title, participantCount: participantCount, placeText: placeText, dateText: dateText) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUp(accessoryView: UIView?) { + backgroundColor = .white + layer.cornerRadius = 20 + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.25 + layer.shadowRadius = 2 + layer.shadowOffset = CGSize(width: 0, height: 4) + + [participantRow, placeRow, dateRow].forEach { rowStack.addArrangedSubview($0) } + + [titleLabel, rowStack].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + addSubview($0) + } + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), + + rowStack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + rowStack.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: 6), + rowStack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -14) + ]) + + setUpAccessoryViewIfNeeded(accessoryView) + } + + private func setUpAccessoryViewIfNeeded(_ accessoryView: UIView?) { + guard let accessoryView else { return } + accessoryView.translatesAutoresizingMaskIntoConstraints = false + addSubview(accessoryView) + + NSLayoutConstraint.activate([ + accessoryView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + accessoryView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: accessoryView.leadingAnchor, constant: -8) + ]) + } + + private func configure(title: String, participantCount: Int, placeText: String, dateText: String) { + titleLabel.text = title + participantRow.text = "\(participantCount)명" + placeRow.text = placeText + dateRow.text = dateText + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift index 2539f6e..f8bf36b 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift @@ -11,90 +11,38 @@ import UIKit // - 제목 + 남은 시간(옵셔널) + 인원/장소/날짜 3줄로 구성 // - 카드 자체가 탭 가능 -final class UpcomingAppointmentCard: UIView { +final class UpcomingAppointmentCard: AppointmentCardBase { var onTap: (() -> Void)? - private let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFont.preferredFont(forTextStyle: .headline) - label.lineBreakMode = .byTruncatingTail - label.numberOfLines = 1 - return label - }() - - private let remainingTimeLabel: UILabel = { - let label = UILabel() - label.font = .boldPreferredFont(forTextStyle: .footnote) - label.textColor = .blue2 - label.adjustsFontForContentSizeCategory = true - label.setContentHuggingPriority(.required, for: .horizontal) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - return label - }() - - private let participantRow = IconLabel(iconName: "person.2.fill") - private let placeRow = IconLabel(iconName: "location.fill") - private let dateRow = IconLabel(iconName: "calendar.badge.clock") - - private lazy var rowStack: UIStackView = { - let stack = UIStackView(arrangedSubviews: [participantRow, placeRow, dateRow]) - stack.axis = .vertical - stack.spacing = 3 - return stack - }() - init(appointment: UpcomingAppointment) { - super.init(frame: .zero) - setUp() - configure(with: appointment) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented — use init(appointment:)") - } - - private func setUp() { - backgroundColor = .white - layer.cornerRadius = 20 - layer.shadowColor = UIColor.black.cgColor - layer.shadowOpacity = 0.25 - layer.shadowRadius = 2 - layer.shadowOffset = CGSize(width: 0, height: 4) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - remainingTimeLabel.translatesAutoresizingMaskIntoConstraints = false - rowStack.translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - addSubview(remainingTimeLabel) - addSubview(rowStack) - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), - titleLabel.trailingAnchor.constraint( - lessThanOrEqualTo: remainingTimeLabel.leadingAnchor, constant: -8 - ), - - remainingTimeLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - remainingTimeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), - - rowStack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), - rowStack.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: 6), - rowStack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -14), - rowStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14) - ]) + let remainingTimeLabel: UILabel = { + let label = UILabel() + label.font = .boldPreferredFont(forTextStyle: .footnote) + label.textColor = .blue2 + label.adjustsFontForContentSizeCategory = true + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.text = appointment.date?.remainingTimeText + return label + }() + + super.init( + participantCount: appointment.participantCount, + placeText: appointment.location?.title ?? "미정", + dateText: appointment.date?.appointmentDateTimeText ?? "미정", + title: appointment.title, + accessoryView: remainingTimeLabel + ) + + rowStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14).isActive = true isUserInteractionEnabled = true addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cardTapped))) } - private func configure(with appointment: UpcomingAppointment) { - titleLabel.text = appointment.title - remainingTimeLabel.text = appointment.date?.remainingTimeText - participantRow.text = "\(appointment.participantCount)명" - placeRow.text = appointment.location?.title ?? "미정" - dateRow.text = appointment.date?.appointmentDateTimeText ?? "미정" + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented — use init(appointment:)") } @objc private func cardTapped() { From de814696e1c27017038fbe824e4ade3c24da465a Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:20:25 +0900 Subject: [PATCH 06/27] =?UTF-8?q?FEAT:=20AppointmentListCard=EC=97=90=20?= =?UTF-8?q?=EA=BE=B9=20=EB=88=8C=EB=9F=AC=20=EC=95=8C=EB=A6=BC=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80/=EC=95=BD=EC=86=8D=20=EB=82=98=EA=B0=80=EA=B8=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index 7d693a1..c11c218 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -17,6 +17,10 @@ final class AppointmentListCard: AppointmentCardBase { var onChatTap: (() -> Void)? var onMapTap: (() -> Void)? + var onToggleNotification: (() -> Void)? + var onLeave: (() -> Void)? + + private var isNotificationEnabled: Bool private lazy var chatButton = UIButton.filled( title: "대화 열기", background: .systemGray6, tint: .label, font: .preferredFont(forTextStyle: .footnote) @@ -26,6 +30,8 @@ final class AppointmentListCard: AppointmentCardBase { ) init(item: AppointmentListItem, accessoryView: UIView?) { + isNotificationEnabled = item.isNotificationEnabled + super.init( participantCount: item.participantCount, placeText: item.location?.title ?? "미정", @@ -34,6 +40,7 @@ final class AppointmentListCard: AppointmentCardBase { accessoryView: accessoryView ) setUpButtons() + setUpContextMenu() } required init?(coder: NSCoder) { @@ -59,4 +66,43 @@ final class AppointmentListCard: AppointmentCardBase { mapButton.addAction(UIAction { [weak self] _ in self?.onMapTap?() }, for: .touchUpInside) } + private func setUpContextMenu() { + addInteraction(UIContextMenuInteraction(delegate: self)) + } + + private func makeContextMenu() -> UIMenu { + let notificationTitle = isNotificationEnabled ? "알림 끄기" : "알림 켜기" + let notificationIcon = isNotificationEnabled ? "bell.slash" : "bell" + + let toggleNotificationAction = UIAction( + title: notificationTitle, + image: UIImage(systemName: notificationIcon) + ) { [weak self] _ in + self?.onToggleNotification?() + } + + let leaveAction = UIAction( + title: "약속 나가기", + image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), + attributes: .destructive + ) { [weak self] _ in + self?.onLeave?() + } + + return UIMenu(children: [toggleNotificationAction, leaveAction]) + } + +} + +// MARK: - UIContextMenuInteractionDelegate + +extension AppointmentListCard: UIContextMenuInteractionDelegate { + + func contextMenuInteraction( + _ interaction: UIContextMenuInteraction, + configurationForMenuAtLocation location: CGPoint + ) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration(actionProvider: { [weak self] _ in self?.makeContextMenu() }) + } + } From 302b4ad86ab3ecf49d9e6c71efe66bc3cd088416 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:23:26 +0900 Subject: [PATCH 07/27] =?UTF-8?q?FEAT:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=EC=9A=A9=20AppointmentListViewMo?= =?UTF-8?q?del=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewModel.swift | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift new file mode 100644 index 0000000..02af7bc --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -0,0 +1,101 @@ +// +// AppointmentListViewModel.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import Foundation + +// MARK: - 약속 목록 화면(탭)의 데이터 +// - "오늘 약속"은 date가 오늘 날짜(같은 day)인 약속, "예정된 약속"은 그 외 미래 약속 +// - 오늘 약속이 없으면 todayAppointments가 빈 배열이 되고, 화면은 헤더 없이 예정된 약속만 표시 + +final class AppointmentListViewModel { + + private(set) var todayAppointments: [AppointmentListItem] = [] + private(set) var upcomingAppointments: [AppointmentListItem] = [] + + init() { + loadDummyData() + } + + @discardableResult + func toggleNotification(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { + todayAppointments = todayAppointments.map { toggledIfMatching(id: id, item: $0) } + upcomingAppointments = upcomingAppointments.map { toggledIfMatching(id: id, item: $0) } + return (todayAppointments, upcomingAppointments) + } + + @discardableResult + func leave(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { + todayAppointments.removeAll { $0.id == id } + upcomingAppointments.removeAll { $0.id == id } + return (todayAppointments, upcomingAppointments) + } + + private func toggledIfMatching(id: String, item: AppointmentListItem) -> AppointmentListItem { + guard item.id == id else { return item } + return AppointmentListItem( + id: item.id, + title: item.title, + participantCount: item.participantCount, + location: item.location, + date: item.date, + isNotificationEnabled: !item.isNotificationEnabled + ) + } + + private func loadDummyData() { + let allAppointments = [ + AppointmentListItem( + id: "1", + title: "고등학교 친구들과 저녁", + participantCount: 5, + location: AppointmentLocation( + title: "고기굽는방앗간 이수역점", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .hour, value: 10, to: Date()), + isNotificationEnabled: true + ), + AppointmentListItem( + id: "2", + title: "고등학교 친구들과 저녁", + participantCount: 5, + location: AppointmentLocation( + title: "고기굽는방앗간 이수역점", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .day, value: 1, to: Date()), + isNotificationEnabled: true + ), + AppointmentListItem( + id: "3", + title: "고등학교 친구들과 저녁", + participantCount: 5, + location: AppointmentLocation( + title: "고기굽는방앗간 이수역점", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .day, value: 365, to: Date()), + isNotificationEnabled: false + ) + ] + + let (today, upcoming) = allAppointments.reduce(into: ([AppointmentListItem](), [AppointmentListItem]())) { result, item in + if let date = item.date, Calendar.current.isDateInToday(date) { + result.0.append(item) + } else { + result.1.append(item) + } + } + + todayAppointments = today.sorted { ($0.date ?? .distantFuture) < ($1.date ?? .distantFuture) } + upcomingAppointments = upcoming.sorted { ($0.date ?? .distantFuture) < ($1.date ?? .distantFuture) } + } + +} From cdb26de67f1a9c09409456288390b0fdfc646086 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:26:44 +0900 Subject: [PATCH 08/27] =?UTF-8?q?FEAT:=20AppointmentListViewController=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B0=8F=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98/=EC=98=88=EC=A0=95=20=EC=95=BD=EC=86=8D=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=91=9C=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift new file mode 100644 index 0000000..8aa85fb --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -0,0 +1,175 @@ +// +// AppointmentListViewController.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import UIKit + +final class AppointmentListViewController: UIViewController { + + private static let cardSpacing: CGFloat = 16 + + private let viewModel = AppointmentListViewModel() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "약속 목록" + label.font = .boldPreferredFont(forTextStyle: .title1) + return label + }() + + private let scrollView = UIScrollView() + + private lazy var contentStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = Self.cardSpacing + return stack + }() + + private let emptyLabel: UILabel = { + let label = UILabel() + label.text = "약속이 없습니다." + label.font = .preferredFont(forTextStyle: .body) + label.textColor = .secondaryLabel + label.textAlignment = .center + label.isHidden = true + return label + }() + + private let pastAppointmentButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(systemName: "arrow.uturn.backward.circle.fill"), for: .normal) + button.tintColor = .blue2 + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + setUpLayout() + setUpActions() + reloadCards() + } + + private func setUpLayout() { + [titleLabel, scrollView, emptyLabel, pastAppointmentButton].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + contentStack.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentStack) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), + + scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 24), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -24), + contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -48), + + emptyLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + + pastAppointmentButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24), + pastAppointmentButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24), + pastAppointmentButton.widthAnchor.constraint(equalToConstant: 48), + pastAppointmentButton.heightAnchor.constraint(equalToConstant: 48) + ]) + } + + private func setUpActions() { + pastAppointmentButton.addAction(UIAction { [weak self] _ in + self?.presentPastAppointmentList() + }, for: .touchUpInside) + } + + private func reloadCards() { + contentStack.arrangedSubviews.forEach { + contentStack.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + let today = viewModel.todayAppointments + let upcoming = viewModel.upcomingAppointments + let hasAnyAppointment = !today.isEmpty || !upcoming.isEmpty + + emptyLabel.isHidden = hasAnyAppointment + scrollView.isHidden = !hasAnyAppointment + + if !today.isEmpty { + addSectionHeaderIfNeeded(title: "오늘 약속", isNeeded: !upcoming.isEmpty) + today.forEach { addCard(for: $0) } + } + + if !upcoming.isEmpty { + addSectionHeaderIfNeeded(title: "예정된 약속", isNeeded: !today.isEmpty) + upcoming.forEach { addCard(for: $0) } + } + } + + private func addSectionHeaderIfNeeded(title: String, isNeeded: Bool) { + guard isNeeded else { return } + let label = UILabel() + label.text = title + label.font = .preferredFont(forTextStyle: .callout) + label.textColor = .secondaryLabel + contentStack.addArrangedSubview(label) + } + + private func addCard(for item: AppointmentListItem) { + let remainingTimeLabel: UILabel = { + let label = UILabel() + label.font = .boldPreferredFont(forTextStyle: .footnote) + label.textColor = .blue2 + label.text = item.date?.remainingTimeText + return label + }() + + let card = AppointmentListCard(item: item, accessoryView: remainingTimeLabel) + card.onChatTap = { + print("\(item.title) 대화 열기") + } + card.onMapTap = { + print("\(item.title) 지도 열기") + } + card.onToggleNotification = { [weak self] in + self?.viewModel.toggleNotification(id: item.id) + self?.reloadCards() + } + card.onLeave = { [weak self] in + self?.presentLeaveConfirmAlert(id: item.id, title: item.title) + } + + contentStack.addArrangedSubview(card) + } + + private func presentLeaveConfirmAlert(id: String, title: String) { + let alert = UIAlertController( + title: "약속 나가기", + message: "'\(title)' 약속에서 나가시겠어요?", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "취소", style: .cancel)) + alert.addAction(UIAlertAction(title: "나가기", style: .destructive) { [weak self] _ in + self?.viewModel.leave(id: id) + self?.reloadCards() + }) + present(alert, animated: true) + } + + private func presentPastAppointmentList() { + print("지난 약속 화면으로 이동") + } + +} From de3aaa66e286042f9bfad6b3491a31a5dc2f94d5 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:30:21 +0900 Subject: [PATCH 09/27] =?UTF-8?q?FEAT:=20=EC=A7=80=EB=82=9C=20=EC=95=BD?= =?UTF-8?q?=EC=86=8D=20=ED=99=94=EB=A9=B4=EC=9A=A9=20PastAppointmentListVi?= =?UTF-8?q?ewModel=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PastAppointmentListViewModel.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift new file mode 100644 index 0000000..1f2abff --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -0,0 +1,60 @@ +// +// PastAppointmentListViewModel.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import Foundation + +// MARK: - 지난 약속 화면의 데이터 +// - 약속 시각이 지났고, 지난 지 30일 이내인 약속만 보관 + +final class PastAppointmentListViewModel { + + private(set) var pastAppointments: [AppointmentListItem] = [] + + init() { + loadDummyData() + } + + @discardableResult + func delete(id: String) -> [AppointmentListItem] { + pastAppointments.removeAll { $0.id == id } + return pastAppointments + } + + private func loadDummyData() { + let allPastAppointments = [ + AppointmentListItem( + id: "past-1", + title: "고등학교 친구들과 저녁", + participantCount: 5, + location: AppointmentLocation( + title: "고기굽는방앗간 이수역점", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .day, value: -5, to: Date()), + isNotificationEnabled: true + ), + AppointmentListItem( + id: "past-2", + title: "동아리 MT", + participantCount: 12, + location: AppointmentLocation( + title: "강촌 펜션", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .day, value: -20, to: Date()), + isNotificationEnabled: true + ) + ] + + pastAppointments = allPastAppointments.sorted { + ($0.date ?? .distantPast) > ($1.date ?? .distantPast) + } + } + +} From 418d6342e64387db6c6206a32b3382358f7c6dda Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:35:02 +0900 Subject: [PATCH 10/27] =?UTF-8?q?FEAT:=20PastAppointmentListViewController?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B0=8F=20=EC=A7=80?= =?UTF-8?q?=EB=82=9C=20=EC=95=BD=EC=86=8D=20=EC=B9=B4=EB=93=9C/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=9D=90=EB=A6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PastAppointmentListViewController.swift | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift new file mode 100644 index 0000000..44f705f --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -0,0 +1,133 @@ +// +// PastAppointmentListViewController.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import UIKit + +final class PastAppointmentListViewController: UIViewController { + + private static let cardSpacing: CGFloat = 16 + + private let viewModel = PastAppointmentListViewModel() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.text = "30일 이내의 지난 약속이 보관됩니다." + label.font = .preferredFont(forTextStyle: .footnote) + label.textColor = .secondaryLabel + label.textAlignment = .center + return label + }() + + private let scrollView = UIScrollView() + + private lazy var contentStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = Self.cardSpacing + return stack + }() + + private let emptyLabel: UILabel = { + let label = UILabel() + label.text = "지난 약속이 없습니다." + label.font = .preferredFont(forTextStyle: .body) + label.textColor = .secondaryLabel + label.textAlignment = .center + label.isHidden = true + return label + }() + + override func viewDidLoad() { + super.viewDidLoad() + title = "지난 약속" + view.backgroundColor = .systemBackground + setUpLayout() + reloadCards() + } + + private func setUpLayout() { + [descriptionLabel, scrollView, emptyLabel].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + contentStack.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentStack) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), + descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), + + scrollView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 16), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 24), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -24), + contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -48), + + emptyLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } + + private func reloadCards() { + contentStack.arrangedSubviews.forEach { + contentStack.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + let pastAppointments = viewModel.pastAppointments + emptyLabel.isHidden = !pastAppointments.isEmpty + scrollView.isHidden = pastAppointments.isEmpty + + pastAppointments.forEach { addCard(for: $0) } + } + + private func addCard(for item: AppointmentListItem) { + let deleteButton = UIButton(type: .system) + deleteButton.setImage(UIImage(systemName: "trash.circle.fill"), for: .normal) + deleteButton.tintColor = .customRed + + let card = AppointmentListCard(item: item, accessoryView: deleteButton) + card.onChatTap = { + print("\(item.title) 대화 열기") + } + card.onMapTap = { + print("\(item.title) 지도 열기") + } + + deleteButton.addAction(UIAction { [weak self] _ in + self?.presentDeleteConfirmAlert(id: item.id, title: item.title) + }, for: .touchUpInside) + + card.onLeave = { [weak self] in + self?.presentDeleteConfirmAlert(id: item.id, title: item.title) + } + + contentStack.addArrangedSubview(card) + } + + private func presentDeleteConfirmAlert(id: String, title: String) { + let alert = UIAlertController( + title: "약속 삭제", + message: "'\(title)' 약속을 목록에서 삭제하시겠어요?", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "취소", style: .cancel)) + alert.addAction(UIAlertAction(title: "삭제", style: .destructive) { [weak self] _ in + self?.viewModel.delete(id: id) + self?.reloadCards() + }) + present(alert, animated: true) + } + +} From 996c503ffced8dbf783e596ea9930ff018ad478f Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 20:35:52 +0900 Subject: [PATCH 11/27] =?UTF-8?q?FEAT:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=A7=80?= =?UTF-8?q?=EB=82=9C=20=EC=95=BD=EC=86=8D=20=ED=99=94=EB=A9=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=8B=A4=EC=A0=9C=20push=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 8aa85fb..bab45b3 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -169,7 +169,7 @@ final class AppointmentListViewController: UIViewController { } private func presentPastAppointmentList() { - print("지난 약속 화면으로 이동") + navigationController?.pushViewController(PastAppointmentListViewController(), animated: true) } } From e59eb70571073832f89fa2857bcb0a4f179ae8ec Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:10:22 +0900 Subject: [PATCH 12/27] =?UTF-8?q?DESIGN:=20AppointmentListCard=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=83=89=EC=83=81/=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index c11c218..34a0492 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -11,7 +11,7 @@ import UIKit // - 제목 + 인원/장소/날짜 3줄 + 대화 열기·지도 열기 버튼으로 구성 // - 짧게 탭했을 땐 카드 자체는 반응하지 않고 대화/지도 버튼만 반응 // - 꾹 눌렀을 땐 카드 전체가 반응해 컨텍스트 메뉴(알림 토글/약속 나가기)를 표시 -// - 오른쪽 상단 액세서리(남은 시간 라벨 / 휴지통 버튼 등)는 외부에서 주입 +// - 오른쪽 상단 액세서리는 외부에서 주입 final class AppointmentListCard: AppointmentCardBase { @@ -22,12 +22,37 @@ final class AppointmentListCard: AppointmentCardBase { private var isNotificationEnabled: Bool - private lazy var chatButton = UIButton.filled( - title: "대화 열기", background: .systemGray6, tint: .label, font: .preferredFont(forTextStyle: .footnote) - ) - private lazy var mapButton = UIButton.filled( - title: "지도 열기", background: .systemGray6, tint: .label, font: .preferredFont(forTextStyle: .footnote) - ) + private lazy var chatButton: UIButton = { + let button = UIButton.filled( + title: "대화 열기", + background: .blue2.withAlphaComponent(0.8), + tint: .white, + font: .preferredFont(forTextStyle: .caption1) + ) + button.configuration?.image = UIImage(systemName: "text.bubble") + button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( + font: .preferredFont(forTextStyle: .caption1) + ) + button.configuration?.imagePlacement = .leading + button.configuration?.imagePadding = 6 + return button + }() + + private lazy var mapButton: UIButton = { + let button = UIButton.filled( + title: "지도 열기", + background: .blue2.withAlphaComponent(0.8), + tint: .white, + font: .preferredFont(forTextStyle: .caption1) + ) + button.configuration?.image = UIImage(systemName: "map") + button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( + font: .preferredFont(forTextStyle: .caption1) + ) + button.configuration?.imagePlacement = .leading + button.configuration?.imagePadding = 6 + return button + }() init(item: AppointmentListItem, accessoryView: UIView?) { isNotificationEnabled = item.isNotificationEnabled @@ -56,7 +81,7 @@ final class AppointmentListCard: AppointmentCardBase { addSubview(buttonStack) NSLayoutConstraint.activate([ - buttonStack.topAnchor.constraint(equalTo: rowStack.bottomAnchor, constant: 16), + buttonStack.topAnchor.constraint(equalTo: rowStack.bottomAnchor, constant: 10), buttonStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), buttonStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), buttonStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14) From bd41619efea1c8d123ea4976495f7f6085968c3c Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:23:33 +0900 Subject: [PATCH 13/27] =?UTF-8?q?REFACTOR:=20=EC=A7=80=EB=82=9C=20?= =?UTF-8?q?=EC=95=BD=EC=86=8D=20=EC=B9=B4=EB=93=9C=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=82=98=EA=B0=80=EA=B8=B0=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=96=BC=EB=9F=BF=EC=9D=84=20=EA=B3=B5=EC=9A=A9=20extension?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 10 ++----- .../PastAppointmentListViewController.swift | 21 +++------------ .../UIAlertController+LeaveConfirm.swift | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 WhereAreYou/WhereAreYou/Presentation/Extension/UIAlertController+LeaveConfirm.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index bab45b3..98d475b 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -155,16 +155,10 @@ final class AppointmentListViewController: UIViewController { } private func presentLeaveConfirmAlert(id: String, title: String) { - let alert = UIAlertController( - title: "약속 나가기", - message: "'\(title)' 약속에서 나가시겠어요?", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "취소", style: .cancel)) - alert.addAction(UIAlertAction(title: "나가기", style: .destructive) { [weak self] _ in + let alert = UIAlertController.leaveConfirmAlert(title: title) { [weak self] in self?.viewModel.leave(id: id) self?.reloadCards() - }) + } present(alert, animated: true) } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index 44f705f..15ab213 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -93,22 +93,13 @@ final class PastAppointmentListViewController: UIViewController { } private func addCard(for item: AppointmentListItem) { - let deleteButton = UIButton(type: .system) - deleteButton.setImage(UIImage(systemName: "trash.circle.fill"), for: .normal) - deleteButton.tintColor = .customRed - - let card = AppointmentListCard(item: item, accessoryView: deleteButton) + let card = AppointmentListCard(item: item, accessoryView: nil) card.onChatTap = { print("\(item.title) 대화 열기") } card.onMapTap = { print("\(item.title) 지도 열기") } - - deleteButton.addAction(UIAction { [weak self] _ in - self?.presentDeleteConfirmAlert(id: item.id, title: item.title) - }, for: .touchUpInside) - card.onLeave = { [weak self] in self?.presentDeleteConfirmAlert(id: item.id, title: item.title) } @@ -117,16 +108,10 @@ final class PastAppointmentListViewController: UIViewController { } private func presentDeleteConfirmAlert(id: String, title: String) { - let alert = UIAlertController( - title: "약속 삭제", - message: "'\(title)' 약속을 목록에서 삭제하시겠어요?", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "취소", style: .cancel)) - alert.addAction(UIAlertAction(title: "삭제", style: .destructive) { [weak self] _ in + let alert = UIAlertController.leaveConfirmAlert(title: title) { [weak self] in self?.viewModel.delete(id: id) self?.reloadCards() - }) + } present(alert, animated: true) } diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/UIAlertController+LeaveConfirm.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/UIAlertController+LeaveConfirm.swift new file mode 100644 index 0000000..ebbd3f2 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/UIAlertController+LeaveConfirm.swift @@ -0,0 +1,26 @@ +// +// UIAlertController+LeaveConfirm.swift +// WhereAreYou +// +// Created by 김성훈 on 7/21/26. +// + +import UIKit + +// MARK: - 약속 나가기(목록에서 제거) 확인 알럿 +// - 약속 목록/지난 약속 화면에서 동일한 문구·동작으로 재사용 + +extension UIAlertController { + + static func leaveConfirmAlert(title: String, onConfirm: @escaping () -> Void) -> UIAlertController { + let alert = UIAlertController( + title: "약속 나가기", + message: "'\(title)' 약속에서 나가시겠어요?", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "취소", style: .cancel)) + alert.addAction(UIAlertAction(title: "나가기", style: .destructive) { _ in onConfirm() }) + return alert + } + +} From 3e39b2bf103aa426b38728dd8cc2f0545c5218eb Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:35:58 +0900 Subject: [PATCH 14/27] =?UTF-8?q?DESIGN:=20=EC=A7=80=EB=82=9C=20=EC=95=BD?= =?UTF-8?q?=EC=86=8D=20=EC=9D=B4=EB=8F=99=20=EB=B2=84=ED=8A=BC=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 98d475b..af6b861 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -41,8 +41,24 @@ final class AppointmentListViewController: UIViewController { private let pastAppointmentButton: UIButton = { let button = UIButton(type: .system) - button.setImage(UIImage(systemName: "arrow.uturn.backward.circle.fill"), for: .normal) - button.tintColor = .blue2 + button.backgroundColor = .white + button.tintColor = .black + button.layer.cornerRadius = 24 + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.25 + button.layer.shadowRadius = 4 + button.layer.shadowOffset = CGSize(width: 0, height: 2) + + let symbolConfiguration = UIImage.SymbolConfiguration( + font: .systemFont(ofSize: 19, weight: .semibold) + ) + button.setImage( + UIImage( + systemName: "clock.arrow.trianglehead.counterclockwise.rotate.90", + withConfiguration: symbolConfiguration + ), + for: .normal + ) return button }() From d89ea5364117b20f0bbb42999bf7c76e1365465e Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:38:12 +0900 Subject: [PATCH 15/27] =?UTF-8?q?DESIGN:=20=EC=A7=80=EB=82=9C=20=EC=95=BD?= =?UTF-8?q?=EC=86=8D=20=ED=99=94=EB=A9=B4=EC=97=90=20=EB=A1=B1=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8A=A4=20=EC=82=AD=EC=A0=9C=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/PastAppointmentListViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index 15ab213..a11b1c4 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -15,10 +15,11 @@ final class PastAppointmentListViewController: UIViewController { private let descriptionLabel: UILabel = { let label = UILabel() - label.text = "30일 이내의 지난 약속이 보관됩니다." + label.text = "30일 이내의 지난 약속이 보관됩니다.\n약속을 길게 눌러 삭제할 수 있습니다." label.font = .preferredFont(forTextStyle: .footnote) label.textColor = .secondaryLabel label.textAlignment = .center + label.numberOfLines = 0 return label }() @@ -59,7 +60,7 @@ final class PastAppointmentListViewController: UIViewController { scrollView.addSubview(contentStack) NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), From 42ddaf898c4f86115b471dbcb0761258d2f53f73 Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:43:56 +0900 Subject: [PATCH 16/27] =?UTF-8?q?DESIGN:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D/=EC=A7=80=EB=82=9C=20=EC=95=BD=EC=86=8D=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=A2=8C=EC=9A=B0=20=EC=97=AC=EB=B0=B1?= =?UTF-8?q?=EC=9D=84=20=ED=99=88=20=ED=99=94=EB=A9=B4=EA=B3=BC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=98=EA=B2=8C=2040pt=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListViewController.swift | 6 +++--- .../AppointmentList/PastAppointmentListViewController.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index af6b861..46250a0 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -89,10 +89,10 @@ final class AppointmentListViewController: UIViewController { scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8), - contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 24), - contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -24), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 40), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -40), contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16), - contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -48), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -80), emptyLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), emptyLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index a11b1c4..d59e717 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -70,10 +70,10 @@ final class PastAppointmentListViewController: UIViewController { scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8), - contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 24), - contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -24), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 40), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -40), contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16), - contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -48), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -80), emptyLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), emptyLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) From b2eb9401c673e4f7a692cf99a33a5552ac9f114a Mon Sep 17 00:00:00 2001 From: snughnu Date: Tue, 21 Jul 2026 23:50:23 +0900 Subject: [PATCH 17/27] =?UTF-8?q?DESIGN:=20AppointmentListCard=20=EB=8C=80?= =?UTF-8?q?=ED=99=94/=EC=A7=80=EB=8F=84=20=EB=B2=84=ED=8A=BC=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=EC=99=80=20=EB=AA=A8=EC=84=9C=EB=A6=AC=20radius=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index 34a0492..f1e6ae8 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -33,8 +33,11 @@ final class AppointmentListCard: AppointmentCardBase { button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( font: .preferredFont(forTextStyle: .caption1) ) + button.configuration?.cornerStyle = .fixed + button.configuration?.background.cornerRadius = 8 button.configuration?.imagePlacement = .leading button.configuration?.imagePadding = 6 + button.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) return button }() @@ -49,8 +52,11 @@ final class AppointmentListCard: AppointmentCardBase { button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( font: .preferredFont(forTextStyle: .caption1) ) + button.configuration?.cornerStyle = .fixed + button.configuration?.background.cornerRadius = 8 button.configuration?.imagePlacement = .leading button.configuration?.imagePadding = 6 + button.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) return button }() @@ -84,7 +90,7 @@ final class AppointmentListCard: AppointmentCardBase { buttonStack.topAnchor.constraint(equalTo: rowStack.bottomAnchor, constant: 10), buttonStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), buttonStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), - buttonStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14) + buttonStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) ]) chatButton.addAction(UIAction { [weak self] _ in self?.onChatTap?() }, for: .touchUpInside) From 48a233c62954773b1b2f3dc48b82aa7b5e4064ce Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 22 Jul 2026 00:01:59 +0900 Subject: [PATCH 18/27] =?UTF-8?q?DESIGN:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=20=ED=97=A4=EB=8D=94(=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0/=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=83=89=EC=83=81)=20?= =?UTF-8?q?=EB=B0=8F=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=94=20=ED=91=9C=EC=8B=9C=20=EC=A0=84=ED=99=98=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 23 ++++++++++++++++--- .../PastAppointmentListViewController.swift | 5 ++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 46250a0..ec12a6c 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -13,10 +13,17 @@ final class AppointmentListViewController: UIViewController { private let viewModel = AppointmentListViewModel() + private let logoImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "Logo")) + imageView.contentMode = .scaleAspectFit + return imageView + }() + private let titleLabel: UILabel = { let label = UILabel() label.text = "약속 목록" label.font = .boldPreferredFont(forTextStyle: .title1) + label.textColor = .blue1 return label }() @@ -70,8 +77,13 @@ final class AppointmentListViewController: UIViewController { reloadCards() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: animated) + } + private func setUpLayout() { - [titleLabel, scrollView, emptyLabel, pastAppointmentButton].forEach { + [logoImageView, titleLabel, scrollView, emptyLabel, pastAppointmentButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false view.addSubview($0) } @@ -80,8 +92,13 @@ final class AppointmentListViewController: UIViewController { scrollView.addSubview(contentStack) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), + logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + logoImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + logoImageView.widthAnchor.constraint(equalToConstant: 77), + logoImageView.heightAnchor.constraint(equalToConstant: 48), + + titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 18), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index d59e717..bd67682 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -50,6 +50,11 @@ final class PastAppointmentListViewController: UIViewController { reloadCards() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + } + private func setUpLayout() { [descriptionLabel, scrollView, emptyLabel].forEach { $0.translatesAutoresizingMaskIntoConstraints = false From 9ca85c493225ed2d385bb677c453c4c260fbd2cf Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 22 Jul 2026 00:14:08 +0900 Subject: [PATCH 19/27] =?UTF-8?q?DESIGN:=20=EC=95=BD=EC=86=8D=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=8D=94=EB=AF=B8=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=B9=EC=85=98=20=ED=97=A4=EB=8D=94=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 32 ++++++++++++++++--- .../AppointmentListViewModel.swift | 14 +++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index ec12a6c..365da9d 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -10,6 +10,10 @@ import UIKit final class AppointmentListViewController: UIViewController { private static let cardSpacing: CGFloat = 16 + private static let cardLeadingInset: CGFloat = 40 + private static let sectionHeaderLeadingInset: CGFloat = 26 + private static let sectionHeaderTopSpacing: CGFloat = 18 + private static let sectionHeaderBottomSpacing: CGFloat = 10 private let viewModel = AppointmentListViewModel() @@ -106,10 +110,10 @@ final class AppointmentListViewController: UIViewController { scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8), - contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 40), - contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -40), + contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: Self.cardLeadingInset), + contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -Self.cardLeadingInset), contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16), - contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -80), + contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -(Self.cardLeadingInset * 2)), emptyLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), emptyLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), @@ -153,11 +157,31 @@ final class AppointmentListViewController: UIViewController { private func addSectionHeaderIfNeeded(title: String, isNeeded: Bool) { guard isNeeded else { return } + let label = UILabel() label.text = title label.font = .preferredFont(forTextStyle: .callout) label.textColor = .secondaryLabel - contentStack.addArrangedSubview(label) + + let headerContainer = UIView() + label.translatesAutoresizingMaskIntoConstraints = false + headerContainer.addSubview(label) + + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: headerContainer.topAnchor), + label.bottomAnchor.constraint(equalTo: headerContainer.bottomAnchor), + label.leadingAnchor.constraint( + equalTo: headerContainer.leadingAnchor, + constant: Self.sectionHeaderLeadingInset - Self.cardLeadingInset + ), + label.trailingAnchor.constraint(lessThanOrEqualTo: headerContainer.trailingAnchor) + ]) + + if let previousView = contentStack.arrangedSubviews.last { + contentStack.setCustomSpacing(Self.sectionHeaderTopSpacing, after: previousView) + } + contentStack.addArrangedSubview(headerContainer) + contentStack.setCustomSpacing(Self.sectionHeaderBottomSpacing, after: headerContainer) } private func addCard(for item: AppointmentListItem) { diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index 02af7bc..474bd0a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -57,7 +57,19 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .hour, value: 10, to: Date()), + date: Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date()), + isNotificationEnabled: true + ), + AppointmentListItem( + id: "today-2", + title: "오늘 저녁 약속", + participantCount: 3, + location: AppointmentLocation( + title: "테스트 장소", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()), isNotificationEnabled: true ), AppointmentListItem( From b7fe7e891b841fdc20f922930d353810857cfeb9 Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 22 Jul 2026 00:23:48 +0900 Subject: [PATCH 20/27] =?UTF-8?q?FIX:=20=EB=82=A8=EC=9D=80=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B3=84=EC=82=B0=EC=9D=98=200=EC=8B=9C=EA=B0=84/0?= =?UTF-8?q?=EC=9D=BC=20=EB=B2=84=EA=B7=B8=EC=99=80=20=EC=A7=80=EB=82=9C=20?= =?UTF-8?q?=EC=95=BD=EC=86=8D=20=EC=98=A4=ED=8C=90=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EA=B3=A7=20=EC=8B=9C=EC=9E=91=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewModel.swift | 12 +++++++++ .../Extension/Date+RemainingTime.swift | 26 ++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index 474bd0a..d6fea55 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -72,6 +72,18 @@ final class AppointmentListViewModel { date: Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()), isNotificationEnabled: true ), + AppointmentListItem( + id: "today-3", + title: "곧 시작하는 약속", + participantCount: 4, + location: AppointmentLocation( + title: "테스트 장소", + address: "", + coordinate: Coordinate(latitude: 0, longitude: 0) + ), + date: Calendar.current.date(byAdding: .minute, value: 30, to: Date()), + isNotificationEnabled: true + ), AppointmentListItem( id: "2", title: "고등학교 친구들과 저녁", diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift index 66399c6..eb1a6c3 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/Date+RemainingTime.swift @@ -8,27 +8,33 @@ import Foundation // MARK: - 현재 시각 기준 남은 시간을 화면에 표시할 문자열로 변환 -// - 약속 시간이 지났다면 nil을 반환해 "남은 시간 표시 안 함" 규칙을 표현 -// - 경계 판정과 단위 숫자 계산 모두 시간(hour) 차이 하나만 기준으로 함 (달력 월/년 컴포넌트 미사용) -// - 12시간 이하 → 시간 단위 -// - 30일(24*30시간) 이하 → 일 단위 -// - 365일(24*365시간) 이하 → 개월 단위 (30일 = 1개월로 근사) +// - 약속 시간이 지났다면(self < 지금) nil을 반환해 "남은 시간 표시 안 함" 규칙을 표현 +// 지남 여부는 Date 값 자체(초 단위)로 판정 — hour 절삭값으로 판정하면 자정 근처 등에서 오판 가능 +// - 안 지났다면 시간(hour) 차이를 기준으로 구간을 나눠 단위를 정함 (달력 월/년 컴포넌트 미사용) +// - 각 구간의 경계는 그 구간에서 쓰는 나눗셈 단위와 맞춰, 정수 나눗셈으로 값이 0이 되는 경우가 없도록 함 +// - 1시간 미만 → "곧 시작" +// - 24시간 미만 → 시간 단위 +// - 30일(24*30시간) 미만 → 일 단위 +// - 365일(24*365시간) 미만 → 개월 단위 (30일 = 1개월로 근사) // - 그 외 → 연 단위 (365일 = 1년으로 근사) extension Date { var remainingTimeText: String? { - let hours = Calendar.current.dateComponents([.hour], from: Date(), to: self).hour ?? 0 + guard self >= Date() else { return nil } - guard hours >= 0 else { return nil } + let hours = Calendar.current.dateComponents([.hour], from: Date(), to: self).hour ?? 0 - if hours <= 12 { + if hours == 0 { + return "곧 시작" + } + if hours < 24 { return "\(hours)시간 남음" } - if hours <= 24 * 30 { + if hours < 24 * 30 { return "\(hours / 24)일 남음" } - if hours <= 24 * 365 { + if hours < 24 * 365 { return "\(hours / 24 / 30)개월 남음" } return "\(hours / 24 / 365)년 남음" From cb5d45f55204bf67913e508926b48f430409c91b Mon Sep 17 00:00:00 2001 From: snughnu Date: Wed, 22 Jul 2026 00:26:19 +0900 Subject: [PATCH 21/27] =?UTF-8?q?DESIGN:=20=EC=A7=80=EB=82=9C=20=EC=95=BD?= =?UTF-8?q?=EC=86=8D=20=ED=99=94=EB=A9=B4=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A5=BC=2015=EA=B0=9C=EB=A1=9C=20=EB=8A=98?= =?UTF-8?q?=EB=A0=A4=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PastAppointmentListViewModel.swift | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift index 1f2abff..1b76973 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -25,32 +25,20 @@ final class PastAppointmentListViewModel { } private func loadDummyData() { - let allPastAppointments = [ + let allPastAppointments = (1...15).map { index in AppointmentListItem( - id: "past-1", - title: "고등학교 친구들과 저녁", - participantCount: 5, + id: "past-\(index)", + title: "지난 약속 \(index)", + participantCount: index % 10 + 1, location: AppointmentLocation( - title: "고기굽는방앗간 이수역점", + title: "테스트 장소 \(index)", address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: -5, to: Date()), - isNotificationEnabled: true - ), - AppointmentListItem( - id: "past-2", - title: "동아리 MT", - participantCount: 12, - location: AppointmentLocation( - title: "강촌 펜션", - address: "", - coordinate: Coordinate(latitude: 0, longitude: 0) - ), - date: Calendar.current.date(byAdding: .day, value: -20, to: Date()), + date: Calendar.current.date(byAdding: .day, value: -index, to: Date()), isNotificationEnabled: true ) - ] + } pastAppointments = allPastAppointments.sorted { ($0.date ?? .distantPast) > ($1.date ?? .distantPast) From 18f800fdb900e56ea56a04701219044a586ec2ad Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 11:06:09 +0900 Subject: [PATCH 22/27] =?UTF-8?q?REFACTOR:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EC=9D=98=20=EC=83=81=ED=83=9C=20=ED=8C=90=EB=8B=A8=EC=9D=84=20?= =?UTF-8?q?ViewModel=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 31 ++----------------- .../AppointmentListViewController.swift | 22 +++++++++++-- .../AppointmentListViewModel.swift | 8 +++++ .../PastAppointmentListViewController.swift | 24 ++++++++++++-- .../PastAppointmentListViewModel.swift | 26 ++++++++++++++++ 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index f1e6ae8..b183ce9 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -17,10 +17,7 @@ final class AppointmentListCard: AppointmentCardBase { var onChatTap: (() -> Void)? var onMapTap: (() -> Void)? - var onToggleNotification: (() -> Void)? - var onLeave: (() -> Void)? - - private var isNotificationEnabled: Bool + var contextMenuProvider: (() -> UIMenu)? private lazy var chatButton: UIButton = { let button = UIButton.filled( @@ -61,8 +58,6 @@ final class AppointmentListCard: AppointmentCardBase { }() init(item: AppointmentListItem, accessoryView: UIView?) { - isNotificationEnabled = item.isNotificationEnabled - super.init( participantCount: item.participantCount, placeText: item.location?.title ?? "미정", @@ -101,28 +96,6 @@ final class AppointmentListCard: AppointmentCardBase { addInteraction(UIContextMenuInteraction(delegate: self)) } - private func makeContextMenu() -> UIMenu { - let notificationTitle = isNotificationEnabled ? "알림 끄기" : "알림 켜기" - let notificationIcon = isNotificationEnabled ? "bell.slash" : "bell" - - let toggleNotificationAction = UIAction( - title: notificationTitle, - image: UIImage(systemName: notificationIcon) - ) { [weak self] _ in - self?.onToggleNotification?() - } - - let leaveAction = UIAction( - title: "약속 나가기", - image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), - attributes: .destructive - ) { [weak self] _ in - self?.onLeave?() - } - - return UIMenu(children: [toggleNotificationAction, leaveAction]) - } - } // MARK: - UIContextMenuInteractionDelegate @@ -133,7 +106,7 @@ extension AppointmentListCard: UIContextMenuInteractionDelegate { _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint ) -> UIContextMenuConfiguration? { - UIContextMenuConfiguration(actionProvider: { [weak self] _ in self?.makeContextMenu() }) + UIContextMenuConfiguration(actionProvider: { [weak self] _ in self?.contextMenuProvider?() }) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 365da9d..1245edd 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -200,15 +200,31 @@ final class AppointmentListViewController: UIViewController { card.onMapTap = { print("\(item.title) 지도 열기") } - card.onToggleNotification = { [weak self] in + card.contextMenuProvider = { [weak self] in + self?.makeContextMenu(for: item) ?? UIMenu(children: []) + } + + contentStack.addArrangedSubview(card) + } + + private func makeContextMenu(for item: AppointmentListItem) -> UIMenu { + let notificationAction = UIAction( + title: viewModel.notificationMenuTitle(for: item), + image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) + ) { [weak self] _ in self?.viewModel.toggleNotification(id: item.id) self?.reloadCards() } - card.onLeave = { [weak self] in + + let leaveAction = UIAction( + title: "약속 나가기", + image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), + attributes: .destructive + ) { [weak self] _ in self?.presentLeaveConfirmAlert(id: item.id, title: item.title) } - contentStack.addArrangedSubview(card) + return UIMenu(children: [notificationAction, leaveAction]) } private func presentLeaveConfirmAlert(id: String, title: String) { diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index d6fea55..e608035 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -27,6 +27,14 @@ final class AppointmentListViewModel { return (todayAppointments, upcomingAppointments) } + func notificationMenuTitle(for item: AppointmentListItem) -> String { + item.isNotificationEnabled ? "알림 끄기" : "알림 켜기" + } + + func notificationMenuIcon(for item: AppointmentListItem) -> String { + item.isNotificationEnabled ? "bell.slash" : "bell" + } + @discardableResult func leave(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { todayAppointments.removeAll { $0.id == id } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index bd67682..4debba7 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -106,13 +106,33 @@ final class PastAppointmentListViewController: UIViewController { card.onMapTap = { print("\(item.title) 지도 열기") } - card.onLeave = { [weak self] in - self?.presentDeleteConfirmAlert(id: item.id, title: item.title) + card.contextMenuProvider = { [weak self] in + self?.makeContextMenu(for: item) ?? UIMenu(children: []) } contentStack.addArrangedSubview(card) } + private func makeContextMenu(for item: AppointmentListItem) -> UIMenu { + let notificationAction = UIAction( + title: viewModel.notificationMenuTitle(for: item), + image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) + ) { [weak self] _ in + self?.viewModel.toggleNotification(id: item.id) + self?.reloadCards() + } + + let leaveAction = UIAction( + title: "약속 나가기", + image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), + attributes: .destructive + ) { [weak self] _ in + self?.presentDeleteConfirmAlert(id: item.id, title: item.title) + } + + return UIMenu(children: [notificationAction, leaveAction]) + } + private func presentDeleteConfirmAlert(id: String, title: String) { let alert = UIAlertController.leaveConfirmAlert(title: title) { [weak self] in self?.viewModel.delete(id: id) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift index 1b76973..e928f2a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -24,6 +24,32 @@ final class PastAppointmentListViewModel { return pastAppointments } + @discardableResult + func toggleNotification(id: String) -> [AppointmentListItem] { + pastAppointments = pastAppointments.map { toggledIfMatching(id: id, item: $0) } + return pastAppointments + } + + func notificationMenuTitle(for item: AppointmentListItem) -> String { + item.isNotificationEnabled ? "알림 끄기" : "알림 켜기" + } + + func notificationMenuIcon(for item: AppointmentListItem) -> String { + item.isNotificationEnabled ? "bell.slash" : "bell" + } + + private func toggledIfMatching(id: String, item: AppointmentListItem) -> AppointmentListItem { + guard item.id == id else { return item } + return AppointmentListItem( + id: item.id, + title: item.title, + participantCount: item.participantCount, + location: item.location, + date: item.date, + isNotificationEnabled: !item.isNotificationEnabled + ) + } + private func loadDummyData() { let allPastAppointments = (1...15).map { index in AppointmentListItem( From 7626db569b8ec07159edd5e0397a1807a6652ba0 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 11:14:10 +0900 Subject: [PATCH 23/27] =?UTF-8?q?REFACTOR:=20UIImage(named:)=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20.logo=20=ED=83=80=EC=9E=85=20=EC=84=B8=EC=9D=B4?= =?UTF-8?q?=ED=94=84=20=EC=A0=91=EA=B7=BC=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 1245edd..094e9f8 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -18,7 +18,7 @@ final class AppointmentListViewController: UIViewController { private let viewModel = AppointmentListViewModel() private let logoImageView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "Logo")) + let imageView = UIImageView(image: .logo) imageView.contentMode = .scaleAspectFit return imageView }() From e0cae79e0709b9c9b91f7b83656a762b8f763cd2 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 11:24:32 +0900 Subject: [PATCH 24/27] =?UTF-8?q?REFACTOR:=20=ED=86=A0=EA=B8=80/=EB=82=98?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=20=EB=B0=98=ED=99=98=EA=B0=92=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EC=A7=80=EB=82=9C=20=EC=95=BD=EC=86=8D?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EB=AA=85=EC=B9=AD=EC=9D=84=20=EB=82=98?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 20 ++++++++++++------- .../AppointmentListViewModel.swift | 2 -- .../PastAppointmentListViewController.swift | 19 ++++++++++-------- .../PastAppointmentListViewModel.swift | 12 +++++------ 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index 094e9f8..c5b2eff 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -131,14 +131,18 @@ final class AppointmentListViewController: UIViewController { }, for: .touchUpInside) } - private func reloadCards() { + private func reloadCards( + today: [AppointmentListItem]? = nil, + upcoming: [AppointmentListItem]? = nil + ) { + let today = today ?? viewModel.todayAppointments + let upcoming = upcoming ?? viewModel.upcomingAppointments + contentStack.arrangedSubviews.forEach { contentStack.removeArrangedSubview($0) $0.removeFromSuperview() } - let today = viewModel.todayAppointments - let upcoming = viewModel.upcomingAppointments let hasAnyAppointment = !today.isEmpty || !upcoming.isEmpty emptyLabel.isHidden = hasAnyAppointment @@ -212,8 +216,9 @@ final class AppointmentListViewController: UIViewController { title: viewModel.notificationMenuTitle(for: item), image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) ) { [weak self] _ in - self?.viewModel.toggleNotification(id: item.id) - self?.reloadCards() + guard let self else { return } + let (today, upcoming) = viewModel.toggleNotification(id: item.id) + reloadCards(today: today, upcoming: upcoming) } let leaveAction = UIAction( @@ -229,8 +234,9 @@ final class AppointmentListViewController: UIViewController { private func presentLeaveConfirmAlert(id: String, title: String) { let alert = UIAlertController.leaveConfirmAlert(title: title) { [weak self] in - self?.viewModel.leave(id: id) - self?.reloadCards() + guard let self else { return } + let (today, upcoming) = viewModel.leave(id: id) + reloadCards(today: today, upcoming: upcoming) } present(alert, animated: true) } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index e608035..b9c1797 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -20,7 +20,6 @@ final class AppointmentListViewModel { loadDummyData() } - @discardableResult func toggleNotification(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { todayAppointments = todayAppointments.map { toggledIfMatching(id: id, item: $0) } upcomingAppointments = upcomingAppointments.map { toggledIfMatching(id: id, item: $0) } @@ -35,7 +34,6 @@ final class AppointmentListViewModel { item.isNotificationEnabled ? "bell.slash" : "bell" } - @discardableResult func leave(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { todayAppointments.removeAll { $0.id == id } upcomingAppointments.removeAll { $0.id == id } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index 4debba7..463dffc 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -85,13 +85,14 @@ final class PastAppointmentListViewController: UIViewController { ]) } - private func reloadCards() { + private func reloadCards(pastAppointments: [AppointmentListItem]? = nil) { + let pastAppointments = pastAppointments ?? viewModel.pastAppointments + contentStack.arrangedSubviews.forEach { contentStack.removeArrangedSubview($0) $0.removeFromSuperview() } - let pastAppointments = viewModel.pastAppointments emptyLabel.isHidden = !pastAppointments.isEmpty scrollView.isHidden = pastAppointments.isEmpty @@ -118,8 +119,9 @@ final class PastAppointmentListViewController: UIViewController { title: viewModel.notificationMenuTitle(for: item), image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) ) { [weak self] _ in - self?.viewModel.toggleNotification(id: item.id) - self?.reloadCards() + guard let self else { return } + let updated = viewModel.toggleNotification(id: item.id) + reloadCards(pastAppointments: updated) } let leaveAction = UIAction( @@ -127,16 +129,17 @@ final class PastAppointmentListViewController: UIViewController { image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), attributes: .destructive ) { [weak self] _ in - self?.presentDeleteConfirmAlert(id: item.id, title: item.title) + self?.presentLeaveConfirmAlert(id: item.id, title: item.title) } return UIMenu(children: [notificationAction, leaveAction]) } - private func presentDeleteConfirmAlert(id: String, title: String) { + private func presentLeaveConfirmAlert(id: String, title: String) { let alert = UIAlertController.leaveConfirmAlert(title: title) { [weak self] in - self?.viewModel.delete(id: id) - self?.reloadCards() + guard let self else { return } + let updated = viewModel.leave(id: id) + reloadCards(pastAppointments: updated) } present(alert, animated: true) } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift index e928f2a..2a0e226 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -18,13 +18,6 @@ final class PastAppointmentListViewModel { loadDummyData() } - @discardableResult - func delete(id: String) -> [AppointmentListItem] { - pastAppointments.removeAll { $0.id == id } - return pastAppointments - } - - @discardableResult func toggleNotification(id: String) -> [AppointmentListItem] { pastAppointments = pastAppointments.map { toggledIfMatching(id: id, item: $0) } return pastAppointments @@ -38,6 +31,11 @@ final class PastAppointmentListViewModel { item.isNotificationEnabled ? "bell.slash" : "bell" } + func leave(id: String) -> [AppointmentListItem] { + pastAppointments.removeAll { $0.id == id } + return pastAppointments + } + private func toggledIfMatching(id: String, item: AppointmentListItem) -> AppointmentListItem { guard item.id == id else { return item } return AppointmentListItem( From 0c0f2b7dd06e19f218c825b1ef22c7ad6f5ecf6b Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 11:45:41 +0900 Subject: [PATCH 25/27] =?UTF-8?q?REFACTOR:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EC=9E=AC?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=97=86=EC=9D=B4=20ViewModel=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A7=8C=20=EA=B0=B1=EC=8B=A0=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentListViewController.swift | 10 ++++----- .../AppointmentListViewModel.swift | 21 ++++++++++++------- .../PastAppointmentListViewController.swift | 10 ++++----- .../PastAppointmentListViewModel.swift | 15 +++++++------ 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index c5b2eff..ce8b34e 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -205,20 +205,20 @@ final class AppointmentListViewController: UIViewController { print("\(item.title) 지도 열기") } card.contextMenuProvider = { [weak self] in - self?.makeContextMenu(for: item) ?? UIMenu(children: []) + self?.makeContextMenu(id: item.id) ?? UIMenu(children: []) } contentStack.addArrangedSubview(card) } - private func makeContextMenu(for item: AppointmentListItem) -> UIMenu { + private func makeContextMenu(id: String) -> UIMenu { + guard let item = viewModel.item(id: id) else { return UIMenu(children: []) } + let notificationAction = UIAction( title: viewModel.notificationMenuTitle(for: item), image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) ) { [weak self] _ in - guard let self else { return } - let (today, upcoming) = viewModel.toggleNotification(id: item.id) - reloadCards(today: today, upcoming: upcoming) + self?.viewModel.toggleNotification(id: item.id) } let leaveAction = UIAction( diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index b9c1797..fea8728 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -20,10 +20,18 @@ final class AppointmentListViewModel { loadDummyData() } - func toggleNotification(id: String) -> (today: [AppointmentListItem], upcoming: [AppointmentListItem]) { - todayAppointments = todayAppointments.map { toggledIfMatching(id: id, item: $0) } - upcomingAppointments = upcomingAppointments.map { toggledIfMatching(id: id, item: $0) } - return (todayAppointments, upcomingAppointments) + func toggleNotification(id: String) { + if let index = todayAppointments.firstIndex(where: { $0.id == id }) { + todayAppointments[index] = toggledNotification(of: todayAppointments[index]) + return + } + if let index = upcomingAppointments.firstIndex(where: { $0.id == id }) { + upcomingAppointments[index] = toggledNotification(of: upcomingAppointments[index]) + } + } + + func item(id: String) -> AppointmentListItem? { + todayAppointments.first { $0.id == id } ?? upcomingAppointments.first { $0.id == id } } func notificationMenuTitle(for item: AppointmentListItem) -> String { @@ -40,9 +48,8 @@ final class AppointmentListViewModel { return (todayAppointments, upcomingAppointments) } - private func toggledIfMatching(id: String, item: AppointmentListItem) -> AppointmentListItem { - guard item.id == id else { return item } - return AppointmentListItem( + private func toggledNotification(of item: AppointmentListItem) -> AppointmentListItem { + AppointmentListItem( id: item.id, title: item.title, participantCount: item.participantCount, diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift index 463dffc..fbfd4cf 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewController.swift @@ -108,20 +108,20 @@ final class PastAppointmentListViewController: UIViewController { print("\(item.title) 지도 열기") } card.contextMenuProvider = { [weak self] in - self?.makeContextMenu(for: item) ?? UIMenu(children: []) + self?.makeContextMenu(id: item.id) ?? UIMenu(children: []) } contentStack.addArrangedSubview(card) } - private func makeContextMenu(for item: AppointmentListItem) -> UIMenu { + private func makeContextMenu(id: String) -> UIMenu { + guard let item = viewModel.item(id: id) else { return UIMenu(children: []) } + let notificationAction = UIAction( title: viewModel.notificationMenuTitle(for: item), image: UIImage(systemName: viewModel.notificationMenuIcon(for: item)) ) { [weak self] _ in - guard let self else { return } - let updated = viewModel.toggleNotification(id: item.id) - reloadCards(pastAppointments: updated) + self?.viewModel.toggleNotification(id: item.id) } let leaveAction = UIAction( diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift index 2a0e226..ba0cc01 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -18,9 +18,13 @@ final class PastAppointmentListViewModel { loadDummyData() } - func toggleNotification(id: String) -> [AppointmentListItem] { - pastAppointments = pastAppointments.map { toggledIfMatching(id: id, item: $0) } - return pastAppointments + func toggleNotification(id: String) { + guard let index = pastAppointments.firstIndex(where: { $0.id == id }) else { return } + pastAppointments[index] = toggledNotification(of: pastAppointments[index]) + } + + func item(id: String) -> AppointmentListItem? { + pastAppointments.first { $0.id == id } } func notificationMenuTitle(for item: AppointmentListItem) -> String { @@ -36,9 +40,8 @@ final class PastAppointmentListViewModel { return pastAppointments } - private func toggledIfMatching(id: String, item: AppointmentListItem) -> AppointmentListItem { - guard item.id == id else { return item } - return AppointmentListItem( + private func toggledNotification(of item: AppointmentListItem) -> AppointmentListItem { + AppointmentListItem( id: item.id, title: item.title, participantCount: item.participantCount, From 2bacfde3a054d6d0d78d79735ff9feceb2b0d32a Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 12:04:33 +0900 Subject: [PATCH 26/27] =?UTF-8?q?FIX:=20AppointmentListCard=EC=9D=98=20pre?= =?UTF-8?q?ferredFont=20=ED=83=80=EC=9E=85=20=EC=B6=94=EB=A1=A0=20?= =?UTF-8?q?=EB=AA=A8=ED=98=B8=EC=84=B1=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=B9=8C=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index b183ce9..020ccf5 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -24,11 +24,11 @@ final class AppointmentListCard: AppointmentCardBase { title: "대화 열기", background: .blue2.withAlphaComponent(0.8), tint: .white, - font: .preferredFont(forTextStyle: .caption1) + font: UIFont.preferredFont(forTextStyle: .caption1) ) button.configuration?.image = UIImage(systemName: "text.bubble") button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( - font: .preferredFont(forTextStyle: .caption1) + font: UIFont.preferredFont(forTextStyle: .caption1) ) button.configuration?.cornerStyle = .fixed button.configuration?.background.cornerRadius = 8 @@ -43,11 +43,11 @@ final class AppointmentListCard: AppointmentCardBase { title: "지도 열기", background: .blue2.withAlphaComponent(0.8), tint: .white, - font: .preferredFont(forTextStyle: .caption1) + font: UIFont.preferredFont(forTextStyle: .caption1) ) button.configuration?.image = UIImage(systemName: "map") button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( - font: .preferredFont(forTextStyle: .caption1) + font: UIFont.preferredFont(forTextStyle: .caption1) ) button.configuration?.cornerStyle = .fixed button.configuration?.background.cornerRadius = 8 From 941363a3afa730766a1e3b984aac06806f4b2463 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 24 Jul 2026 12:10:24 +0900 Subject: [PATCH 27/27] =?UTF-8?q?FIX:=20UIButton.filled=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20AppointmentListCard=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentList/AppointmentListCard.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index 020ccf5..f42646f 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -24,17 +24,16 @@ final class AppointmentListCard: AppointmentCardBase { title: "대화 열기", background: .blue2.withAlphaComponent(0.8), tint: .white, - font: UIFont.preferredFont(forTextStyle: .caption1) + font: .caption1, + edgeInsets: NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) ) + button.layer.cornerRadius = 8 button.configuration?.image = UIImage(systemName: "text.bubble") button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( - font: UIFont.preferredFont(forTextStyle: .caption1) + font: .preferredFont(forTextStyle: .caption1) ) - button.configuration?.cornerStyle = .fixed - button.configuration?.background.cornerRadius = 8 button.configuration?.imagePlacement = .leading button.configuration?.imagePadding = 6 - button.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) return button }() @@ -43,17 +42,16 @@ final class AppointmentListCard: AppointmentCardBase { title: "지도 열기", background: .blue2.withAlphaComponent(0.8), tint: .white, - font: UIFont.preferredFont(forTextStyle: .caption1) + font: .caption1, + edgeInsets: NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) ) + button.layer.cornerRadius = 8 button.configuration?.image = UIImage(systemName: "map") button.configuration?.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( - font: UIFont.preferredFont(forTextStyle: .caption1) + font: .preferredFont(forTextStyle: .caption1) ) - button.configuration?.cornerStyle = .fixed - button.configuration?.background.cornerRadius = 8 button.configuration?.imagePlacement = .leading button.configuration?.imagePadding = 6 - button.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) return button }()