From b2953900ea465654eb33249922ee9a91c4f88ce9 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:41:23 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[#457]=203.3.0=20QA=20=EB=B0=98=EC=98=81:?= =?UTF-8?q?=20=EC=B0=9C=20=EC=A4=91=EB=B3=B5=20=EB=B3=91=ED=95=A9,=20?= =?UTF-8?q?=EC=B9=A9=20=EC=98=A4=EB=B2=84=ED=94=8C=EB=A1=9C=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AC=ED=95=91,=20=EC=B2=B4=ED=81=AC=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=81=AC=EA=B8=B0=C2=B7=EC=A0=95=EB=A0=AC,=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=A0=84=ED=99=98=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=EC=9C=A0=EC=A7=80,=20ja/vi=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EC=98=81=EC=96=B4=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Like/PartnershipLikeManager.swift | 30 ++++++++++++++++- .../ChangeMenuTableResponse+Display.swift | 5 +-- .../Like/View/LikedPartnershipCell.swift | 5 +-- .../LikedPartnershipEditViewController.swift | 14 +++++--- .../MainMapViewController.swift | 4 +-- .../Sources/Utility/Literal/AppLanguage.swift | 3 +- .../Utility/UIComponent/FilterChipBar.swift | 3 +- .../UnitTests/MealMenuDisplayTests.swift | 14 ++++++-- .../UnitTests/PartnershipLikeLogicTests.swift | 33 +++++++++++++++++++ 9 files changed, 94 insertions(+), 17 deletions(-) diff --git a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift index e72e7c28..60bc8918 100644 --- a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift +++ b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift @@ -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) @@ -199,6 +201,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) diff --git a/EATSSU/App/Sources/Presentation/Home/Model/ChangeMenuTableResponse+Display.swift b/EATSSU/App/Sources/Presentation/Home/Model/ChangeMenuTableResponse+Display.swift index b15f8ebe..2c8db11e 100644 --- a/EATSSU/App/Sources/Presentation/Home/Model/ChangeMenuTableResponse+Display.swift +++ b/EATSSU/App/Sources/Presentation/Home/Model/ChangeMenuTableResponse+Display.swift @@ -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는 서버가 영어 폴백) diff --git a/EATSSU/App/Sources/Presentation/Like/View/LikedPartnershipCell.swift b/EATSSU/App/Sources/Presentation/Like/View/LikedPartnershipCell.swift index f3e92a6c..8973e40e 100644 --- a/EATSSU/App/Sources/Presentation/Like/View/LikedPartnershipCell.swift +++ b/EATSSU/App/Sources/Presentation/Like/View/LikedPartnershipCell.swift @@ -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 diff --git a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift index ec123230..467e741d 100644 --- a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift +++ b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift @@ -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 @@ -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) } @@ -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 @@ -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 diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index dc8bab9f..33b9d9b3 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -365,9 +365,7 @@ final class MainMapViewController: BaseViewController { MapAnalyticsManager.shared.logClickGoodPriceCategory(category: goodPriceCategory) loadGoodPriceMarkers() } - - // 필터가 바뀌면 캠퍼스 주변으로 되돌려 새 마커가 바로 보이게 한다 (필터 전환 전 동작과 동일) - setInitialCameraPosition(animated: true) + // 필터 전환 시 카메라는 보고 있던 위치를 그대로 유지한다 (QA) } /// 축제 → click_map_festival, 그 외 → click_map_mine diff --git a/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift b/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift index 6f6bbdb5..dbcba1da 100644 --- a/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift +++ b/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift @@ -31,7 +31,8 @@ enum AppLanguage: String, CaseIterable { /// 미지원 언어를 보내면 서버가 한국어를 돌려주긴 하지만, 대표메뉴만 표시하는 규칙이 잘못 켜지지 않도록 /// 식단 API는 이 조건으로만 언어를 전달한다. 서버가 지원을 넓히면 여기만 수정한다. var supportsMealTranslation: Bool { - self == .english + // 변동식단은 서버가 영어 번역만 제공하므로, 비한국어 언어는 모두 영어 표기를 사용한다 (QA) + self != .korean } var title: String { diff --git a/EATSSU/App/Sources/Utility/UIComponent/FilterChipBar.swift b/EATSSU/App/Sources/Utility/UIComponent/FilterChipBar.swift index 9604b9ec..7968a878 100644 --- a/EATSSU/App/Sources/Utility/UIComponent/FilterChipBar.swift +++ b/EATSSU/App/Sources/Utility/UIComponent/FilterChipBar.swift @@ -55,7 +55,8 @@ final class FilterChipBar: BaseUIView { scrollView.showsHorizontalScrollIndicator = false scrollView.alwaysBounceHorizontal = true - scrollView.clipsToBounds = false + // 우측에 고정 요소(편집 버튼, 찜 하트)가 있는 화면에서 칩이 그 위로 넘쳐 그려지지 않도록 잘라낸다 + scrollView.clipsToBounds = true scrollView.contentInset = UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset) stackView.axis = .horizontal diff --git a/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift b/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift index 2e097555..1efde501 100644 --- a/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift +++ b/EATSSU/Tests/UnitTests/MealMenuDisplayTests.swift @@ -34,6 +34,12 @@ final class MealMenuDisplayTests: XCTestCase { ) } + func test_일본어에서도_대표메뉴만_표시한다() { + languageSandbox.set(.japanese) + let meal = makeMeal([("Pork Cutlet", true), ("김치", false)]) + XCTAssertEqual(meal.displayMenus.map(\.name), ["Pork Cutlet"]) + } + func test_영어에서는_대표메뉴만_표시한다() { languageSandbox.set(.english) let meal = makeMeal([("Pork Cutlet", true), ("김치", false), ("밥", false)]) @@ -53,12 +59,16 @@ final class MealMenuDisplayTests: XCTestCase { XCTAssertEqual(meal.displayMenus.count, 2) } - func test_변동식단_언어_파라미터는_영어만_전달한다() { + func test_변동식단_언어_파라미터는_비한국어_전부_EN을_전달한다() { + // 서버가 변동식단 번역을 영어로만 제공하므로 ja/vi도 EN을 요청한다 (QA) languageSandbox.set(.english) XCTAssertEqual(ChangeMenuTableResponse.mealLanguageParameter, "EN") languageSandbox.set(.japanese) - XCTAssertNil(ChangeMenuTableResponse.mealLanguageParameter) + XCTAssertEqual(ChangeMenuTableResponse.mealLanguageParameter, "EN") + + languageSandbox.set(.vietnamese) + XCTAssertEqual(ChangeMenuTableResponse.mealLanguageParameter, "EN") languageSandbox.set(.korean) XCTAssertNil(ChangeMenuTableResponse.mealLanguageParameter) diff --git a/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift b/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift index 053fd313..0f01beb4 100644 --- a/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift +++ b/EATSSU/Tests/UnitTests/PartnershipLikeLogicTests.swift @@ -106,6 +106,39 @@ final class PartnershipLikeLogicTests: XCTestCase { XCTAssertEqual(sorted.map(\.storeName), ["기록됨", "미기록1", "미기록2"]) } + // MARK: - mergedByStore (같은 업소가 여러 줄로 온 응답 병합) + + func test_같은_업소가_여러_줄로_오면_한_줄로_병합된다() { + let row1 = makeStore(name: "A", infos: [makeInfo(id: 1, isLiked: true)]) + let row2 = makeStore(name: "A", infos: [makeInfo(id: 2, isLiked: true)]) + let other = makeStore(name: "B", infos: [makeInfo(id: 3)]) + + let merged = PartnershipLikeManager.mergedByStore([row1, other, row2]) + + XCTAssertEqual(merged.map(\.storeName), ["A", "B"]) + XCTAssertEqual(merged[0].partnershipIds, [1, 2]) + } + + func test_병합시_중복_항목_id는_한_번만_남는다() { + let row1 = makeStore(name: "A", infos: [makeInfo(id: 1), makeInfo(id: 2)]) + let row2 = makeStore(name: "A", infos: [makeInfo(id: 2), makeInfo(id: 3)]) + + let merged = PartnershipLikeManager.mergedByStore([row1, row2]) + + XCTAssertEqual(merged.count, 1) + XCTAssertEqual(merged[0].partnershipIds, [1, 2, 3]) + } + + func test_좌표가_다르면_다른_업소로_병합하지_않는다() { + let a = makeStore(name: "A", infos: [makeInfo(id: 1)]) + let b = PartnershipDTO( + storeName: "A", longitude: 127.0, latitude: 37.5, restaurantType: "RESTAURANT", + naverMapUrl: nil, kakaoMapUrl: nil, partnershipInfos: [makeInfo(id: 2)] + ) + + XCTAssertEqual(PartnershipLikeManager.mergedByStore([a, b]).count, 2) + } + // MARK: - storeKey func test_업체_키는_이름과_좌표로_구성된다() { From 5481c5325ea01a21a559e95a64a2eeb3c48efced Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:17:41 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[#457]=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=B0=A9=ED=95=9C=EA=B0=80=EA=B2=A9=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=ED=98=84=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A1=9C=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20(=EA=B6=8C=ED=95=9C=20=EC=97=86=EC=9C=BC=EB=A9=B4?= =?UTF-8?q?=20=EC=88=AD=EC=8B=A4=EB=8C=80=20=EC=9C=A0=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController+Location.swift | 33 +++++++++++++++++++ .../MainMapViewController.swift | 11 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift index 0fe12767..add8633a 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift @@ -8,6 +8,8 @@ import UIKit import CoreLocation +import NMapsMap + // MARK: - Location Management extension MainMapViewController: CLLocationManagerDelegate { @@ -55,10 +57,39 @@ extension MainMapViewController: CLLocationManagerDelegate { return nil } + /// 비로그인 진입 시 현위치로 카메라 이동 + /// 권한 미결정이면 요청하고, 거부 상태면 아무것도 하지 않는다(숭실대 유지, 설정 유도 알럿도 띄우지 않음) + func moveToCurrentLocationIfAvailable() { + 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: false) + } else { + wantsInitialCurrentLocation = true + locationManager.requestLocation() + } + case .notDetermined: + wantsInitialCurrentLocation = true + locationManager.requestWhenInUseAuthorization() + default: + break + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard wantsInitialCurrentLocation, let location = locations.last else { return } + wantsInitialCurrentLocation = false + moveCamera(to: NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude), animated: true) + } + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: root.mapView.mapView.positionMode = .direction + if wantsInitialCurrentLocation { + locationManager.requestLocation() + } case .denied, .restricted: if hasRequestedLocationPermission { @@ -96,6 +127,8 @@ extension MainMapViewController: CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + // 진입 시 현위치 조회 실패는 조용히 포기하고 기본 위치(숭실대)를 유지한다 + wantsInitialCurrentLocation = false if let clError = error as? CLError { switch clError.code { case .denied: diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 33b9d9b3..e014dc95 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -43,6 +43,8 @@ final class MainMapViewController: BaseViewController { var currentDepartmentId: Int? var currentCollegeId: Int? var hasRequestedLocationPermission = false + /// 비로그인 진입 시 현위치로 이동하기 위해 권한/위치 응답을 기다리는 중인지 + var wantsInitialCurrentLocation = false var clusterer: NMCClusterer? @@ -159,7 +161,7 @@ final class MainMapViewController: BaseViewController { locationManager.delegate = self configureNavigationBar() - setInitialCameraPosition(animated: false) + setEntryCameraPosition() setupLocationButtonObserver() setupMarkerTapHandler() applyTabUI() @@ -438,6 +440,13 @@ final class MainMapViewController: BaseViewController { } } + /// 진입 시 카메라 위치. 로그인(탭 지도)은 숭실대 상권, 비로그인(단독 착한가격)은 현위치(권한 없으면 숭실대) + private func setEntryCameraPosition() { + setInitialCameraPosition(animated: false) + guard mode == .standaloneGoodPrice else { return } + moveToCurrentLocationIfAvailable() + } + func setInitialCameraPosition(animated: Bool) { moveCamera( to: NMGLatLng(lat: CameraConstants.initialLatitude, lng: CameraConstants.initialLongitude), From 2441876d250f393e8b98d3bd3907feb43dedf697 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:45:16 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[#457]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=84=A4=EB=AA=85=20=EC=A4=84=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?6pt=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PartnershipDetailSheetViewController.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 5c4aeb3f..7cc3ab79 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -264,10 +264,15 @@ final class PartnershipDetailSheetViewController: BaseViewController { titleDateLabel.attributedText = attrText let descriptionLabel = UILabel() - descriptionLabel.font = .body3 descriptionLabel.textColor = EATSSUDesignColors.Color.gray700 descriptionLabel.numberOfLines = 0 - descriptionLabel.text = info.description + // 여러 줄 설명은 줄간격을 벌려 가독성을 확보한다 (피그마 수치 확정 시 조정) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 6 + descriptionLabel.attributedText = NSAttributedString( + string: info.description, + attributes: [.paragraphStyle: paragraphStyle, .font: UIFont.body3] + ) let contentStack = UIStackView(arrangedSubviews: [titleDateLabel, descriptionLabel]) contentStack.axis = .vertical From 70ff1bf40c1da0032ff66cc7658397e04059ed6b Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:37:49 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[#457]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=8B=A8=EA=B3=BC=EB=8C=80=20=EC=A0=9C=EB=AA=A9?= =?UTF-8?q?=EA=B3=BC=20=EC=84=A4=EB=AA=85=20=EC=82=AC=EC=9D=B4=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=204=E2=86=9210pt=20=ED=99=95=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/PartnershipDetailSheetViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 7cc3ab79..c44de23f 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -276,7 +276,8 @@ final class PartnershipDetailSheetViewController: BaseViewController { let contentStack = UIStackView(arrangedSubviews: [titleDateLabel, descriptionLabel]) contentStack.axis = .vertical - contentStack.spacing = 4 + // 설명 내부 줄간격(6)보다 확실히 넓혀 단과대 제목 블록이 구분되게 한다 + contentStack.spacing = 10 let separator = UIView() separator.backgroundColor = EATSSUDesignColors.Color.gray300 From 2ef0071a272c2f8b436c53155e893ba8fd9edfee Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:41:02 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[#457]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=95=AD=EB=AA=A9=20=EC=97=AC=EB=B0=B1=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=20(=EC=A0=9C=EB=AA=A9-=EC=84=A4=EB=AA=85=2012pt,=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EA=B0=84=2012pt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PartnershipDetailSheetViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index c44de23f..4e57a59e 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -276,8 +276,8 @@ final class PartnershipDetailSheetViewController: BaseViewController { let contentStack = UIStackView(arrangedSubviews: [titleDateLabel, descriptionLabel]) contentStack.axis = .vertical - // 설명 내부 줄간격(6)보다 확실히 넓혀 단과대 제목 블록이 구분되게 한다 - contentStack.spacing = 10 + // 설명 내부 줄간격(6)보다 확실히 넓혀 단과대 제목 블록이 구분되게 한다 (Frame 43540 기준 + 여백 보강) + contentStack.spacing = 12 let separator = UIView() separator.backgroundColor = EATSSUDesignColors.Color.gray300 @@ -288,12 +288,12 @@ final class PartnershipDetailSheetViewController: BaseViewController { let container = UIStackView(arrangedSubviews: [contentStack, separator]) container.axis = .vertical - container.spacing = 10 + container.spacing = 12 let paddedContainer = UIView() paddedContainer.addSubview(container) container.snp.makeConstraints { - $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0)) + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 12, right: 0)) } return paddedContainer From febb0b19631bbfd6b3c1ded0922f1d7a2b91e1c1 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:49:42 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[#457]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=82=A0=EC=A7=9C=C2=B7=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=83=89=EC=83=81=EC=9D=84=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20(gray500/gra?= =?UTF-8?q?y600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/PartnershipDetailSheetViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 4e57a59e..39acb1ed 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -256,7 +256,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { attrText.addAttributes([ .font: UIFont.caption2, - .foregroundColor: EATSSUDesignColors.Color.gray700, + .foregroundColor: EATSSUDesignColors.Color.gray500, // 디자인 #9D9D9D .baselineOffset: +1 ], range: dateRange) @@ -264,7 +264,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { titleDateLabel.attributedText = attrText let descriptionLabel = UILabel() - descriptionLabel.textColor = EATSSUDesignColors.Color.gray700 + descriptionLabel.textColor = EATSSUDesignColors.Color.gray600 // 디자인 #565656 descriptionLabel.numberOfLines = 0 // 여러 줄 설명은 줄간격을 벌려 가독성을 확보한다 (피그마 수치 확정 시 조정) let paragraphStyle = NSMutableParagraphStyle() From f2b58039f3e97162497903e66241e071d162cf8c Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:51:27 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[#457]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=97=85=EC=A2=85=20=EB=9D=BC=EB=B2=A8=20=EC=83=89?= =?UTF-8?q?=EC=83=81=EC=9D=84=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=92(gr?= =?UTF-8?q?ay500)=EC=9C=BC=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 --- .../ViewController/PartnershipDetailSheetViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 39acb1ed..cbcb157d 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -72,7 +72,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { typeIconImageView.snp.makeConstraints { $0.width.height.equalTo(18) } typeTextLabel.font = .body2 - typeTextLabel.textColor = .gray + typeTextLabel.textColor = .gray500 // 디자인 #9D9D9D (시스템 .gray는 더 어두움) typeStackView.axis = .horizontal typeStackView.alignment = .center From fa31ea46763170edb5d6eebc5664823e8ce34058 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:58:47 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[#457]=20=EC=B0=9C=20=ED=8E=B8=EC=A7=91?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=8B=AB=EA=B8=B0=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=81=AC=EA=B8=B0=EB=A5=BC=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=92(12pt)=EC=9C=BC=EB=A1=9C=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 --- .../ViewController/LikedPartnershipEditViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift index 467e741d..055e4562 100644 --- a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift +++ b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift @@ -138,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) From 1f5cb9b9197f9ecee5301c2eca654584bf8bae60 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:07:46 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[#457]=20=EC=B0=9C=20=ED=86=A0=EA=B8=80/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EB=94=94?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EC=8B=A4=ED=8C=A8=20=ED=95=AD=EB=AA=A9=20id=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift | 3 +++ .../ViewController/LikedPartnershipEditViewController.swift | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift index 60bc8918..1ad36bb3 100644 --- a/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift +++ b/EATSSU/App/Sources/Data/Like/PartnershipLikeManager.swift @@ -142,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() diff --git a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift index 055e4562..fbb52f18 100644 --- a/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift +++ b/EATSSU/App/Sources/Presentation/Like/ViewController/LikedPartnershipEditViewController.swift @@ -186,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) } From ce0cb60efcf5ea28926d2fc2a3999880eee2347e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:15:11 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[#457]=20=EC=B0=9C=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=97=B0=20=EC=83=81=EC=84=B8=EB=8A=94=20?= =?UTF-8?q?=EB=82=B4=20=EC=A0=9C=ED=9C=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=98=EA=B3=A0=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=ED=9C=B4=20=ED=95=AD=EB=AA=A9=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EA=B8=B0=EC=A4=80=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 5 ++++- .../PartnershipDetailSheetViewController.swift | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index e014dc95..102035b9 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -221,7 +221,10 @@ final class MainMapViewController: BaseViewController { } pendingDetailStore = nil moveCamera(to: NMGLatLng(lat: store.latitude, lng: store.longitude), animated: false) - showPartnershipDetail(for: store) + // 찜 원본 DTO는 모든 단과대 제휴를 담고 있어, 지도 마커와 동일하게 내 제휴 데이터로 표시한다 + // (내 제휴에 없으면 — 학과 변경 등 — 원본으로 폴백, 시트에서 내용 기준 중복 제거) + let display = cachedMyPartnerships.first { $0.storeKey == store.storeKey } ?? store + showPartnershipDetail(for: display, likeTarget: store) } /// 찜 목록에서 넘어온 경우에만 뒤로가기(찜 탭 복귀) 버튼을 보여준다 diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index cbcb157d..3b854b38 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -164,8 +164,17 @@ final class PartnershipDetailSheetViewController: BaseViewController { .first { $0.restaurantType == partnership.restaurantType }? .title ?? partnership.restaurantType - for (index, info) in partnership.partnershipInfos.enumerated() { - let isLast = index == partnership.partnershipInfos.count - 1 + // 서버 데이터에 같은 제휴가 다른 id로 중복 존재할 수 있어 내용 기준으로 걸러 표시한다 + var seen = Set() + let displayInfos = partnership.partnershipInfos.filter { info in + let key = [ + info.collegeName ?? "", info.departmentName ?? "", + info.description, info.startDate, info.endDate + ].joined(separator: "|") + return seen.insert(key).inserted + } + for (index, info) in displayInfos.enumerated() { + let isLast = index == displayInfos.count - 1 let card = makeInfoCard(info: info, isLast: isLast) infoListStackView.addArrangedSubview(card) } From f6312e6d417379b7a8e79fc851a52ccde87fbe93 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:24:40 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[#457]=20=EC=B0=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=83=81=EC=84=B8=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=20=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=ED=92=80=EB=A6=AC=EB=8A=94=20=EC=A4=8C(17)=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=ED=99=95=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 102035b9..b7ef0ee9 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -23,6 +23,8 @@ final class MainMapViewController: BaseViewController { static let initialLatitude = 37.4960 static let initialLongitude = 126.9555 static let initialZoom: Double = 14.7 + /// 특정 업체를 보여줄 때의 줌. 이웃 마커와 클러스터로 뭉치지 않을 만큼 당긴다 + static let detailZoom: Double = 17 static let animationDuration: TimeInterval = 0.3 } @@ -220,7 +222,11 @@ final class MainMapViewController: BaseViewController { return } pendingDetailStore = nil - moveCamera(to: NMGLatLng(lat: store.latitude, lng: store.longitude), animated: false) + moveCamera( + to: NMGLatLng(lat: store.latitude, lng: store.longitude), + zoom: CameraConstants.detailZoom, + animated: false + ) // 찜 원본 DTO는 모든 단과대 제휴를 담고 있어, 지도 마커와 동일하게 내 제휴 데이터로 표시한다 // (내 제휴에 없으면 — 학과 변경 등 — 원본으로 폴백, 시트에서 내용 기준 중복 제거) let display = cachedMyPartnerships.first { $0.storeKey == store.storeKey } ?? store @@ -457,9 +463,9 @@ final class MainMapViewController: BaseViewController { ) } - /// 지정 좌표로 카메라 이동 (줌은 초기값 고정) - func moveCamera(to position: NMGLatLng, animated: Bool) { - let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: CameraConstants.initialZoom) + /// 지정 좌표로 카메라 이동 + func moveCamera(to position: NMGLatLng, zoom: Double = CameraConstants.initialZoom, animated: Bool) { + let cameraUpdate = NMFCameraUpdate(scrollTo: position, zoomTo: zoom) if animated { cameraUpdate.animation = .easeIn From d03f73190687446f3a3881f4aef8f03d14e94f48 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:34:19 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[#457]=20CI=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EC=9D=BC=EC=8B=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e8480d34..2f97a400 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -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), retrying in 30s" + sleep 30 + done + exit 1 - name: Create secret xcconfig files env: From 3106d4145e42918db31a044e55654da562ea6acd Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:48:19 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[#457]=20=EC=B0=A9=ED=95=9C=EA=B0=80?= =?UTF-8?q?=EA=B2=A9=20=EC=A7=80=EB=8F=84=EB=8A=94=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=97=AC=EB=B6=80=EC=99=80=20=EB=AC=B4=EA=B4=80?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=9C=84=EC=B9=98=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=9E=88=EC=9C=BC=EB=A9=B4=20=ED=98=84=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=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 --- .../MainMapViewController+Location.swift | 10 +++++----- .../Map/ViewController/MainMapViewController.swift | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift index add8633a..eeed1202 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift @@ -57,14 +57,14 @@ extension MainMapViewController: CLLocationManagerDelegate { return nil } - /// 비로그인 진입 시 현위치로 카메라 이동 - /// 권한 미결정이면 요청하고, 거부 상태면 아무것도 하지 않는다(숭실대 유지, 설정 유도 알럿도 띄우지 않음) - func moveToCurrentLocationIfAvailable() { + /// 착한가격 지도에서 현위치로 카메라 이동 (진입·탭 전환 공통) + /// 권한 미결정이면 요청하고, 거부 상태면 숭실대 상권으로 이동한다 (설정 유도 알럿은 띄우지 않음) + 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: false) + moveCamera(to: NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude), animated: animated) } else { wantsInitialCurrentLocation = true locationManager.requestLocation() @@ -73,7 +73,7 @@ extension MainMapViewController: CLLocationManagerDelegate { wantsInitialCurrentLocation = true locationManager.requestWhenInUseAuthorization() default: - break + setInitialCameraPosition(animated: animated) } } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index b7ef0ee9..fa13d675 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -343,7 +343,14 @@ final class MainMapViewController: BaseViewController { private func switchTab(to tab: MapTab) { guard currentTab != tab else { return } currentTab = tab - setInitialCameraPosition(animated: true) + switch tab { + case .partnership: + // 학교 제휴는 숭실대 상권 기준 + setInitialCameraPosition(animated: true) + case .goodPrice: + // 착한가격은 위치 권한이 있으면 항상 현위치 기준, 없으면 숭실대 + moveToCurrentLocationIfAvailable(animated: true) + } switch tab { case .partnership: From 2498b26360f99a059014da1461aaece4111d5e83 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:02:00 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[#457]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81:=20=EC=B0=9C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EB=8A=94=20=EB=82=B4=20=EC=A0=9C=ED=9C=B4=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=9B=84=20=ED=91=9C=EC=8B=9C,=20=EB=8A=A6?= =?UTF-8?q?=EC=9D=80=20=EC=9C=84=EC=B9=98=20=EC=9D=91=EB=8B=B5=20=EA=B0=80?= =?UTF-8?q?=EB=93=9C,=20CI=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 4 +-- .../MainMapViewController+Location.swift | 14 +++++++- .../MainMapViewController+Network.swift | 2 ++ .../MainMapViewController.swift | 35 +++++++++++++++++-- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2f97a400..998f7540 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,8 +31,8 @@ jobs: if sudo xcodebuild -downloadPlatform iOS; then exit 0 fi - echo "downloadPlatform failed (attempt $attempt), retrying in 30s" - sleep 30 + echo "downloadPlatform failed (attempt $attempt)" + [ "$attempt" -lt 3 ] && sleep 30 done exit 1 diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift index eeed1202..22b6edaa 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Location.swift @@ -78,7 +78,8 @@ extension MainMapViewController: CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard wantsInitialCurrentLocation, let location = locations.last else { return } + // 착한가격 지도를 보고 있을 때만 이동 (다른 탭으로 간 뒤 늦게 온 응답이 카메라를 덮지 않게) + 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) } @@ -143,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 { diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift index c0bd0e53..2f370f3d 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift @@ -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)") diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index fa13d675..884862e8 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -54,6 +54,9 @@ final class MainMapViewController: BaseViewController { var cachedAllPartnerships: [PartnershipDTO] = [] /// 내 학과 제휴 캐시 (업종 칩 필터용). 탭바 재탭·학과 변경 시 비움 var cachedMyPartnerships: [PartnershipDTO] = [] + /// 내 제휴를 한 번이라도 받았는지 (찜 → 상세 진입 시 전체 단과대 원본이 노출되지 않게 대기 판단용) + var hasFetchedMyPartnerships = false + private var isLoadingMyPartnershipsForDetail = false /// 착한가격업소 전체 목록 캐시 (카테고리 필터링용) var cachedGoodPriceStores: [GoodPriceStoreDTO] = [] @@ -161,6 +164,7 @@ final class MainMapViewController: BaseViewController { super.viewDidLoad() locationManager.delegate = self + root.mapView.mapView.addCameraDelegate(delegate: self) configureNavigationBar() setEntryCameraPosition() @@ -212,7 +216,7 @@ final class MainMapViewController: BaseViewController { presentPendingDetailIfNeeded() } - private func presentPendingDetailIfNeeded() { + func presentPendingDetailIfNeeded() { guard let store = pendingDetailStore, viewIfLoaded?.window != nil, presentedViewController == nil else { return } @@ -221,6 +225,11 @@ final class MainMapViewController: BaseViewController { if departmentLoadState == .loaded { pendingDetailStore = nil } return } + // 내 제휴 응답 전이면 받아온 뒤 연다 (전체 단과대 원본 시트가 잠깐 노출되는 것 방지) + if !hasFetchedMyPartnerships, cachedMyPartnerships.isEmpty { + loadMyPartnershipsForPendingDetail() + return + } pendingDetailStore = nil moveCamera( to: NMGLatLng(lat: store.latitude, lng: store.longitude), @@ -233,6 +242,26 @@ final class MainMapViewController: BaseViewController { showPartnershipDetail(for: display, likeTarget: store) } + /// 찜 → 상세 진입용 내 제휴 확보. 마커 로드 세대에 영향을 주지 않도록 캐시만 채운다 + /// 실패해도 완료 표시 후 다시 호출해, 시트가 원본(중복 제거) 폴백으로라도 열리게 한다 + private func loadMyPartnershipsForPendingDetail() { + guard !isLoadingMyPartnershipsForDetail else { return } + isLoadingMyPartnershipsForDetail = true + NetworkService.shared.request( + MyRouter.getMyPartnerships, + responseType: [PartnershipDTO].self, + useAuth: true + ) { [weak self] result in + guard let self else { return } + self.isLoadingMyPartnershipsForDetail = false + if case .success(let partnerships) = result, self.cachedMyPartnerships.isEmpty { + self.cachedMyPartnerships = partnerships + } + self.hasFetchedMyPartnerships = true + self.presentPendingDetailIfNeeded() + } + } + /// 찜 목록에서 넘어온 경우에만 뒤로가기(찜 탭 복귀) 버튼을 보여준다 private func updateLikeReturnButton() { guard returnsToLikeTab else { @@ -345,7 +374,8 @@ final class MainMapViewController: BaseViewController { currentTab = tab switch tab { case .partnership: - // 학교 제휴는 숭실대 상권 기준 + // 학교 제휴는 숭실대 상권 기준. 착한가격에서 걸어둔 현위치 이동 대기도 취소한다 + wantsInitialCurrentLocation = false setInitialCameraPosition(animated: true) case .goodPrice: // 착한가격은 위치 권한이 있으면 항상 현위치 기준, 없으면 숭실대 @@ -449,6 +479,7 @@ final class MainMapViewController: BaseViewController { case .partnership: cachedAllPartnerships = [] cachedMyPartnerships = [] + hasFetchedMyPartnerships = false refreshPartnershipTab() case .goodPrice: cachedGoodPriceStores = []