Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
b295390
[#457] 3.3.0 QA 반영: 찜 중복 병합, 칩 오버플로 클리핑, 체크 아이콘 크기·정렬, 필터 전환 카메라 유지, …
Hrepay Sep 1, 2026
5481c53
[#457] 비로그인 착한가격 지도 진입 시 현위치로 카메라 시작 (권한 없으면 숭실대 유지)
Hrepay Sep 1, 2026
2441876
[#457] 제휴 상세 설명 줄간격 6pt 적용
Hrepay Sep 1, 2026
70ff1bf
[#457] 제휴 상세 단과대 제목과 설명 사이 간격 4→10pt 확대
Hrepay Sep 1, 2026
2ef0071
[#457] 제휴 상세 항목 여백 확대 (제목-설명 12pt, 항목 간 12pt)
Hrepay Sep 1, 2026
febb0b1
[#457] 제휴 상세 날짜·설명 색상을 디자인 값으로 수정 (gray500/gray600)
Hrepay Sep 1, 2026
f2b5803
[#457] 제휴 상세 업종 라벨 색상을 디자인 값(gray500)으로 통일
Hrepay Sep 1, 2026
fa31ea4
[#457] 찜 편집 화면 닫기 아이콘 크기를 디자인 값(12pt)으로 수정
Hrepay Sep 1, 2026
1f5cb9b
[#457] 찜 토글/삭제 실패 시 디버그 로그 추가 (실패 항목 id 포함)
Hrepay Sep 1, 2026
ce0cb60
[#457] 찜 목록에서 연 상세는 내 제휴 데이터로 표시하고 시트 제휴 항목 내용 기준 중복 제거
Hrepay Sep 1, 2026
f6312e6
[#457] 찜에서 지도 상세 진입 시 클러스터가 풀리는 줌(17)으로 확대
Hrepay Sep 2, 2026
d03f731
[#457] CI 시뮬레이터 런타임 다운로드 일시 실패 재시도 추가
Hrepay Sep 2, 2026
3106d41
[#457] 착한가격 지도는 로그인 여부와 무관하게 위치 권한 있으면 현위치 기준으로 이동
Hrepay Sep 2, 2026
2498b26
[#457] 코드리뷰 반영: 찜 상세는 내 제휴 응답 후 표시, 늦은 위치 응답 가드, CI 재시도 대기 정리
Hrepay Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ jobs:
xcodebuild -version

# 러너 이미지에는 기본 Xcode용 런타임만 있어, 고정한 Xcode에 맞는 iOS 시뮬레이터 런타임을 내려받는다
# 러너의 일시적 다운로드 실패("Unable to connect to simulator")가 있어 재시도한다
- name: Download iOS simulator runtime
run: sudo xcodebuild -downloadPlatform iOS
run: |
for attempt in 1 2 3; do
if sudo xcodebuild -downloadPlatform iOS; then
exit 0
fi
echo "downloadPlatform failed (attempt $attempt)"
[ "$attempt" -lt 3 ] && sleep 30
done
exit 1

- name: Create secret xcconfig files
env:
Expand Down
33 changes: 32 additions & 1 deletion EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ final class PartnershipLikeManager {
) { [weak self] result in
guard let self else { return }
switch result {
case .success(let stores):
case .success(let rawStores):
// 조회 중에 토글이 있었으면 이 응답은 이미 낡은 것이므로 로컬 상태를 유지한다
guard version == self.stateVersion else {
completion(.success(self.likedStores))
return
}
// 서버가 같은 업소를 찜 항목 단위로 여러 줄 반환할 수 있어 업소 단위로 병합한다
let stores = Self.mergedByStore(rawStores)
self.toggledStates = [:]
self.likedPartnershipIds = Set(stores.flatMap(Self.likedIds(in:)))
self.likedStores = self.sortedByRecent(stores)
Expand Down Expand Up @@ -140,6 +142,9 @@ final class PartnershipLikeManager {
case .success:
toggledIds.append(id)
case .failure(let error):
#if DEBUG
print("찜 토글 실패 partnershipId=\(id):", error)
#endif
if firstError == nil { firstError = error }
}
group.leave()
Expand Down Expand Up @@ -199,6 +204,32 @@ final class PartnershipLikeManager {
.map(\.id)
}

/// 같은 업소(storeKey)가 여러 줄로 온 응답을 한 줄로 병합. 첫 등장 순서를 유지하고 제휴 항목은 id 기준으로 합친다
static func mergedByStore(_ stores: [PartnershipDTO]) -> [PartnershipDTO] {
var order: [String] = []
var byKey: [String: PartnershipDTO] = [:]
for store in stores {
guard let existing = byKey[store.storeKey] else {
order.append(store.storeKey)
byKey[store.storeKey] = store
continue
}
let knownIds = Set(existing.partnershipInfos.map(\.id))
let mergedInfos = existing.partnershipInfos
+ store.partnershipInfos.filter { !knownIds.contains($0.id) }
byKey[store.storeKey] = PartnershipDTO(
storeName: existing.storeName,
longitude: existing.longitude,
latitude: existing.latitude,
restaurantType: existing.restaurantType,
naverMapUrl: existing.naverMapUrl ?? store.naverMapUrl,
kakaoMapUrl: existing.kakaoMapUrl ?? store.kakaoMapUrl,
partnershipInfos: mergedInfos
)
}
return order.compactMap { byKey[$0] }
}

/// 찜 목록 응답에서 실제 찜된 항목 id. 플래그가 하나도 없으면(구버전 응답) 소속 항목 전부로 간주
static func likedIds(in store: PartnershipDTO) -> [Int] {
let flagged = store.partnershipInfos.filter(\.isLiked).map(\.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ extension ChangeMenuTableResponse {
AppLanguageManager.shared.currentLanguage.supportsMealTranslation
}

/// 변동식단(`/meals`, `menus-info`) 조회 시 서버에 넘길 `language` 파라미터. 번역 미지원 언어는 nil(한국어 응답)
/// 변동식단(`/meals`, `menus-info`) 조회 시 서버에 넘길 `language` 파라미터.
/// 서버가 영어 번역만 제공하므로 비한국어 언어는 모두 EN을 요청한다 (한국어는 nil)
static var mealLanguageParameter: String? {
let language = AppLanguageManager.shared.currentLanguage
return language.supportsMealTranslation ? language.serverCode : nil
return language.supportsMealTranslation ? AppLanguage.english.serverCode : nil
}

/// 고정메뉴(`/menus`) 조회 시 서버에 넘길 `language` 파라미터. 한국어 외 전부 전달 (EN/JA 번역, VI는 서버가 영어 폴백)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ final class LikedPartnershipCell: UITableViewCell {
private enum Layout {
static let horizontalInset: CGFloat = 24
static let iconSize: CGFloat = 36
static let checkSize: CGFloat = 18
static let checkLeading: CGFloat = 27
// 에셋은 24pt 캔버스에 원 20pt(여백 2pt)라, 22pt로 렌더해야 디자인의 18pt 원 크기가 된다
static let checkSize: CGFloat = 22
static let checkLeading: CGFloat = 25
static let textSpacing: CGFloat = 12
static let checkTextSpacing: CGFloat = 15
static let topInset: CGFloat = 18
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ final class LikedPartnershipEditViewController: BaseViewController {

// MARK: - Constants

/// 디자인: 전체 선택 행 52, 체크 18 (leading 35), 구분선 2pt, 삭제 버튼 52/r8, 하단 safe area + 6
/// 디자인: 전체 선택 행 52, 구분선 2pt, 삭제 버튼 52/r8, 하단 safe area + 6
/// 체크 아이콘은 셀 체크(centerX 36)와 일직선이 되도록 leading을 맞춘다
private enum Layout {
static let horizontalInset: CGFloat = 24
static let headerHeight: CGFloat = 52
static let headerLeading: CGFloat = 35
static let headerLeading: CGFloat = 25
/// 셀과 동일 (에셋 24pt 캔버스의 원 20pt → 22pt 렌더 시 디자인 18pt)
static let checkIconWidth: CGFloat = 22
static let buttonHeight: CGFloat = 52
static let buttonCornerRadius: CGFloat = 8
static let buttonBottomInset: CGFloat = 6
Expand Down Expand Up @@ -50,7 +53,8 @@ final class LikedPartnershipEditViewController: BaseViewController {
// MARK: - Init

init(stores: [PartnershipDTO]) {
self.stores = stores
// 목록이 중복 행을 담고 있어도 전체 선택 판정이 어긋나지 않도록 업소 단위로 병합해 받는다
self.stores = PartnershipLikeManager.mergedByStore(stores)
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -64,7 +68,7 @@ final class LikedPartnershipEditViewController: BaseViewController {
view.backgroundColor = .white

var config = UIButton.Configuration.plain()
config.image = EATSSUDesignAsset.Images.icUncheck.image.resize(newWidth: 18)
config.image = EATSSUDesignAsset.Images.icUncheck.image.resize(newWidth: Layout.checkIconWidth)
config.imagePadding = 15
config.baseForegroundColor = .label
config.contentInsets = .zero
Expand Down Expand Up @@ -134,7 +138,7 @@ final class LikedPartnershipEditViewController: BaseViewController {
navigationItem.hidesBackButton = true
// 선택 중이어도 확인 없이 즉시 닫는다 (기획)
let closeItem = UIBarButtonItem(
image: EATSSUDesignAsset.Images.icClose.image.resize(newWidth: 24),
image: EATSSUDesignAsset.Images.icClose.image.resize(newWidth: 12), // 디자인 12x12 (stroke gray500)
style: .plain,
target: self,
action: #selector(didTapClose)
Expand All @@ -155,7 +159,7 @@ final class LikedPartnershipEditViewController: BaseViewController {
private func updateSelectAllButton() {
selectAllButton.configuration?.image = (isAllSelected
? EATSSUDesignAsset.Images.icCheck.image
: EATSSUDesignAsset.Images.icUncheck.image).resize(newWidth: 18)
: EATSSUDesignAsset.Images.icUncheck.image).resize(newWidth: Layout.checkIconWidth)
}

// MARK: - Actions
Expand All @@ -182,7 +186,10 @@ final class LikedPartnershipEditViewController: BaseViewController {
case .success:
self.navigationController?.popViewController(animated: true)
self.onDidDelete?()
case .failure:
case .failure(let error):
#if DEBUG
print("찜 삭제 실패:", error)
#endif
self.deleteButton.isEnabled = true
self.showToast(message: TextLiteral.Like.updateFailed, type: .danger)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import UIKit
import CoreLocation

import NMapsMap

// MARK: - Location Management

extension MainMapViewController: CLLocationManagerDelegate {
Expand Down Expand Up @@ -55,10 +57,40 @@ extension MainMapViewController: CLLocationManagerDelegate {
return nil
}

/// 착한가격 지도에서 현위치로 카메라 이동 (진입·탭 전환 공통)
/// 권한 미결정이면 요청하고, 거부 상태면 숭실대 상권으로 이동한다 (설정 유도 알럿은 띄우지 않음)
func moveToCurrentLocationIfAvailable(animated: Bool = false) {
switch locationManager.authorizationStatus {
case .authorizedWhenInUse, .authorizedAlways:
root.mapView.mapView.positionMode = .direction
if let location = locationManager.location {
moveCamera(to: NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude), animated: animated)
} else {
wantsInitialCurrentLocation = true
locationManager.requestLocation()
}
case .notDetermined:
wantsInitialCurrentLocation = true
locationManager.requestWhenInUseAuthorization()
default:
setInitialCameraPosition(animated: animated)
}
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 착한가격 지도를 보고 있을 때만 이동 (다른 탭으로 간 뒤 늦게 온 응답이 카메라를 덮지 않게)
guard wantsInitialCurrentLocation, currentTab == .goodPrice, let location = locations.last else { return }
wantsInitialCurrentLocation = false
moveCamera(to: NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude), animated: true)
Comment thread
Hrepay marked this conversation as resolved.
}

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedWhenInUse, .authorizedAlways:
root.mapView.mapView.positionMode = .direction
if wantsInitialCurrentLocation {
locationManager.requestLocation()
}

case .denied, .restricted:
if hasRequestedLocationPermission {
Expand Down Expand Up @@ -96,6 +128,8 @@ extension MainMapViewController: CLLocationManagerDelegate {
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// 진입 시 현위치 조회 실패는 조용히 포기하고 기본 위치(숭실대)를 유지한다
wantsInitialCurrentLocation = false
if let clError = error as? CLError {
switch clError.code {
case .denied:
Expand All @@ -110,6 +144,17 @@ extension MainMapViewController: CLLocationManagerDelegate {
}
}

// MARK: - NMFMapViewCameraDelegate

extension MainMapViewController: NMFMapViewCameraDelegate {
/// 사용자가 지도를 직접 움직이면 진입 시 걸어둔 현위치 이동을 취소한다 (늦은 응답이 조작을 덮지 않게)
func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) {
if reason == NMFMapChangedByGesture {
wantsInitialCurrentLocation = false
}
}
}

// MARK: - UIGestureRecognizerDelegate

extension MainMapViewController: UIGestureRecognizerDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ extension MainMapViewController {
switch result {
case .success(let partnerships):
self.cachedMyPartnerships = partnerships
self.hasFetchedMyPartnerships = true
self.applyPartnershipMarkers(from: partnerships, periodType: .normal)
self.presentPendingDetailIfNeeded()

case .failure(let error):
print("내 제휴 조회 실패: \(error.localizedDescription)")
Expand Down
Loading
Loading