Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions EATSSU/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@
"map.pub" = "Pub";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "No Department Info";

"map.kakaoMap" = "KakaoMap";

"map.naverMap" = "Naver Map";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "Location Permission Required";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
Expand Down
4 changes: 4 additions & 0 deletions EATSSU/App/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"map.pub" = "居酒屋";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "学科情報なし";

"map.kakaoMap" = "カカオマップ";

"map.naverMap" = "NAVERマップ";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "位置情報権限が必要です";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
Expand Down
4 changes: 4 additions & 0 deletions EATSSU/App/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"map.pub" = "주점";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "학과 정보 없음";

"map.kakaoMap" = "카카오맵";

"map.naverMap" = "네이버지도";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "위치 권한 필요";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
Expand Down
4 changes: 4 additions & 0 deletions EATSSU/App/Resources/vi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"map.pub" = "Quán nhậu";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "Không có thông tin ngành";

"map.kakaoMap" = "KakaoMap";

"map.naverMap" = "Naver Map";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "Cần quyền truy cập vị trí";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ struct PartnershipDTO: Codable {
let longitude: Double
let latitude: Double
let restaurantType: String
let naverMapUrl: String?
let kakaoMapUrl: String?
let partnershipInfos: [PartnershipInfoDTO]
}

Expand Down
86 changes: 86 additions & 0 deletions EATSSU/App/Sources/Data/Network/Service/KakaoLocalService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// KakaoLocalService.swift
// EATSSU
//
// Created by 황상환 on 7/18/26.
//

import Foundation

/// 카카오 로컬 API - 키워드 장소 검색 (카카오맵 place id 조회용)
final class KakaoLocalService {

// MARK: - Singleton

static let shared = KakaoLocalService()

private init() {}

// MARK: - Model

struct Place: Decodable {
let id: String
let placeName: String
let placeURL: String

enum CodingKeys: String, CodingKey {
case id
case placeName = "place_name"
case placeURL = "place_url"
}
}

private struct SearchResponse: Decodable {
let documents: [Place]
}

// MARK: - Properties

private var restAPIKey: String? {
guard let key = Bundle.main.object(forInfoDictionaryKey: "KAKAO REST API KEY") as? String,
!key.isEmpty else { return nil }
return key
}

// MARK: - Request

/// 가게명 + 좌표 기준으로 가장 가까운 장소 1건 검색 (키 미설정/실패 시 nil 반환)
func searchNearestPlace(
keyword: String,
latitude: Double,
longitude: Double,
completion: @escaping (Place?) -> Void
) {
guard let key = restAPIKey else {
completion(nil)
return
}

var components = URLComponents(string: "https://dapi.kakao.com/v2/local/search/keyword.json")
components?.queryItems = [
URLQueryItem(name: "query", value: keyword),
URLQueryItem(name: "x", value: "\(longitude)"),
URLQueryItem(name: "y", value: "\(latitude)"),
URLQueryItem(name: "radius", value: "300"),
URLQueryItem(name: "sort", value: "distance"),
URLQueryItem(name: "size", value: "1")
]

guard let url = components?.url else {
completion(nil)
return
}

var request = URLRequest(url: url)
request.setValue("KakaoAK \(key)", forHTTPHeaderField: "Authorization")

URLSession.shared.dataTask(with: request) { data, _, _ in
let place = data.flatMap {
try? JSONDecoder().decode(SearchResponse.self, from: $0).documents.first
}
DispatchQueue.main.async {
completion(place)
}
}.resume()
}
}
103 changes: 103 additions & 0 deletions EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// PartnershipMockData.swift
// EATSSU
//
// Created by 황상환 on 7/18/26.
//

#if DEBUG
import Foundation

/// 서버에 제휴 데이터가 없을 때 실기기 테스트용 Mock 데이터 (DEBUG 빌드 전용)
enum PartnershipMockData {

/// 숭실대 주변 가게 좌표 기반 샘플
static let samples: [PartnershipDTO] = [
PartnershipDTO(
storeName: "맘스터치 숭실대점",
longitude: 126.9550,
latitude: 37.4959,
restaurantType: "RESTAURANT",
naverMapUrl: nil,
kakaoMapUrl: nil,
partnershipInfos: [
PartnershipInfoDTO(
id: -1,
collegeName: "IT대학",
departmentName: nil,
likeCount: 0,
isLiked: false,
description: "학생증 인증하면 음료수 1개 증정",
startDate: "2025.09.03",
endDate: "2025.12.18",
periodType: .normal
),
PartnershipInfoDTO(
id: -2,
collegeName: nil,
departmentName: "컴퓨터학부",
likeCount: 0,
isLiked: false,
description: "학생증 인증하고 카카오페이 결제 시 10% 할인",
startDate: "2025.09.01",
endDate: "2025.12.31",
periodType: .normal
)
]
),
PartnershipDTO(
storeName: "스타벅스 숭실대입구역점",
longitude: 126.9538,
latitude: 37.4964,
restaurantType: "CAFE",
naverMapUrl: nil,
kakaoMapUrl: nil,
partnershipInfos: [
PartnershipInfoDTO(
id: -3,
collegeName: "경영대학",
departmentName: nil,
likeCount: 0,
isLiked: false,
description: "아메리카노 사이즈업 무료",
startDate: "2025.09.03",
endDate: "2025.12.18",
periodType: .normal
)
]
),
PartnershipDTO(
storeName: "역전할머니맥주 숭실대점",
longitude: 126.9532,
latitude: 37.4949,
restaurantType: "PUB",
naverMapUrl: nil,
kakaoMapUrl: nil,
partnershipInfos: [
PartnershipInfoDTO(
id: -4,
collegeName: "벤처중소기업센터",
departmentName: nil,
likeCount: 0,
isLiked: false,
description: "4인 이상 방문 시 안주 1개 서비스",
startDate: "2025.09.03",
endDate: "2025.12.18",
periodType: .normal
),
PartnershipInfoDTO(
id: -5,
collegeName: "총학생회",
departmentName: nil,
likeCount: 0,
isLiked: false,
description: "축제 기간 병맥주 1+1",
startDate: "2025.10.06",
endDate: "2025.10.08",
periodType: .festival
)
]
)
]
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -155,27 +155,22 @@ extension MainMapViewController {
partnerId: partnership.partnershipInfos.first?.id ?? -1
)

let detailVC = PartnershipDetailSheetViewController(
storeName: partnership.storeName,
restaurantType: partnership.restaurantType,
partnershipInfos: partnership.partnershipInfos
)
let detailVC = PartnershipDetailSheetViewController(partnership: partnership)

// 뷰가 로드된 후 높이 계산
detailVC.loadViewIfNeeded()

if let sheet = detailVC.sheetPresentationController {
let contentHeight = detailVC.calculatePreferredHeight()

// iOS 16.0 이상에서만 custom detent 사용
// 표시 후 safe area 확정 시 invalidateDetents()로 클로저가 재실행되어 높이가 보정됨
if #available(iOS 16.0, *) {
let customDetent = UISheetPresentationController.Detent.custom { _ in
return contentHeight
let customDetent = UISheetPresentationController.Detent.custom { [weak detailVC] _ in
detailVC?.calculatePreferredHeight()
}
sheet.detents = [customDetent]
sheet.detents = [customDetent, .large()]
} else {
// iOS 16.0 미만에서는 medium detent 사용
sheet.detents = [.medium()]
sheet.detents = [.medium(), .large()]
}

sheet.prefersGrabberVisible = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ extension MainMapViewController {
self.cachedAllPartnerships = []
self.displayMarkers([])
}

#if DEBUG
// 서버에 제휴 데이터가 없는 동안 Mock으로 대체 (DEBUG 빌드 전용)
if self.cachedAllPartnerships.isEmpty {
self.cachedAllPartnerships = PartnershipMockData.samples
self.applyCachedMarkers()
}
#endif
}
}

Expand Down Expand Up @@ -57,6 +65,8 @@ extension MainMapViewController {
longitude: partnership.longitude,
latitude: partnership.latitude,
restaurantType: partnership.restaurantType,
naverMapUrl: partnership.naverMapUrl,
kakaoMapUrl: partnership.kakaoMapUrl,
partnershipInfos: matchingInfos
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ final class MainMapViewController: BaseViewController {
collegeId: currentCollegeId,
majorId: currentDepartmentId
)
applyCachedMarkers()
refreshOrApplyCachedMarkers()
}

@objc private func didTapWhole() {
Expand All @@ -146,7 +146,16 @@ final class MainMapViewController: BaseViewController {
collegeId: currentCollegeId,
majorId: currentDepartmentId
)
applyCachedMarkers()
refreshOrApplyCachedMarkers()
}

/// 캐시가 비어있으면(내 제휴 모드로 시작해 전체 데이터를 아직 안 받은 경우) 서버에서 받아오고, 있으면 캐시로 필터링
private func refreshOrApplyCachedMarkers() {
if cachedAllPartnerships.isEmpty {
refreshAllPartnerships()
} else {
applyCachedMarkers()
}
}

@objc private func didTapMyOnly() {
Expand Down
Loading