Skip to content

Commit 110d1d7

Browse files
Merge pull request #172 from YAPP-Github/develop
[Release] 1.1.4 배포
2 parents 14eefe4 + 211936c commit 110d1d7

File tree

23 files changed

+179
-55
lines changed

23 files changed

+179
-55
lines changed

.github/workflows/release.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,24 @@ jobs:
6565
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
6666
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
6767

68-
# 8. Discord 결과 알림 (성공/실패 여부 전송)
68+
# 8. 배포 완료 후 서버로 최신 버전 정보 POST 요청
69+
- name: Send Version Update to Server
70+
if: success() # 배포가 성공적으로 끝났을 때만 실행
71+
run: |
72+
# Xcode 빌드 세팅에서 MARKETING_VERSION(앱 버전) 자동 추출
73+
APP_VERSION=$(xcodebuild -showBuildSettings -project ./Neki-iOS.xcodeproj -scheme Neki-iOS | grep " MARKETING_VERSION " | sed 's/[ ]*MARKETING_VERSION = //')
74+
75+
echo "추출된 최신 버전: $APP_VERSION"
76+
77+
# 추출된 버전을 JSON 바디에 담아서 서버로 POST 요청 (minVersion은 필요시 추후 수정)
78+
curl -X POST "${{ secrets.APP_VERSION_API_ADDRESS }}" \
79+
-H "Content-Type: application/json" \
80+
-d "{
81+
\"minVersion\": \"1.0.0\",
82+
\"currentVersion\": \"$APP_VERSION\"
83+
}"
84+
85+
# 9. Discord 결과 알림 (성공/실패 여부 전송)
6986
- name: Send Discord notification
7087
if: always() # 빌드 성공/실패 여부와 상관없이 무조건 실행
7188
run: |

Neki-iOS.xcodeproj/project.pbxproj

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,11 @@
309309
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
310310
CODE_SIGN_ENTITLEMENTS = "Neki-iOS/Neki-iOS.entitlements";
311311
CODE_SIGN_IDENTITY = "Apple Development";
312-
CODE_SIGN_STYLE = Automatic;
313-
CURRENT_PROJECT_VERSION = 6;
314-
DEVELOPMENT_TEAM = 586LZSS32L;
312+
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
313+
CODE_SIGN_STYLE = Manual;
314+
CURRENT_PROJECT_VERSION = 0;
315+
DEVELOPMENT_TEAM = "";
316+
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 586LZSS32L;
315317
ENABLE_PREVIEWS = YES;
316318
GENERATE_INFOPLIST_FILE = YES;
317319
INFOPLIST_FILE = "Neki-iOS/Info.plist";
@@ -334,6 +336,7 @@
334336
PRODUCT_BUNDLE_IDENTIFIER = "com.OneTen.Neki-dev";
335337
PRODUCT_NAME = "$(TARGET_NAME)";
336338
PROVISIONING_PROFILE_SPECIFIER = "";
339+
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev_neki;
337340
STRING_CATALOG_GENERATE_SYMBOLS = YES;
338341
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
339342
SUPPORTS_MACCATALYST = NO;
@@ -359,7 +362,7 @@
359362
CODE_SIGN_IDENTITY = "Apple Development";
360363
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
361364
CODE_SIGN_STYLE = Manual;
362-
CURRENT_PROJECT_VERSION = 6;
365+
CURRENT_PROJECT_VERSION = 0;
363366
DEVELOPMENT_TEAM = "";
364367
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 586LZSS32L;
365368
ENABLE_PREVIEWS = YES;
@@ -384,7 +387,7 @@
384387
PRODUCT_BUNDLE_IDENTIFIER = "com.OneTen.Neki-iOS";
385388
PRODUCT_NAME = "$(TARGET_NAME)";
386389
PROVISIONING_PROFILE_SPECIFIER = "";
387-
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.OneTen.Neki-iOS";
390+
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = neki_release;
388391
STRING_CATALOG_GENERATE_SYMBOLS = YES;
389392
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
390393
SUPPORTS_MACCATALYST = NO;

Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ struct MainTabCoordinator {
221221
state.selectedTab = .archive
222222
return .send(.archive(.root(.addPhotoFromQRScanner(imageID: imageID))))
223223

224+
case .destination(.presented(.qrScan(.addPhotoFromGalleryButtonTapped))):
225+
state.destination = nil
226+
return .run { send in
227+
await send(.onTapGallery)
228+
await send(.setPhotosPickerPresented(true))
229+
}
230+
224231
default:
225232
return .none
226233
}

Neki-iOS/Core/Sources/Auth/Sources/Presentation/Sources/View/LoginView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public struct LoginView: View {
4545
VStack(spacing: 10) {
4646
Image(.imgAppleLogin)
4747
.resizable()
48-
.frame(maxHeight: 52)
48+
.scaledToFit()
4949
.overlay {
5050
SignInWithAppleButton(.continue) { _ in
5151

@@ -60,7 +60,7 @@ public struct LoginView: View {
6060
} label: {
6161
Image(.imgKakaoLogin)
6262
.resizable()
63-
.frame(maxHeight: 52)
63+
.scaledToFit()
6464
}
6565
.onOpenURL { store.send(.handleKakaoOpenURL($0)) }
6666
}

Neki-iOS/Features/Map/Sources/Presentation/Sources/View/NaverMapView.swift

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ fileprivate enum Constants {
2626
static let zIndexSelected: Int = 100
2727

2828
// Clustering Settings
29-
static let clusterMaxZoom: Int = 15
30-
static let clusterMinZoom: Int = 4
31-
static let clusterScreenDistance: Double = 65.0
29+
static let clusterMaxZoom: Int = 20
30+
static let clusterMinZoom: Int = 12
31+
static let clusterThresholdZoomIn: Double = 60.0
32+
static let clusterThresholdZoomOut: Double = 70.0
3233
static let leafCaptionTextSize: CGFloat = 12.0
3334
static let clusterCaptionTextSize: CGFloat = 14.0
3435
static let captionColorHex: UInt = 0x202227
35-
static let brandClusteringThreshold: Double = 11.0
36+
static let brandClusteringThreshold: Double = 15.0
3637
}
3738

3839
struct NaverMapRepresentable: UIViewRepresentable {
@@ -179,7 +180,7 @@ extension NaverMapRepresentable {
179180
builder.clusterMarkerUpdater = BoothClusterMarkerUpdater(mapView: mapView)
180181
builder.maxClusteringZoom = Constants.clusterMaxZoom
181182
builder.minClusteringZoom = Constants.clusterMinZoom
182-
builder.maxScreenDistance = Constants.clusterScreenDistance
183+
builder.thresholdStrategy = ClusterStrategy()
183184
builder.tagMergeStrategy = BoothTagMergeStrategy()
184185

185186
let clusterer = builder.build()
@@ -296,6 +297,26 @@ extension NaverMapRepresentable {
296297
}
297298
}
298299

300+
final class ClusterStrategy: NMCDefaultDistanceStrategy, NMCThresholdStrategy {
301+
func getThreshold(_ zoom: Int) -> Double {
302+
guard zoom < 15 else { return Constants.clusterThresholdZoomIn }
303+
return Constants.clusterThresholdZoomOut
304+
}
305+
306+
override func getDistance(_ zoom: Int, node1: NMCNode, node2: NMCNode) -> Double {
307+
let defaultDistance = super.getDistance(zoom, node1: node1, node2: node2)
308+
309+
if Double(zoom) < Constants.brandClusteringThreshold { return defaultDistance }
310+
311+
guard let tag1 = node1.tag as? NSNumber,
312+
let tag2 = node2.tag as? NSNumber,
313+
tag1 == tag2
314+
else { return Double.infinity }
315+
316+
return defaultDistance
317+
}
318+
}
319+
299320
final class BoothClusteringKey: NSObject, NMCClusteringKey {
300321
let identifier: Int
301322
let brandID: Int
@@ -395,8 +416,8 @@ extension NaverMapRepresentable {
395416
marker.anchor = CGPoint(x: 0.5, y: 0.5)
396417
marker.zIndex = 50
397418

398-
marker.touchHandler = { [weak mapView, weak marker] _ in
399-
guard let mapView, let marker else { return false }
419+
marker.touchHandler = { [weak mapView] overlay in
420+
guard let mapView, let marker = overlay as? NMFMarker else { return false }
400421
let currentZoom = mapView.zoomLevel
401422
let targetZoom = min(currentZoom + 2.5, Constants.maxZoomLevel)
402423
let cameraUpdate = NMFCameraUpdate(scrollTo: marker.position, zoomTo: targetZoom)
@@ -420,11 +441,20 @@ extension NaverMapRepresentable {
420441

421442
final class BoothTagMergeStrategy: NSObject, NMCTagMergeStrategy {
422443
func mergeTag(_ cluster: NMCCluster) -> NSObject? {
423-
// TODO: [Feature] 브랜드별 클러스터링 도입 시 구현 필요
424-
// 1. cluster.children 노드를 순회하며 각 마커가 가진 태그(예: 브랜드 식별자)를 수집
425-
// 2. 모든 자식 노드의 태그가 동일하다면 해당 태그를 반환하면 부모 노드의 태그가 됨
426-
// 3. 여러 브랜드가 섞여있다면 줌 레벨에 따라 브랜드별 클러스터링 마커가 아닌 대표 클러스터링 마커일 것이므로 분기처리 필요
427-
return nil
444+
var mergedBrandID: Int?
445+
446+
for node in cluster.children {
447+
guard let tag = node.tag as? NSNumber else { return nil }
448+
let currentBrandID = tag.intValue
449+
450+
if mergedBrandID == nil {
451+
mergedBrandID = currentBrandID
452+
} else if mergedBrandID != currentBrandID {
453+
return nil
454+
}
455+
}
456+
457+
return mergedBrandID.map { NSNumber(value: $0) }
428458
}
429459
}
430460
}

Neki-iOS/Features/Pose/Sources/Presentation/Sources/Feature/RandomPoseCarouselFeature.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import os
1313
struct RandomPoseCarouselFeature {
1414
enum SlideDirection { case previous, next, none }
1515

16+
private enum CancelID { case poseRequest }
17+
1618
@ObservableState
1719
struct State {
1820
@Shared(.appStorage("RandomPoseTutorial")) var isTutorialPresented: Bool = true
@@ -80,15 +82,19 @@ struct RandomPoseCarouselFeature {
8082

8183
case .tapLeft:
8284
state.slideDirection = .previous
85+
state.isLoading = true
8386
return .run { send in
8487
await send(.poseResponse(Result { try await poseClient.startRandomPoseSuggestion(direction: .left) }))
8588
}
89+
.cancellable(id: CancelID.poseRequest, cancelInFlight: true)
8690

8791
case .tapRight:
8892
state.slideDirection = .next
93+
state.isLoading = true
8994
return .run { send in
9095
await send(.poseResponse(Result { try await poseClient.startRandomPoseSuggestion(direction: .right) }))
9196
}
97+
.cancellable(id: CancelID.poseRequest, cancelInFlight: true)
9298

9399
// MARK: - Scrap Logic (Optimistic)
94100
case .onTapScrap:

Neki-iOS/Features/Pose/Sources/Presentation/Sources/View/RandomPoseCarouselView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ struct RandomPoseCarouselView: View {
6161

6262
controlButtons
6363
.frame(maxHeight: .infinity, alignment: .bottom)
64+
.disabled(store.isLoading)
6465

6566
if store.isTutorialPresented { tutorialOverlay }
6667
}

Neki-iOS/Features/QRCodeScanner/Sources/Data/Sources/Strategies/Implementations/PhotosignatureStrategy.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ struct PhotoSignatureStrategy: QRCodeParsingStrategy {
1717
Logger.data.debug("포토시그니처 파싱 시도: \(url.absoluteString)")
1818

1919
let urlString = url.absoluteString
20-
let cleanString = urlString.hasSuffix("/") ? String(urlString.dropLast()) : urlString
21-
let imageURLString = cleanString + "/a.jpg"
20+
var imageURLString = String()
21+
22+
if urlString.hasSuffix("index.html") {
23+
imageURLString = urlString.replacingOccurrences(of: "index.html", with: "a.jpg")
24+
} else {
25+
let cleanString = urlString.hasSuffix("/") ? String(urlString.dropLast()) : urlString
26+
imageURLString = cleanString + "/a.jpg"
27+
}
2228

2329
guard let imageURL = URL(string: imageURLString) else {
2430
Logger.domain.error("이미지 URL 생성 실패.")
@@ -35,6 +41,8 @@ struct PhotoSignatureStrategy: QRCodeParsingStrategy {
3541

3642
return ParsedQRResult(brand: .photosignature, originalImage: data)
3743

44+
} catch let error as QRParseError {
45+
throw error
3846
} catch {
3947
Logger.network.warning("이미지 없음(404 등). 만료 확인.")
4048
throw .imageDownloadFailed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// CameraZoomFactor.swift
3+
// Neki-iOS
4+
//
5+
// Created by SwainYun on 3/1/26.
6+
//
7+
8+
import Foundation
9+
10+
public enum CameraZoomFactor {
11+
static let defaultMaxZoomFactor: CGFloat = 5.0 // 5배 줌
12+
static let defaultMinZoomFactor: CGFloat = 1.0 // 1배 줌 (원본)
13+
}

Neki-iOS/Features/QRCodeScanner/Sources/Presentation/Sources/Features/QRCodeScanFeature.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@ struct QRCodeScanFeature {
2525
var isWebViewPresented: Bool = false
2626

2727
var isCameraActive: Bool {
28-
isLoading == false && isWebViewPresented == false && isManualDownloadNeededAlertPresented == false && isUnsupportedBrandAlertPresented == false
28+
isLoading == false &&
29+
isWebViewPresented == false &&
30+
isManualDownloadNeededAlertPresented == false &&
31+
isUnsupportedBrandAlertPresented == false &&
32+
isExpiredAlertPresented == false
2933
}
3034
}
3135

3236
enum Action: BindableAction {
3337
// View Actions
3438
case closeButtonTapped
3539
case lightButtonTapped
40+
case openGalleryButtonTapped
3641
case openSuggestBrandPage
3742
case openWebViewButtonTapped
3843
case closeWebViewButtonTapped
@@ -82,6 +87,10 @@ struct QRCodeScanFeature {
8287
state.webViewURL = nil
8388
return .none
8489

90+
case .openGalleryButtonTapped:
91+
state.isUnsupportedBrandAlertPresented = false
92+
return .send(.addPhotoFromGalleryButtonTapped)
93+
8594
case .openSuggestBrandPage:
8695
Logger.presentation.debug("브랜드 제안 페이지 이동 요청")
8796
return .run { _ in
@@ -95,7 +104,7 @@ struct QRCodeScanFeature {
95104

96105
// MARK: - Scanning Flow
97106
case .codeScanned(let urlString):
98-
guard state.isLoading == false else { return .none }
107+
guard state.isLoading == false, state.isCameraActive else { return .none }
99108
state.isLoading = true
100109
Logger.presentation.debug("QR 스캔 감지: \(urlString)")
101110

@@ -109,6 +118,7 @@ struct QRCodeScanFeature {
109118

110119
case let .parseQRResult(.failure(error)):
111120
state.isLoading = false
121+
Logger.domain.info("QR 파싱 실패: \(error)")
112122
return handleError(error, state: &state)
113123

114124
// MARK: - WebView Download Flow

0 commit comments

Comments
 (0)