From cc27c5fca51d5fe180941a620dd3610f68cf9e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 15:00:19 +0900 Subject: [PATCH 001/101] =?UTF-8?q?delete:=20koinUITest=20=ED=83=80?= =?UTF-8?q?=EA=B2=9F=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 125 +----------------- .../xcshareddata/xcschemes/koin.xcscheme | 10 -- koinUITests/koinUITests.swift | 41 ------ koinUITests/koinUITestsLaunchTests.swift | 32 ----- 4 files changed, 5 insertions(+), 203 deletions(-) delete mode 100644 koinUITests/koinUITests.swift delete mode 100644 koinUITests/koinUITestsLaunchTests.swift diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 29bf3268..a6cdb9d8 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -689,7 +689,7 @@ 835DC2062C84D09400488506 /* FetchRecommendedSearchWordRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC2052C84D09400488506 /* FetchRecommendedSearchWordRequest.swift */; }; 836242362BD9522400845499 /* FirebaseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836242352BD9522400845499 /* FirebaseLog.swift */; }; 8380FDC32BC1CBB800120036 /* BusCourseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8380FDC22BC1CBB800120036 /* BusCourseDto.swift */; }; - 838F114E2CA51C6C00BF3B25 /* (null) in Sources */ = {isa = PBXBuildFile; }; + 838F114E2CA51C6C00BF3B25 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; 838F11502CA5330900BF3B25 /* AddNoticeKeywordType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F114F2CA5330900BF3B25 /* AddNoticeKeywordType.swift */; }; 839D71192BEB1570001BC7F7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839D71182BEB1570001BC7F7 /* NotificationService.swift */; }; 839D711D2BEB1570001BC7F7 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 839D71162BEB1570001BC7F7 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -697,7 +697,7 @@ 83B7457D2C9B2EBB005868BD /* IndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B7457C2C9B2EBB005868BD /* IndicatorView.swift */; }; 83BB83132CEC814800946623 /* FetchKeywordNoticePhraseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BB83122CEC814800946623 /* FetchKeywordNoticePhraseUseCase.swift */; }; 83C509622BFA0CBB007379A0 /* ScrollLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C509612BFA0CBB007379A0 /* ScrollLog.swift */; }; - 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */ = {isa = PBXBuildFile; }; + 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 83DD0D742C4F6FD000278ABD /* BusType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D732C4F6FD000278ABD /* BusType.swift */; }; 83DD0D762C4F6FDE00278ABD /* BusPlace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D752C4F6FDE00278ABD /* BusPlace.swift */; }; 83DD0D782C4F6FF000278ABD /* BusDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D772C4F6FF000278ABD /* BusDirection.swift */; }; @@ -976,7 +976,6 @@ D2FFA98B2D63654A00EF8E56 /* FetchLostItemsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFA98A2D63654A00EF8E56 /* FetchLostItemsRequest.swift */; }; D80AD4E42E6C6BC30061334B /* waveLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D80AD4E32E6C6BC30061334B /* waveLogo.json */; }; D80AD4E52E6C6BC30061334B /* waveLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D80AD4E32E6C6BC30061334B /* waveLogo.json */; }; - D80AD4E62E6C6BC30061334B /* waveLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D80AD4E32E6C6BC30061334B /* waveLogo.json */; }; D837FB122E254BD8002BFB9F /* CustomSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D837FB112E254BD8002BFB9F /* CustomSessionManager.swift */; }; D85939752DC12C9D00CE4CB2 /* CheckDuplicatedIdUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85939742DC12C9D00CE4CB2 /* CheckDuplicatedIdUsecase.swift */; }; D85939772DC12D0900CE4CB2 /* CheckDuplicatedIdRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85939762DC12D0900CE4CB2 /* CheckDuplicatedIdRequest.swift */; }; @@ -997,7 +996,6 @@ D8B4E0042DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B4E0032DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift */; }; D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */; }; D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; - D8F5C5542E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; D8F5C5552E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; @@ -1019,13 +1017,6 @@ remoteGlobalIDString = A001E2B32845091D00D6C310; remoteInfo = koin; }; - A001E2CF2845091F00D6C310 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A001E2AC2845091D00D6C310 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A001E2B32845091D00D6C310; - remoteInfo = koin; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -1750,9 +1741,6 @@ 83EBB87D2D0FE1BE00346018 /* FetchKoinPickerDateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKoinPickerDateUseCase.swift; sourceTree = ""; }; A001E2B42845091D00D6C310 /* koin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = koin.app; sourceTree = BUILT_PRODUCTS_DIR; }; A001E2C42845091F00D6C310 /* koinTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A001E2CE2845091F00D6C310 /* koinUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A001E2D22845091F00D6C310 /* koinUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = koinUITests.swift; sourceTree = ""; }; - A001E2D42845091F00D6C310 /* koinUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = koinUITestsLaunchTests.swift; sourceTree = ""; }; B438AEE32E756AB600E889C4 /* OrderImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderImage.swift; sourceTree = ""; }; B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSummaryDto.swift; sourceTree = ""; }; B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopSummaryFromShopUseCase.swift; sourceTree = ""; }; @@ -2065,13 +2053,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2CB2845091F00D6C310 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -4545,7 +4526,6 @@ D27DCD682BA5BADC0081FD36 /* Info.plist */, D2D380E02BA6AC50009AD5A9 /* koin.entitlements */, D27DD0202BA608310081FD36 /* Koin */, - A001E2D12845091F00D6C310 /* koinUITests */, 839D71172BEB1570001BC7F7 /* NotificationService */, 7C7F56B52FFB9F4A00847151 /* Common */, A001E2B52845091D00D6C310 /* Products */, @@ -4557,21 +4537,11 @@ children = ( A001E2B42845091D00D6C310 /* koin.app */, A001E2C42845091F00D6C310 /* koinTests.xctest */, - A001E2CE2845091F00D6C310 /* koinUITests.xctest */, 839D71162BEB1570001BC7F7 /* NotificationService.appex */, ); name = Products; sourceTree = ""; }; - A001E2D12845091F00D6C310 /* koinUITests */ = { - isa = PBXGroup; - children = ( - A001E2D22845091F00D6C310 /* koinUITests.swift */, - A001E2D42845091F00D6C310 /* koinUITestsLaunchTests.swift */, - ); - path = koinUITests; - sourceTree = ""; - }; B46B8D242E770A3100A8E797 /* Fonts */ = { isa = PBXGroup; children = ( @@ -5538,24 +5508,6 @@ productReference = A001E2C42845091F00D6C310 /* koinTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - A001E2CD2845091F00D6C310 /* koinUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A001E2DE2845091F00D6C310 /* Build configuration list for PBXNativeTarget "koinUITests" */; - buildPhases = ( - A001E2CA2845091F00D6C310 /* Sources */, - A001E2CB2845091F00D6C310 /* Frameworks */, - A001E2CC2845091F00D6C310 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A001E2D02845091F00D6C310 /* PBXTargetDependency */, - ); - name = koinUITests; - productName = koinUITests; - productReference = A001E2CE2845091F00D6C310 /* koinUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -5612,7 +5564,6 @@ targets = ( A001E2B32845091D00D6C310 /* koin */, A001E2C32845091F00D6C310 /* koinTests */, - A001E2CD2845091F00D6C310 /* koinUITests */, 839D71152BEB1570001BC7F7 /* NotificationService */, ); }; @@ -5637,8 +5588,8 @@ 7CB061D22F32312E00E83CA9 /* DropDownCell.xib in Resources */, B46B8D272E770A3100A8E797 /* Pretendard-Medium.otf in Resources */, D27DD0B22BA608310081FD36 /* Assets.xcassets in Resources */, - 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */, - 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */, + 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */, + 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */, B6A0D62A2EEB01E600A19521 /* Debug.xcconfig in Resources */, D261BBAB2D7B456A00A67F06 /* PrivacyInfo.xcprivacy in Resources */, D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */, @@ -5657,15 +5608,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2CC2845091F00D6C310 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D80AD4E62E6C6BC30061334B /* waveLogo.json in Resources */, - D8F5C5542E6F144300FB6708 /* floatingLogo.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -5746,7 +5688,7 @@ 7CEB3B4B2FBFF4B700AF2B81 /* DeleteDeviceTokenUseCase.swift in Sources */, 7CC42B132F56AB9D00940CE1 /* CallVanListCollectionView.swift in Sources */, D20AB9542C553503006BC684 /* ChangeNotiUseCase.swift in Sources */, - 838F114E2CA51C6C00BF3B25 /* (null) in Sources */, + 838F114E2CA51C6C00BF3B25 /* BuildFile in Sources */, D27DD0CE2BA608310081FD36 /* ViewModelProtocol.swift in Sources */, 7CB2D8B62FDDA7000A9C1234 /* HostingControllerProtocol.swift in Sources */, D2FB23622BFDAC240098BA2B /* ShopAPI.swift in Sources */, @@ -6671,13 +6613,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2CA2845091F00D6C310 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -6691,11 +6626,6 @@ target = A001E2B32845091D00D6C310 /* koin */; targetProxy = A001E2C52845091F00D6C310 /* PBXContainerItemProxy */; }; - A001E2D02845091F00D6C310 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A001E2B32845091D00D6C310 /* koin */; - targetProxy = A001E2CF2845091F00D6C310 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -6994,42 +6924,6 @@ }; name = Release; }; - A001E2DF2845091F00D6C310 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K626UYGR25; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "코인(stage)"; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = koin; - }; - name = Debug; - }; - A001E2E02845091F00D6C310 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K626UYGR25; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "코인(stage)"; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = koin; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -7069,15 +6963,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A001E2DE2845091F00D6C310 /* Build configuration list for PBXNativeTarget "koinUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A001E2DF2845091F00D6C310 /* Debug */, - A001E2E02845091F00D6C310 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme b/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme index 00543f19..0950ecc4 100644 --- a/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme +++ b/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme @@ -40,16 +40,6 @@ ReferencedContainer = "container:koin.xcodeproj"> - - - - Date: Sat, 8 Aug 2026 15:02:34 +0900 Subject: [PATCH 002/101] =?UTF-8?q?delete:=20=EA=B8=B0=EC=A1=B4=EC=97=90?= =?UTF-8?q?=20=EB=A7=8C=EB=93=A4=EC=96=B4=EC=A7=84=20koinUnitTest=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 107 ------------------ .../xcshareddata/xcschemes/koin.xcscheme | 10 -- 2 files changed, 117 deletions(-) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a6cdb9d8..ca17970d 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -975,7 +975,6 @@ D2FFA9872D63343600EF8E56 /* ReportLostItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFA9862D63343600EF8E56 /* ReportLostItemUseCase.swift */; }; D2FFA98B2D63654A00EF8E56 /* FetchLostItemsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FFA98A2D63654A00EF8E56 /* FetchLostItemsRequest.swift */; }; D80AD4E42E6C6BC30061334B /* waveLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D80AD4E32E6C6BC30061334B /* waveLogo.json */; }; - D80AD4E52E6C6BC30061334B /* waveLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D80AD4E32E6C6BC30061334B /* waveLogo.json */; }; D837FB122E254BD8002BFB9F /* CustomSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D837FB112E254BD8002BFB9F /* CustomSessionManager.swift */; }; D85939752DC12C9D00CE4CB2 /* CheckDuplicatedIdUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85939742DC12C9D00CE4CB2 /* CheckDuplicatedIdUsecase.swift */; }; D85939772DC12D0900CE4CB2 /* CheckDuplicatedIdRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85939762DC12D0900CE4CB2 /* CheckDuplicatedIdRequest.swift */; }; @@ -996,7 +995,6 @@ D8B4E0042DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B4E0032DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift */; }; D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */; }; D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; - D8F5C5552E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; F1A000010000000000000003 /* ZoomingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */; }; @@ -1010,13 +1008,6 @@ remoteGlobalIDString = 839D71152BEB1570001BC7F7; remoteInfo = NotificationService; }; - A001E2C52845091F00D6C310 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A001E2AC2845091D00D6C310 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A001E2B32845091D00D6C310; - remoteInfo = koin; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -1740,7 +1731,6 @@ 83EBB8792D0FDFD700346018 /* KoinPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinPickerView.swift; sourceTree = ""; }; 83EBB87D2D0FE1BE00346018 /* FetchKoinPickerDateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKoinPickerDateUseCase.swift; sourceTree = ""; }; A001E2B42845091D00D6C310 /* koin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = koin.app; sourceTree = BUILT_PRODUCTS_DIR; }; - A001E2C42845091F00D6C310 /* koinTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B438AEE32E756AB600E889C4 /* OrderImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderImage.swift; sourceTree = ""; }; B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSummaryDto.swift; sourceTree = ""; }; B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopSummaryFromShopUseCase.swift; sourceTree = ""; }; @@ -2046,13 +2036,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2C12845091F00D6C310 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -4536,7 +4519,6 @@ isa = PBXGroup; children = ( A001E2B42845091D00D6C310 /* koin.app */, - A001E2C42845091F00D6C310 /* koinTests.xctest */, 839D71162BEB1570001BC7F7 /* NotificationService.appex */, ); name = Products; @@ -5490,24 +5472,6 @@ productReference = A001E2B42845091D00D6C310 /* koin.app */; productType = "com.apple.product-type.application"; }; - A001E2C32845091F00D6C310 /* koinTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A001E2DB2845091F00D6C310 /* Build configuration list for PBXNativeTarget "koinTests" */; - buildPhases = ( - A001E2C02845091F00D6C310 /* Sources */, - A001E2C12845091F00D6C310 /* Frameworks */, - A001E2C22845091F00D6C310 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A001E2C62845091F00D6C310 /* PBXTargetDependency */, - ); - name = koinTests; - productName = koinTests; - productReference = A001E2C42845091F00D6C310 /* koinTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -5563,7 +5527,6 @@ projectRoot = ""; targets = ( A001E2B32845091D00D6C310 /* koin */, - A001E2C32845091F00D6C310 /* koinTests */, 839D71152BEB1570001BC7F7 /* NotificationService */, ); }; @@ -5599,15 +5562,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2C22845091F00D6C310 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D80AD4E52E6C6BC30061334B /* waveLogo.json in Resources */, - D8F5C5552E6F144300FB6708 /* floatingLogo.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -6606,13 +6560,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A001E2C02845091F00D6C310 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -6621,11 +6568,6 @@ target = 839D71152BEB1570001BC7F7 /* NotificationService */; targetProxy = 839D711B2BEB1570001BC7F7 /* PBXContainerItemProxy */; }; - A001E2C62845091F00D6C310 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A001E2B32845091D00D6C310 /* koin */; - targetProxy = A001E2C52845091F00D6C310 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -6884,46 +6826,6 @@ }; name = Release; }; - A001E2DC2845091F00D6C310 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K626UYGR25; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "코인(stage)"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/koin.app/koin"; - }; - name = Debug; - }; - A001E2DD2845091F00D6C310 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = K626UYGR25; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "코인(stage)"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/koin.app/koin"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -6954,15 +6856,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A001E2DB2845091F00D6C310 /* Build configuration list for PBXNativeTarget "koinTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A001E2DC2845091F00D6C310 /* Debug */, - A001E2DD2845091F00D6C310 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme b/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme index 0950ecc4..121e9cc4 100644 --- a/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme +++ b/koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme @@ -30,16 +30,6 @@ shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> - - - - Date: Sat, 8 Aug 2026 15:35:25 +0900 Subject: [PATCH 003/101] =?UTF-8?q?add:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Un?= =?UTF-8?q?it=20Testing=20=ED=83=80=EA=B2=9F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 150 ++++++++++++++++-- .../xcschemes/NotificationService.xcscheme | 13 ++ .../xcschemes/koin-stage.xcscheme | 13 ++ .../xcshareddata/xcschemes/koin.xcscheme | 11 ++ koinUnitTests/koinUnitTests.swift | 18 +++ 5 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 koinUnitTests/koinUnitTests.swift diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index ca17970d..a8e88593 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -689,7 +689,7 @@ 835DC2062C84D09400488506 /* FetchRecommendedSearchWordRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC2052C84D09400488506 /* FetchRecommendedSearchWordRequest.swift */; }; 836242362BD9522400845499 /* FirebaseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836242352BD9522400845499 /* FirebaseLog.swift */; }; 8380FDC32BC1CBB800120036 /* BusCourseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8380FDC22BC1CBB800120036 /* BusCourseDto.swift */; }; - 838F114E2CA51C6C00BF3B25 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 838F114E2CA51C6C00BF3B25 /* (null) in Sources */ = {isa = PBXBuildFile; }; 838F11502CA5330900BF3B25 /* AddNoticeKeywordType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F114F2CA5330900BF3B25 /* AddNoticeKeywordType.swift */; }; 839D71192BEB1570001BC7F7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839D71182BEB1570001BC7F7 /* NotificationService.swift */; }; 839D711D2BEB1570001BC7F7 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 839D71162BEB1570001BC7F7 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -697,7 +697,7 @@ 83B7457D2C9B2EBB005868BD /* IndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B7457C2C9B2EBB005868BD /* IndicatorView.swift */; }; 83BB83132CEC814800946623 /* FetchKeywordNoticePhraseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BB83122CEC814800946623 /* FetchKeywordNoticePhraseUseCase.swift */; }; 83C509622BFA0CBB007379A0 /* ScrollLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C509612BFA0CBB007379A0 /* ScrollLog.swift */; }; - 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */ = {isa = PBXBuildFile; }; 83DD0D742C4F6FD000278ABD /* BusType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D732C4F6FD000278ABD /* BusType.swift */; }; 83DD0D762C4F6FDE00278ABD /* BusPlace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D752C4F6FDE00278ABD /* BusPlace.swift */; }; 83DD0D782C4F6FF000278ABD /* BusDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DD0D772C4F6FF000278ABD /* BusDirection.swift */; }; @@ -996,6 +996,7 @@ D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */; }; D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; + EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC69228E302700E800EE26ED /* koinUnitTests.swift */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; F1A000010000000000000003 /* ZoomingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */; }; /* End PBXBuildFile section */ @@ -1008,6 +1009,13 @@ remoteGlobalIDString = 839D71152BEB1570001BC7F7; remoteInfo = NotificationService; }; + EC692289302700A400EE26ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A001E2AC2845091D00D6C310 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A001E2B32845091D00D6C310; + remoteInfo = koin; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -2001,6 +2009,8 @@ D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderFloatingButton.swift; sourceTree = ""; }; D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationManageable.swift; sourceTree = ""; }; D8F5C5532E6F144300FB6708 /* floatingLogo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = floatingLogo.json; sourceTree = ""; }; + EC692285302700A400EE26ED /* koinUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EC69228E302700E800EE26ED /* koinUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = koinUnitTests.swift; sourceTree = ""; }; F1A000010000000000000002 /* ZoomingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionView.swift; sourceTree = ""; }; F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionViewCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2036,6 +2046,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EC692282302700A400EE26ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -4511,6 +4528,7 @@ D27DD0202BA608310081FD36 /* Koin */, 839D71172BEB1570001BC7F7 /* NotificationService */, 7C7F56B52FFB9F4A00847151 /* Common */, + EC69228F302700E800EE26ED /* koinUnitTests */, A001E2B52845091D00D6C310 /* Products */, ); sourceTree = ""; @@ -4520,6 +4538,7 @@ children = ( A001E2B42845091D00D6C310 /* koin.app */, 839D71162BEB1570001BC7F7 /* NotificationService.appex */, + EC692285302700A400EE26ED /* koinUnitTests.xctest */, ); name = Products; sourceTree = ""; @@ -5432,6 +5451,14 @@ path = Dining; sourceTree = ""; }; + EC69228F302700E800EE26ED /* koinUnitTests */ = { + isa = PBXGroup; + children = ( + EC69228E302700E800EE26ED /* koinUnitTests.swift */, + ); + path = koinUnitTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -5472,6 +5499,26 @@ productReference = A001E2B42845091D00D6C310 /* koin.app */; productType = "com.apple.product-type.application"; }; + EC692284302700A400EE26ED /* koinUnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EC69228D302700A400EE26ED /* Build configuration list for PBXNativeTarget "koinUnitTests" */; + buildPhases = ( + EC692281302700A400EE26ED /* Sources */, + EC692282302700A400EE26ED /* Frameworks */, + EC692283302700A400EE26ED /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EC69228A302700A400EE26ED /* PBXTargetDependency */, + ); + name = koinUnitTests; + packageProductDependencies = ( + ); + productName = koinUnitTests; + productReference = EC692285302700A400EE26ED /* koinUnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -5479,7 +5526,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1530; + LastSwiftUpdateCheck = 2640; LastUpgradeCheck = 1530; TargetAttributes = { 839D71152BEB1570001BC7F7 = { @@ -5488,12 +5535,8 @@ A001E2B32845091D00D6C310 = { CreatedOnToolsVersion = 13.3.1; }; - A001E2C32845091F00D6C310 = { - CreatedOnToolsVersion = 13.3.1; - TestTargetID = A001E2B32845091D00D6C310; - }; - A001E2CD2845091F00D6C310 = { - CreatedOnToolsVersion = 13.3.1; + EC692284302700A400EE26ED = { + CreatedOnToolsVersion = 26.4.1; TestTargetID = A001E2B32845091D00D6C310; }; }; @@ -5528,6 +5571,7 @@ targets = ( A001E2B32845091D00D6C310 /* koin */, 839D71152BEB1570001BC7F7 /* NotificationService */, + EC692284302700A400EE26ED /* koinUnitTests */, ); }; /* End PBXProject section */ @@ -5551,8 +5595,8 @@ 7CB061D22F32312E00E83CA9 /* DropDownCell.xib in Resources */, B46B8D272E770A3100A8E797 /* Pretendard-Medium.otf in Resources */, D27DD0B22BA608310081FD36 /* Assets.xcassets in Resources */, - 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */, - 83D5E9C82CCB844D00C6D4CD /* BuildFile in Resources */, + 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */, + 83D5E9C82CCB844D00C6D4CD /* (null) in Resources */, B6A0D62A2EEB01E600A19521 /* Debug.xcconfig in Resources */, D261BBAB2D7B456A00A67F06 /* PrivacyInfo.xcprivacy in Resources */, D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */, @@ -5562,6 +5606,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EC692283302700A400EE26ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -5642,7 +5693,7 @@ 7CEB3B4B2FBFF4B700AF2B81 /* DeleteDeviceTokenUseCase.swift in Sources */, 7CC42B132F56AB9D00940CE1 /* CallVanListCollectionView.swift in Sources */, D20AB9542C553503006BC684 /* ChangeNotiUseCase.swift in Sources */, - 838F114E2CA51C6C00BF3B25 /* BuildFile in Sources */, + 838F114E2CA51C6C00BF3B25 /* (null) in Sources */, D27DD0CE2BA608310081FD36 /* ViewModelProtocol.swift in Sources */, 7CB2D8B62FDDA7000A9C1234 /* HostingControllerProtocol.swift in Sources */, D2FB23622BFDAC240098BA2B /* ShopAPI.swift in Sources */, @@ -6560,6 +6611,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EC692281302700A400EE26ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -6568,6 +6627,11 @@ target = 839D71152BEB1570001BC7F7 /* NotificationService */; targetProxy = 839D711B2BEB1570001BC7F7 /* PBXContainerItemProxy */; }; + EC69228A302700A400EE26ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A001E2B32845091D00D6C310 /* koin */; + targetProxy = EC692289302700A400EE26ED /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -6826,6 +6890,59 @@ }; name = Release; }; + EC69228B302700A400EE26ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = K626UYGR25; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/koin.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/koin"; + }; + name = Debug; + }; + EC69228C302700A400EE26ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = K626UYGR25; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koinUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/koin.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/koin"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -6856,6 +6973,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + EC69228D302700A400EE26ED /* Build configuration list for PBXNativeTarget "koinUnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EC69228B302700A400EE26ED /* Debug */, + EC69228C302700A400EE26ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/koin.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme b/koin.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme index 25a32353..02329507 100644 --- a/koin.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme +++ b/koin.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme @@ -44,6 +44,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + + + + + + + + + + Date: Sat, 8 Aug 2026 17:24:19 +0900 Subject: [PATCH 004/101] =?UTF-8?q?add:=20Combine=20Publisher=EB=A5=BC=20a?= =?UTF-8?q?sync=EB=A1=9C=20=EB=B8=8C=EB=A6=AC=EC=A7=95=ED=95=98=EB=8A=94?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=97=AC=ED=8D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Support/PublisherTestSupport.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 koinUnitTests/Support/PublisherTestSupport.swift diff --git a/koinUnitTests/Support/PublisherTestSupport.swift b/koinUnitTests/Support/PublisherTestSupport.swift new file mode 100644 index 00000000..8dceeb16 --- /dev/null +++ b/koinUnitTests/Support/PublisherTestSupport.swift @@ -0,0 +1,26 @@ +// +// PublisherTestError.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Combine +import Foundation + +enum PublisherTestError: Error { + case finishedWithoutValue +} + +extension Publisher { + /// Combine 파이프라인이 방출하는 첫 번째 값을 async로 받아온다. + /// + /// 프로덕션 코드가 `AnyPublisher`를 반환하기 때문에, Swift Testing의 `async` 테스트에서 + /// `await`로 결과를 기다리기 위해 `Publisher.values`(AsyncSequence)를 경유한다. + func firstValue() async throws -> Output { + for try await value in values { + return value + } + throw PublisherTestError.finishedWithoutValue + } +} From 750b398139e092124b6197d81aa926ad3b83c87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:42 +0900 Subject: [PATCH 005/101] =?UTF-8?q?add:=20=EC=8B=9D=EB=8B=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20Fixture=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Support/DiningFixtures.swift | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 koinUnitTests/Support/DiningFixtures.swift diff --git a/koinUnitTests/Support/DiningFixtures.swift b/koinUnitTests/Support/DiningFixtures.swift new file mode 100644 index 00000000..0ceb71d9 --- /dev/null +++ b/koinUnitTests/Support/DiningFixtures.swift @@ -0,0 +1,83 @@ +// +// DiningFixture.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Foundation +@testable import koin + +enum DiningFixture { + + static func dto( + id: Int = 1, + date: String = "2026-08-03", + type: DiningType = .lunch, + place: DiningPlace = .cornerA, + menu: [String]? = ["김치찌개", "밥"], + imageURL: String? = nil + ) -> DiningDto { + DiningDto( + id: id, + date: date, + type: type, + place: place, + priceCard: nil, + priceCash: nil, + kcal: nil, + menu: menu, + createdAt: "", + updatedAt: "", + soldoutAt: nil, + changedAt: nil, + imageURL: imageURL, + likes: 0, + isLiked: false + ) + } + + static func item( + id: Int = 1, + date: String = "2026-08-03", + type: DiningType = .lunch, + place: DiningPlace = .cornerA, + menu: [String] = ["김치찌개", "밥"], + imageUrl: String? = nil + ) -> DiningItem { + DiningItem( + id: id, + type: type, + place: place, + priceCard: nil, + priceCash: nil, + kcal: 0, + menu: menu, + soldoutAt: nil, + changedAt: nil, + imageUrl: imageUrl, + likes: 0, + isLiked: false, + date: date + ) + } + + /// 프로덕션 코드(`DefaultDateProvider`, `Date.formatDateToYYMMDD()`)가 `Calendar.current`와 + /// 기본 `DateFormatter`를 쓰기 때문에, 테스트도 같은 기준으로 Date를 만든다. + static func date( + year: Int = 2026, + month: Int = 8, + day: Int = 3, + hour: Int = 0, + minute: Int = 0, + calendar: Calendar = .current + ) -> Date? { + var components = DateComponents() + components.year = year + components.month = month + components.day = day + components.hour = hour + components.minute = minute + return calendar.date(from: components) + } +} From 6a2e0edd28cc678b02f8c1cfb5a86a6efa8e7d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:47 +0900 Subject: [PATCH 006/101] =?UTF-8?q?add:=20DiningRepository=20Spy=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 --- .../Doubles/SpyDiningRepository.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 koinUnitTests/Doubles/SpyDiningRepository.swift diff --git a/koinUnitTests/Doubles/SpyDiningRepository.swift b/koinUnitTests/Doubles/SpyDiningRepository.swift new file mode 100644 index 00000000..fff72f6d --- /dev/null +++ b/koinUnitTests/Doubles/SpyDiningRepository.swift @@ -0,0 +1,37 @@ +// +// SpyDiningRepository.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Combine +import Foundation +@testable import koin + +/// 호출 여부와 전달된 인자를 기록만 하고, 검증은 테스트 코드에 맡기는 테스트 더블. +final class SpyDiningRepository: DiningRepository { + + /// `fetchDiningList`가 돌려줄 응답. 테스트에서 시나리오별로 갈아끼운다. + var stubbedDiningList: [DiningDto] = [] + + private(set) var fetchDiningListCallCount = 0 + private(set) var receivedFetchRequests: [FetchDiningListRequest] = [] + private(set) var receivedShareModels: [ShareDiningMenu] = [] + + func fetchDiningList(requestModel: FetchDiningListRequest) -> AnyPublisher<[DiningDto], ErrorResponse> { + fetchDiningListCallCount += 1 + receivedFetchRequests.append(requestModel) + return Just(stubbedDiningList) + .setFailureType(to: ErrorResponse.self) + .eraseToAnyPublisher() + } + + func fetchCoopShopList() -> AnyPublisher { + Empty().eraseToAnyPublisher() + } + + func shareMenuList(shareModel: ShareDiningMenu) { + receivedShareModels.append(shareModel) + } +} From bb875237b7aafe911242b58737ab3593d1a1b74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:49 +0900 Subject: [PATCH 007/101] =?UTF-8?q?add:=20DateProvider=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EB=8C=80=20=ED=8C=90=EB=8B=A8=20=EA=B2=BD=EA=B3=84?= =?UTF-8?q?=EA=B0=92=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Dining/DateProviderTests.swift | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 koinUnitTests/Dining/DateProviderTests.swift diff --git a/koinUnitTests/Dining/DateProviderTests.swift b/koinUnitTests/Dining/DateProviderTests.swift new file mode 100644 index 00000000..d354e0f2 --- /dev/null +++ b/koinUnitTests/Dining/DateProviderTests.swift @@ -0,0 +1,81 @@ +// +// DateProviderTests.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Foundation +import Testing +@testable import koin + +@Suite("DateProvider - 시간대 판단 로직 경계값") +struct DateProviderTests { + + private let sut = DefaultDateProvider() + private let calendar = Calendar.current + + private func date(hour: Int, minute: Int) throws -> Date { + try #require(DiningFixture.date(hour: hour, minute: minute, calendar: calendar)) + } + + @Test("09:00 이전이면 breakfast를 반환한다") + func 아홉시_이전이면_breakfast를_반환한다() throws { + let input = try date(hour: 8, minute: 59) + + let result = sut.execute(date: input) + + #expect(result.diningType == .breakfast) + #expect(calendar.isDate(result.date, inSameDayAs: input)) + } + + @Test("09:00이면 lunch를 반환한다") + func 아홉시면_lunch를_반환한다() throws { + let input = try date(hour: 9, minute: 0) + + let result = sut.execute(date: input) + + #expect(result.diningType == .lunch) + #expect(calendar.isDate(result.date, inSameDayAs: input)) + } + + @Test("13:30이면 lunch를 반환한다") + func 열세시_삼십분이면_lunch를_반환한다() throws { + let input = try date(hour: 13, minute: 30) + + let result = sut.execute(date: input) + + #expect(result.diningType == .lunch) + } + + @Test("13:31이면 dinner를 반환한다") + func 열세시_삼십일분이면_dinner를_반환한다() throws { + let input = try date(hour: 13, minute: 31) + + let result = sut.execute(date: input) + + #expect(result.diningType == .dinner) + #expect(calendar.isDate(result.date, inSameDayAs: input)) + } + + @Test("18:30이면 dinner를 반환한다") + func 열여덟시_삼십분이면_dinner를_반환한다() throws { + let input = try date(hour: 18, minute: 30) + + let result = sut.execute(date: input) + + #expect(result.diningType == .dinner) + } + + /// 18:31과 23:59는 "다음 날 breakfast"라는 동일한 결과를 기대하므로 매개변수화한다. + @Test("18:30 이후면 다음 날 breakfast를 반환한다", arguments: [(18, 31), (23, 59)]) + func 열여덟시_삼십분_이후면_다음_날_breakfast를_반환한다(hour: Int, minute: Int) throws { + let input = try date(hour: hour, minute: minute) + let expectedNextDay = try #require(calendar.date(byAdding: .day, value: 1, to: input)) + + let result = sut.execute(date: input) + + #expect(result.diningType == .breakfast) + #expect(calendar.isDate(result.date, inSameDayAs: expectedNextDay)) + } +} From 7f45dcdd7576f62d161df167efb39e5c86820049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:52 +0900 Subject: [PATCH 008/101] =?UTF-8?q?add:=20FetchDiningListUseCase=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81/=EC=A0=95=EB=A0=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dining/FetchDiningListUseCaseTests.swift | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 koinUnitTests/Dining/FetchDiningListUseCaseTests.swift diff --git a/koinUnitTests/Dining/FetchDiningListUseCaseTests.swift b/koinUnitTests/Dining/FetchDiningListUseCaseTests.swift new file mode 100644 index 00000000..68dc7544 --- /dev/null +++ b/koinUnitTests/Dining/FetchDiningListUseCaseTests.swift @@ -0,0 +1,88 @@ +// +// FetchDiningListUseCaseTests.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Foundation +import Testing +@testable import koin + +@Suite("FetchDiningListUseCase - segmentControl에 따른 데이터 필터링") +struct FetchDiningListUseCaseTests { + + private func makeSUT( + stubbedDiningList: [DiningDto] + ) -> (sut: DefaultFetchDiningListUseCase, spy: SpyDiningRepository) { + let spy = SpyDiningRepository() + spy.stubbedDiningList = stubbedDiningList + return (DefaultFetchDiningListUseCase(diningRepository: spy), spy) + } + + private func diningInfo( + type: DiningType, + date: Date = Date() + ) -> CurrentDiningTime { + CurrentDiningTime(date: date, diningType: type) + } + + @Test("요청한 시간대의 식단만 반환한다") + func 요청한_시간대의_식단만_반환한다() async throws { + let (sut, _) = makeSUT(stubbedDiningList: [ + DiningFixture.dto(id: 1, type: .breakfast, place: .cornerA), + DiningFixture.dto(id: 2, type: .lunch, place: .cornerB), + DiningFixture.dto(id: 3, type: .dinner, place: .cornerC), + DiningFixture.dto(id: 4, type: .lunch, place: .special) + ]) + + let result = try await sut.execute(diningInfo: diningInfo(type: .lunch)).firstValue() + + #expect(result.count == 2) + #expect(result.allSatisfy { $0.type == .lunch }) + #expect(result.map(\.id).sorted() == [2, 4]) + } + + @Test( + "미운영 메뉴는 제외한다", + arguments: [DiningType.breakfast, .lunch, .dinner] + ) + func 미운영_메뉴는_제외한다(requestedType: DiningType) async throws { + let (sut, _) = makeSUT(stubbedDiningList: [ + DiningFixture.dto(id: 1, type: requestedType, place: .cornerA, menu: ["미운영"]), + DiningFixture.dto(id: 2, type: requestedType, place: .cornerB, menu: ["김치찌개", "밥"]) + ]) + + let result = try await sut.execute(diningInfo: diningInfo(type: requestedType)).firstValue() + + #expect(result.count == 1) + #expect(result.first?.id == 2) + #expect(!result.contains { $0.menu.first == "미운영" }) + } + + @Test("장소 우선순위대로 정렬한다") + func 장소_우선순위대로_정렬한다() async throws { + let (sut, _) = makeSUT(stubbedDiningList: [ + DiningFixture.dto(id: 1, type: .lunch, place: .secondCampus), + DiningFixture.dto(id: 2, type: .lunch, place: .special), + DiningFixture.dto(id: 3, type: .lunch, place: .cornerC), + DiningFixture.dto(id: 4, type: .lunch, place: .cornerA), + DiningFixture.dto(id: 5, type: .lunch, place: .cornerB) + ]) + + let result = try await sut.execute(diningInfo: diningInfo(type: .lunch)).firstValue() + + #expect(result.map(\.place) == [.cornerA, .cornerB, .cornerC, .special, .secondCampus]) + } + + @Test("요청 날짜를 yyMMdd 형식으로 전달한다") + func 요청_날짜를_yyMMdd_형식으로_전달한다() async throws { + let (sut, spy) = makeSUT(stubbedDiningList: []) + let requestedDate = try #require(DiningFixture.date(year: 2026, month: 8, day: 3)) + + _ = try await sut.execute(diningInfo: diningInfo(type: .lunch, date: requestedDate)).firstValue() + + #expect(spy.fetchDiningListCallCount == 1) + #expect(spy.receivedFetchRequests.first?.date == "260803") + } +} From 4d126a021678db2c1c90c84eb1f0407fdc1a0387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:55 +0900 Subject: [PATCH 009/101] =?UTF-8?q?add:=20ShareMenuListUseCase=20=EC=8B=9D?= =?UTF-8?q?=EB=8B=A8=20=EA=B3=B5=EC=9C=A0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../Dining/ShareMenuListUseCaseTests.swift | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 koinUnitTests/Dining/ShareMenuListUseCaseTests.swift diff --git a/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift b/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift new file mode 100644 index 00000000..c3471107 --- /dev/null +++ b/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift @@ -0,0 +1,84 @@ +// +// ShareMenuListUseCaseTests.swift +// koinUnitTests +// +// Created by 이은지 on 8/8/26. +// + +import Foundation +import Testing +@testable import koin + +@Suite("ShareMenuListUseCase - 카카오톡 식단 공유하기") +struct ShareMenuListUseCaseTests { + + @Test("DiningItem을 ShareDiningMenu로 변환하면 메뉴와 이미지를 유지한다") + func DiningItem을_ShareDiningMenu로_변환하면_메뉴와_이미지를_유지한다() { + let item = DiningFixture.item( + type: .lunch, + place: .cornerA, + menu: ["김치찌개", "밥"], + imageUrl: "url1" + ) + + let shareModel = item.toShareDiningItem() + + #expect(shareModel.menuList == ["김치찌개", "밥"]) + #expect(shareModel.imageUrl == "url1") + #expect(shareModel.type == .lunch) + #expect(shareModel.place == .cornerA) + } + + @Test("날짜를 yyMMdd 형식으로 변환한다") + func 날짜를_yyMMdd_형식으로_변환한다() { + let item = DiningFixture.item(date: "2026-08-03") + + let shareModel = item.toShareDiningItem() + + #expect(shareModel.date == "260803") + } + + /// `DateFormatter`는 구분자가 달라도 관대하게 파싱하기 때문에 "2026/08/03"은 fallback 경로를 타지 않는다. + /// (실제로 "260803"으로 변환된다.) 그래서 파싱이 확실히 실패하는 입력만 검증한다. + @Test( + "날짜 변환에 실패하면 원본 문자열을 사용한다", + arguments: ["", "날짜없음", "2026-13-45"] + ) + func 날짜_변환에_실패하면_원본_문자열을_사용한다(invalidDate: String) { + let item = DiningFixture.item(date: invalidDate) + + let shareModel = item.toShareDiningItem() + + #expect(shareModel.date == invalidDate) + } + + @Test("구분자가 달라도 파싱에 성공하면 yyMMdd로 변환된다") + func 구분자가_달라도_파싱에_성공하면_yyMMdd로_변환된다() { + let item = DiningFixture.item(date: "2026/08/03") + + let shareModel = item.toShareDiningItem() + + #expect(shareModel.date == "260803") + } + + @Test("공유 모델을 레포지토리에 전달한다") + func 공유_모델을_레포지토리에_전달한다() throws { + let spy = SpyDiningRepository() + let sut = DefaultShareMenuListUseCase(diningRepository: spy) + let shareModel = DiningFixture.item( + date: "2026-08-03", + place: .cornerB, + menu: ["돈까스"], + imageUrl: "url2" + ).toShareDiningItem() + + sut.execute(shareModel: shareModel) + + #expect(spy.receivedShareModels.count == 1) + let received = try #require(spy.receivedShareModels.first) + #expect(received.menuList == shareModel.menuList) + #expect(received.imageUrl == shareModel.imageUrl) + #expect(received.date == shareModel.date) + #expect(received.place == shareModel.place) + } +} From 731660548a1446a46c0e4d819da4dd781715e150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:24:59 +0900 Subject: [PATCH 010/101] =?UTF-8?q?chore:=20=EC=8B=9D=EB=8B=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20koinUnitTest?= =?UTF-8?q?s=20=ED=83=80=EA=B2=9F=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a8e88593..b4e1701e 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -997,6 +997,12 @@ D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC69228E302700E800EE26ED /* koinUnitTests.swift */; }; + AA20000000000000000001 /* PublisherTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000001 /* PublisherTestSupport.swift */; }; + AA20000000000000000002 /* DiningFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000002 /* DiningFixtures.swift */; }; + AA20000000000000000003 /* SpyDiningRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000003 /* SpyDiningRepository.swift */; }; + AA20000000000000000004 /* DateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000004 /* DateProviderTests.swift */; }; + AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */; }; + AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; F1A000010000000000000003 /* ZoomingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */; }; /* End PBXBuildFile section */ @@ -2011,6 +2017,12 @@ D8F5C5532E6F144300FB6708 /* floatingLogo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = floatingLogo.json; sourceTree = ""; }; EC692285302700A400EE26ED /* koinUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EC69228E302700E800EE26ED /* koinUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = koinUnitTests.swift; sourceTree = ""; }; + AA10000000000000000001 /* PublisherTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherTestSupport.swift; sourceTree = ""; }; + AA10000000000000000002 /* DiningFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningFixtures.swift; sourceTree = ""; }; + AA10000000000000000003 /* SpyDiningRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpyDiningRepository.swift; sourceTree = ""; }; + AA10000000000000000004 /* DateProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderTests.swift; sourceTree = ""; }; + AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchDiningListUseCaseTests.swift; sourceTree = ""; }; + AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCaseTests.swift; sourceTree = ""; }; F1A000010000000000000002 /* ZoomingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionView.swift; sourceTree = ""; }; F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionViewCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -5454,11 +5466,41 @@ EC69228F302700E800EE26ED /* koinUnitTests */ = { isa = PBXGroup; children = ( + AA30000000000000000001 /* Support */, + AA30000000000000000002 /* Doubles */, + AA30000000000000000003 /* Dining */, EC69228E302700E800EE26ED /* koinUnitTests.swift */, ); path = koinUnitTests; sourceTree = ""; }; + AA30000000000000000001 /* Support */ = { + isa = PBXGroup; + children = ( + AA10000000000000000001 /* PublisherTestSupport.swift */, + AA10000000000000000002 /* DiningFixtures.swift */, + ); + path = Support; + sourceTree = ""; + }; + AA30000000000000000002 /* Doubles */ = { + isa = PBXGroup; + children = ( + AA10000000000000000003 /* SpyDiningRepository.swift */, + ); + path = Doubles; + sourceTree = ""; + }; + AA30000000000000000003 /* Dining */ = { + isa = PBXGroup; + children = ( + AA10000000000000000004 /* DateProviderTests.swift */, + AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */, + AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */, + ); + path = Dining; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -6616,6 +6658,12 @@ buildActionMask = 2147483647; files = ( EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */, + AA20000000000000000001 /* PublisherTestSupport.swift in Sources */, + AA20000000000000000002 /* DiningFixtures.swift in Sources */, + AA20000000000000000003 /* SpyDiningRepository.swift in Sources */, + AA20000000000000000004 /* DateProviderTests.swift in Sources */, + AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */, + AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 46433660b9ea59a21d49e2648efbec325a47dc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 8 Aug 2026 17:26:46 +0900 Subject: [PATCH 011/101] =?UTF-8?q?delete:=20=EB=B9=88=20example()=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 82 +++++++++---------- koinUnitTests/Dining/DateProviderTests.swift | 1 - .../Dining/ShareMenuListUseCaseTests.swift | 2 - koinUnitTests/koinUnitTests.swift | 18 ---- 4 files changed, 39 insertions(+), 64 deletions(-) delete mode 100644 koinUnitTests/koinUnitTests.swift diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index b4e1701e..cf61dc45 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -730,6 +730,12 @@ 83E96DB82BBC6FBF00164914 /* BusSearchDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E96DB72BBC6FBF00164914 /* BusSearchDto.swift */; }; 83EBB87A2D0FDFD700346018 /* KoinPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBB8792D0FDFD700346018 /* KoinPickerView.swift */; }; 83EBB87E2D0FE1BE00346018 /* FetchKoinPickerDateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EBB87D2D0FE1BE00346018 /* FetchKoinPickerDateUseCase.swift */; }; + AA20000000000000000001 /* PublisherTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000001 /* PublisherTestSupport.swift */; }; + AA20000000000000000002 /* DiningFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000002 /* DiningFixtures.swift */; }; + AA20000000000000000003 /* SpyDiningRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000003 /* SpyDiningRepository.swift */; }; + AA20000000000000000004 /* DateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000004 /* DateProviderTests.swift */; }; + AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */; }; + AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */; }; B438AEE42E756AB600E889C4 /* OrderImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B438AEE32E756AB600E889C4 /* OrderImage.swift */; }; B46B8CC22E76CB7300A8E797 /* ShopSummaryDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */; }; B46B8CC42E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */; }; @@ -996,13 +1002,6 @@ D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */; }; D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; - EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC69228E302700E800EE26ED /* koinUnitTests.swift */; }; - AA20000000000000000001 /* PublisherTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000001 /* PublisherTestSupport.swift */; }; - AA20000000000000000002 /* DiningFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000002 /* DiningFixtures.swift */; }; - AA20000000000000000003 /* SpyDiningRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000003 /* SpyDiningRepository.swift */; }; - AA20000000000000000004 /* DateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000004 /* DateProviderTests.swift */; }; - AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */; }; - AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; F1A000010000000000000003 /* ZoomingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */; }; /* End PBXBuildFile section */ @@ -1745,6 +1744,12 @@ 83EBB8792D0FDFD700346018 /* KoinPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinPickerView.swift; sourceTree = ""; }; 83EBB87D2D0FE1BE00346018 /* FetchKoinPickerDateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKoinPickerDateUseCase.swift; sourceTree = ""; }; A001E2B42845091D00D6C310 /* koin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = koin.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AA10000000000000000001 /* PublisherTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherTestSupport.swift; sourceTree = ""; }; + AA10000000000000000002 /* DiningFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningFixtures.swift; sourceTree = ""; }; + AA10000000000000000003 /* SpyDiningRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpyDiningRepository.swift; sourceTree = ""; }; + AA10000000000000000004 /* DateProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderTests.swift; sourceTree = ""; }; + AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchDiningListUseCaseTests.swift; sourceTree = ""; }; + AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCaseTests.swift; sourceTree = ""; }; B438AEE32E756AB600E889C4 /* OrderImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderImage.swift; sourceTree = ""; }; B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSummaryDto.swift; sourceTree = ""; }; B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopSummaryFromShopUseCase.swift; sourceTree = ""; }; @@ -2016,13 +2021,6 @@ D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationManageable.swift; sourceTree = ""; }; D8F5C5532E6F144300FB6708 /* floatingLogo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = floatingLogo.json; sourceTree = ""; }; EC692285302700A400EE26ED /* koinUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - EC69228E302700E800EE26ED /* koinUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = koinUnitTests.swift; sourceTree = ""; }; - AA10000000000000000001 /* PublisherTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherTestSupport.swift; sourceTree = ""; }; - AA10000000000000000002 /* DiningFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningFixtures.swift; sourceTree = ""; }; - AA10000000000000000003 /* SpyDiningRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpyDiningRepository.swift; sourceTree = ""; }; - AA10000000000000000004 /* DateProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderTests.swift; sourceTree = ""; }; - AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchDiningListUseCaseTests.swift; sourceTree = ""; }; - AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCaseTests.swift; sourceTree = ""; }; F1A000010000000000000002 /* ZoomingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionView.swift; sourceTree = ""; }; F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionViewCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -4555,6 +4553,33 @@ name = Products; sourceTree = ""; }; + AA30000000000000000001 /* Support */ = { + isa = PBXGroup; + children = ( + AA10000000000000000001 /* PublisherTestSupport.swift */, + AA10000000000000000002 /* DiningFixtures.swift */, + ); + path = Support; + sourceTree = ""; + }; + AA30000000000000000002 /* Doubles */ = { + isa = PBXGroup; + children = ( + AA10000000000000000003 /* SpyDiningRepository.swift */, + ); + path = Doubles; + sourceTree = ""; + }; + AA30000000000000000003 /* Dining */ = { + isa = PBXGroup; + children = ( + AA10000000000000000004 /* DateProviderTests.swift */, + AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */, + AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */, + ); + path = Dining; + sourceTree = ""; + }; B46B8D242E770A3100A8E797 /* Fonts */ = { isa = PBXGroup; children = ( @@ -5469,38 +5494,10 @@ AA30000000000000000001 /* Support */, AA30000000000000000002 /* Doubles */, AA30000000000000000003 /* Dining */, - EC69228E302700E800EE26ED /* koinUnitTests.swift */, ); path = koinUnitTests; sourceTree = ""; }; - AA30000000000000000001 /* Support */ = { - isa = PBXGroup; - children = ( - AA10000000000000000001 /* PublisherTestSupport.swift */, - AA10000000000000000002 /* DiningFixtures.swift */, - ); - path = Support; - sourceTree = ""; - }; - AA30000000000000000002 /* Doubles */ = { - isa = PBXGroup; - children = ( - AA10000000000000000003 /* SpyDiningRepository.swift */, - ); - path = Doubles; - sourceTree = ""; - }; - AA30000000000000000003 /* Dining */ = { - isa = PBXGroup; - children = ( - AA10000000000000000004 /* DateProviderTests.swift */, - AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */, - AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */, - ); - path = Dining; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -6657,7 +6654,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - EC692290302700E800EE26ED /* koinUnitTests.swift in Sources */, AA20000000000000000001 /* PublisherTestSupport.swift in Sources */, AA20000000000000000002 /* DiningFixtures.swift in Sources */, AA20000000000000000003 /* SpyDiningRepository.swift in Sources */, diff --git a/koinUnitTests/Dining/DateProviderTests.swift b/koinUnitTests/Dining/DateProviderTests.swift index d354e0f2..6c8a3989 100644 --- a/koinUnitTests/Dining/DateProviderTests.swift +++ b/koinUnitTests/Dining/DateProviderTests.swift @@ -67,7 +67,6 @@ struct DateProviderTests { #expect(result.diningType == .dinner) } - /// 18:31과 23:59는 "다음 날 breakfast"라는 동일한 결과를 기대하므로 매개변수화한다. @Test("18:30 이후면 다음 날 breakfast를 반환한다", arguments: [(18, 31), (23, 59)]) func 열여덟시_삼십분_이후면_다음_날_breakfast를_반환한다(hour: Int, minute: Int) throws { let input = try date(hour: hour, minute: minute) diff --git a/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift b/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift index c3471107..528b5c06 100644 --- a/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift +++ b/koinUnitTests/Dining/ShareMenuListUseCaseTests.swift @@ -38,8 +38,6 @@ struct ShareMenuListUseCaseTests { #expect(shareModel.date == "260803") } - /// `DateFormatter`는 구분자가 달라도 관대하게 파싱하기 때문에 "2026/08/03"은 fallback 경로를 타지 않는다. - /// (실제로 "260803"으로 변환된다.) 그래서 파싱이 확실히 실패하는 입력만 검증한다. @Test( "날짜 변환에 실패하면 원본 문자열을 사용한다", arguments: ["", "날짜없음", "2026-13-45"] diff --git a/koinUnitTests/koinUnitTests.swift b/koinUnitTests/koinUnitTests.swift deleted file mode 100644 index fcb4d578..00000000 --- a/koinUnitTests/koinUnitTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// koinUnitTests.swift -// koinUnitTests -// -// Created by 이은지 on 8/8/26. -// - -import Testing - -struct koinUnitTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - // Swift Testing Documentation - // https://developer.apple.com/documentation/testing - } - -} From 43c7dcbe6a009930f58ba15da953e3bd0efef3c1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 13:26:37 +0900 Subject: [PATCH 012/101] =?UTF-8?q?design:=20=EA=B2=8C=EC=8B=9C=ED=8C=90?= =?UTF-8?q?=20AI=20=EC=9A=94=EC=95=BD=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/Extensions/Asset/ImageAsset.swift | 1 + .../Decodable/NoticeList/NoticeListDto.swift | 20 +++- .../Model/NoticeList/NoticeAISummary.swift | 44 +++++++ .../Model/NoticeList/NoticeDataInfo.swift | 1 + .../NoticeData/NoticeDataViewController.swift | 29 +++-- .../SubViews/NoticeAISummaryView.swift | 110 ++++++++++++++++++ .../noticeAISummary.imageset/Contents.json | 12 ++ .../noticeAISummary.svg | 5 + koin.xcodeproj/project.pbxproj | 8 ++ 9 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 Koin/Domain/Model/NoticeList/NoticeAISummary.swift create mode 100644 Koin/Presentation/Notice/NoticeData/SubViews/NoticeAISummaryView.swift create mode 100644 Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/Contents.json create mode 100644 Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/noticeAISummary.svg diff --git a/Koin/Core/Extensions/Asset/ImageAsset.swift b/Koin/Core/Extensions/Asset/ImageAsset.swift index c4e6acd1..8a253e2c 100644 --- a/Koin/Core/Extensions/Asset/ImageAsset.swift +++ b/Koin/Core/Extensions/Asset/ImageAsset.swift @@ -208,6 +208,7 @@ public enum ImageAsset: String { case lostItemDelete // MARK: - Notice + case noticeAISummary case noticeLoginToolTip case noticeManageKeyword case noticeNotLoginToolTip diff --git a/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift b/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift index 16419550..424f8139 100644 --- a/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift +++ b/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift @@ -71,7 +71,25 @@ extension NoticeListDto { extension NoticeArticleDto { func toDomain() -> NoticeDataInfo { - return NoticeDataInfo(title: title ?? "", boardId: boardId, content: content ?? "", author: author ?? "-", hit: hit, prevId: prevId, nextId: nextId, attachments: attachments ?? [], url: url, registeredAt: registeredAt) + return NoticeDataInfo( + title: title ?? "", + boardId: boardId, + aiSummary: .init( // TODO: API v2 + status: .success, + items: [ + .init(icon: "📅", text: "인공지능 및 컴퓨터공학 전공 학부생 또는 대학원생은 이력서와 연구계획서를 dice_lab@kaist.ac.kr로 제출해야 합니다."), + .init(icon: "🎯", text: "프로그래밍 역량이 필수이며, 대상은 인공지능 및 컴퓨터공학 전공 학부생 또는 대학원생입니다."), + .init(icon: "📝", text: "모집 공고는 2025-03-01에 게시되었습니다.") + ]), + content: content ?? "", + author: author ?? "-", + hit: hit, + prevId: prevId, + nextId: nextId, + attachments: attachments ?? [], + url: url, + registeredAt: registeredAt + ) } func toDomainWithChangedDate() -> NoticeArticleDto { diff --git a/Koin/Domain/Model/NoticeList/NoticeAISummary.swift b/Koin/Domain/Model/NoticeList/NoticeAISummary.swift new file mode 100644 index 00000000..5ac28fb1 --- /dev/null +++ b/Koin/Domain/Model/NoticeList/NoticeAISummary.swift @@ -0,0 +1,44 @@ +// +// NoticeAISummary.swift +// koin +// +// Created by 홍기정 on 8/11/26. +// + +import Foundation + +struct NoticeAISummary { + let status: NoticeAISummaryStatus + let items: [NoticeAISummaryItem] +} + +struct NoticeAISummaryItem: Identifiable { + var id: String { + icon + text + } + + let icon: String + let text: String +} + +enum NoticeAISummaryStatus { + case loading + case success + case pending + case unavailable +} + +extension NoticeAISummaryItem { + var attributedString: AttributedString { + var attributedString = AttributedString("\(icon) \(text)") + + if let iconRange = attributedString.range(of: icon) { + attributedString[iconRange].font = .system(size: 14) + } + if let textRange = attributedString.range(of: text) { + attributedString[textRange].font = .appFont(.pretendardRegular, size: 14) + } + + return attributedString + } +} diff --git a/Koin/Domain/Model/NoticeList/NoticeDataInfo.swift b/Koin/Domain/Model/NoticeList/NoticeDataInfo.swift index 7471f20a..4e5914c1 100644 --- a/Koin/Domain/Model/NoticeList/NoticeDataInfo.swift +++ b/Koin/Domain/Model/NoticeList/NoticeDataInfo.swift @@ -10,6 +10,7 @@ import Foundation struct NoticeDataInfo { let title: String let boardId: Int + let aiSummary: NoticeAISummary let content: String let author: String let hit: Int? diff --git a/Koin/Presentation/Notice/NoticeData/NoticeDataViewController.swift b/Koin/Presentation/Notice/NoticeData/NoticeDataViewController.swift index ee1dcebc..ccf8e421 100644 --- a/Koin/Presentation/Notice/NoticeData/NoticeDataViewController.swift +++ b/Koin/Presentation/Notice/NoticeData/NoticeDataViewController.swift @@ -78,7 +78,11 @@ final class NoticeDataViewController: UIViewController, UIGestureRecognizerDeleg private let scrollView = UIScrollView() private let contentView = UIView().then { - $0.backgroundColor = .white + $0.backgroundColor = .appColor(.neutral100) + } + + private let aiSummaryViewHostingController = NoticeAISummaryViewHostingController().then { + $0.sizingOptions = .intrinsicContentSize } private let contentTextView = UITextView().then { @@ -96,14 +100,6 @@ final class NoticeDataViewController: UIViewController, UIGestureRecognizerDeleg $0.text = "첨부파일" } - private let separateView1 = UIView().then { - $0.backgroundColor = UIColor.appColor(.neutral100) - } - - private let separateView2 = UIView().then { - $0.backgroundColor = UIColor.appColor(.neutral100) - } - private let noticeAttachmentsTableView = NoticeAttachmentsTableView(frame: .zero, style: .plain) // MARK: - Initialization @@ -132,6 +128,7 @@ final class NoticeDataViewController: UIViewController, UIGestureRecognizerDeleg inputSubject.send(.getPopularNotices) commonConfigureView() + aiSummaryViewHostingController.didMove(toParent: self) inputSubject.send(.getNoticeData) } @@ -281,6 +278,8 @@ extension NoticeDataViewController { urlRedirectButton.isHidden = true } } + + aiSummaryViewHostingController.configure(summary: noticeData.aiSummary) } private func updatePopularArticle(notices: [NoticeArticleDto]) { @@ -332,9 +331,11 @@ extension NoticeDataViewController { } private func setUpLayOuts() { + addChild(aiSummaryViewHostingController) + view.addSubview(scrollView) scrollView.addSubview(contentView) - [titleWrappedView, contentWrappedView,popularNoticeWrappedView].forEach { + [titleWrappedView, aiSummaryViewHostingController.view, contentWrappedView,popularNoticeWrappedView].forEach { contentView.addSubview($0) } [titleGuideLabel, titleLabel, createdDateLabel, separatorDotLabel, nickNameLabel, separatorDot2Label, eyeImageView, hitLabel].forEach { @@ -401,10 +402,16 @@ extension NoticeDataViewController { $0.top.equalTo(nickNameLabel) $0.height.equalTo(19) } - contentWrappedView.snp.makeConstraints { + + aiSummaryViewHostingController.view.snp.makeConstraints { $0.top.equalTo(titleWrappedView.snp.bottom).offset(6) $0.leading.trailing.equalToSuperview() } + + contentWrappedView.snp.makeConstraints { + $0.top.equalTo(aiSummaryViewHostingController.view.snp.bottom).offset(6) + $0.leading.trailing.equalToSuperview() + } contentTextView.snp.makeConstraints { $0.top.equalToSuperview().offset(16) $0.leading.equalToSuperview().offset(24) diff --git a/Koin/Presentation/Notice/NoticeData/SubViews/NoticeAISummaryView.swift b/Koin/Presentation/Notice/NoticeData/SubViews/NoticeAISummaryView.swift new file mode 100644 index 00000000..4cca5cd2 --- /dev/null +++ b/Koin/Presentation/Notice/NoticeData/SubViews/NoticeAISummaryView.swift @@ -0,0 +1,110 @@ +// +// NoticeAISummaryView.swift +// koin +// +// Created by 홍기정 on 8/11/26. +// + +import SwiftUI + +final class NoticeAISummaryViewHostingController: UIHostingController { + + // MARK: - Initializer + init() { + super.init(rootView: NoticeAISummaryView(summary: .init(status: .loading, items: []))) + } + @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + func configure(summary: NoticeAISummary) { + rootView = NoticeAISummaryView(summary: summary) + } +} + +struct NoticeAISummaryView: View { + + // MARK: - Properties + let summary: NoticeAISummary + + // MARK: - Initializer + init(summary: NoticeAISummary) { + self.summary = summary + } + + // MARK: - Body + var body: some View { + VStack(alignment: .leading, spacing: 8) { + header + + switch summary.status { + case .loading: + EmptyView() + case .success: + successView + case .pending: + pendingView + case .unavailable: + unavailableView + } + } + .padding(.vertical, 12) + .padding(.horizontal, 24) + .frame(maxWidth: .infinity, alignment: .leading) + } + + @ViewBuilder + var header: some View { + HStack(alignment: .center, spacing: 2) { + Text("AI 요약") + .font(.appFont(.pretendardMedium, size: 14)) + .foregroundStyle(Color.appColor(.new800)) + + Image.appImage(asset: .noticeAISummary) + + Spacer() + } + .frame(height: 22) + } + + @ViewBuilder + var pendingView: some View { + HStack(alignment: .center, spacing: 0) { + Spacer() + + Text("요약중...") + .font(.appFont(.pretendardRegular, size: 14)) + .foregroundStyle(Color.appColor(.neutral500)) + .frame(minHeight: 22) + + Spacer() + } + .padding(EdgeInsets(top: 40, leading: 0, bottom: 52, trailing: 0)) + } + + @ViewBuilder + var unavailableView: some View { + HStack(alignment: .center, spacing: 0) { + Spacer() + + Text("요약할 내용이 없어요.") + .font(.appFont(.pretendardRegular, size: 14)) + .foregroundStyle(Color.appColor(.neutral500)) + .frame(minHeight: 22) + + Spacer() + } + .padding(EdgeInsets(top: 40, leading: 0, bottom: 52, trailing: 0)) + } + + @ViewBuilder + var successView: some View { + VStack(alignment: .leading, spacing: 22) { + ForEach(summary.items) { item in + Text(item.attributedString) + .linespacing(fontSize: 14, percent: 160) + } + } + } +} diff --git a/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/Contents.json b/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/Contents.json new file mode 100644 index 00000000..0fbb5d27 --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "noticeAISummary.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/noticeAISummary.svg b/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/noticeAISummary.svg new file mode 100644 index 00000000..a1b9b00f --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Notice/noticeAISummary.imageset/noticeAISummary.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 53fc9e6d..641381da 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -217,6 +217,8 @@ 7C7BD71D302B2B0A003C5A15 /* AppImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD71C302B2B0A003C5A15 /* AppImage.swift */; }; 7C7BD71F302B2B37003C5A15 /* PostLostItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD71E302B2B37003C5A15 /* PostLostItemUseCase.swift */; }; 7C7BD721302B2C25003C5A15 /* ReportLostItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD720302B2C25003C5A15 /* ReportLostItemUseCase.swift */; }; + 7C7BD723302B3D8F003C5A15 /* NoticeAISummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */; }; + 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */; }; 7C7CCE4A2F834D4B00E3A54B /* CallVanModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */; }; 7C7CCE702F866E9400E3A54B /* AppPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */; }; 7C7CCE722F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */; }; @@ -1245,6 +1247,8 @@ 7C7BD71C302B2B0A003C5A15 /* AppImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppImage.swift; sourceTree = ""; }; 7C7BD71E302B2B37003C5A15 /* PostLostItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLostItemUseCase.swift; sourceTree = ""; }; 7C7BD720302B2C25003C5A15 /* ReportLostItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportLostItemUseCase.swift; sourceTree = ""; }; + 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummary.swift; sourceTree = ""; }; + 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummaryView.swift; sourceTree = ""; }; 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanModalViewController.swift; sourceTree = ""; }; 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPath.swift; sourceTree = ""; }; 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCallVanRestrictionUseCase.swift; sourceTree = ""; }; @@ -3682,6 +3686,7 @@ children = ( 7CED8F662EF2D1F900457128 /* HotNoticeArticlesTableView */, 7CED8F692EF2D1F900457128 /* NoticeAttachmentsTableView */, + 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */, ); path = SubViews; sourceTree = ""; @@ -4394,6 +4399,7 @@ 833D9C422C7061EC00982145 /* NoticeArticlesInfo.swift */, 833D9C332C70418100982145 /* NoticeListPages.swift */, 833D9C5F2C7457EE00982145 /* NoticeDataInfo.swift */, + 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */, 831ABDC32CA29A120099B70C /* NoticeKeywordsFetchResult.swift */, 838F114F2CA5330900BF3B25 /* AddNoticeKeywordType.swift */, ); @@ -5977,6 +5983,7 @@ 7CED90D52EF2D1F900457128 /* SelectDeptModalViewController.swift in Sources */, 7CED90D72EF2D1F900457128 /* RecommendedKeywordCollectionView.swift in Sources */, 7CED90D92EF2D1F900457128 /* DiningViewController.swift in Sources */, + 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */, 7CCB1E4B2F29D87C00472669 /* PostChatDetailUseCase.swift in Sources */, 7CED90DA2EF2D1F900457128 /* RecentSearchTableView.swift in Sources */, 7C61D35F2F24000C005FAC3A /* LostItemDataButtonsView.swift in Sources */, @@ -6400,6 +6407,7 @@ D208A7BB2CA169DF007040E7 /* FetchBeneficialShopUseCase.swift in Sources */, D2D462712D63C5ED00C60864 /* ChatRoomItem.swift in Sources */, 7C8ADD2D2F20B43F00F85BDE /* UpdateLostItemRequest.swift in Sources */, + 7C7BD723302B3D8F003C5A15 /* NoticeAISummary.swift in Sources */, D2B7EBCA2D37C81B00EE46B0 /* PostLostItemRequest.swift in Sources */, 0FCD163D2BA80EAA00C23A2F /* KeychainWorker.swift in Sources */, 8354BE3D2C79D709009D4D7A /* AddNotificationKeywordUseCase.swift in Sources */, From 547051877ea0cb2155b053d3d86ec5b5f59ba220 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 13:56:09 +0900 Subject: [PATCH 013/101] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20A?= =?UTF-8?q?I=20=EC=9A=94=EC=95=BD=20API=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NoticeList/NoticeAISummaryDto.swift | 52 +++++++++++++++++++ .../Decodable/NoticeList/NoticeListDto.swift | 11 ++-- .../Service/Network/API/NoticeListAPI.swift | 2 +- koin.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 Koin/Data/DTOs/Decodable/NoticeList/NoticeAISummaryDto.swift diff --git a/Koin/Data/DTOs/Decodable/NoticeList/NoticeAISummaryDto.swift b/Koin/Data/DTOs/Decodable/NoticeList/NoticeAISummaryDto.swift new file mode 100644 index 00000000..bd140879 --- /dev/null +++ b/Koin/Data/DTOs/Decodable/NoticeList/NoticeAISummaryDto.swift @@ -0,0 +1,52 @@ +// +// NoticeAISummaryDto.swift +// koin +// +// Created by 홍기정 on 8/13/26. +// + +import Foundation + +struct NoticeAISummaryDto: Decodable { + let status: NoticeAISummaryStatusDto + let items: [NoticeAISummaryItemDto] +} + +struct NoticeAISummaryItemDto: Decodable { + let icon: String + let text: String +} + +enum NoticeAISummaryStatusDto: String, Decodable { + case success = "SUCCESS" + case pending = "PENDING" + case unavailable = "UNAVAILABLE" +} + +extension NoticeAISummaryDto { + func toDomain() -> NoticeAISummary { + NoticeAISummary( + status: status.toDomain(), + items: items.map { $0.toDomain() } + ) + } +} + +extension NoticeAISummaryItemDto { + func toDomain() -> NoticeAISummaryItem { + NoticeAISummaryItem( + icon: icon, + text: text + ) + } +} + +extension NoticeAISummaryStatusDto { + func toDomain() -> NoticeAISummaryStatus { + switch self { + case .success: .success + case .pending: .pending + case .unavailable: .unavailable + } + } +} diff --git a/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift b/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift index 424f8139..2fa8ea8d 100644 --- a/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift +++ b/Koin/Data/DTOs/Decodable/NoticeList/NoticeListDto.swift @@ -26,6 +26,7 @@ struct NoticeArticleDto: Decodable { let boardId: Int let title: String? let content: String? + let aiSummary: NoticeAISummaryDto? let author: String? let hit: Int? let url: String? @@ -39,6 +40,7 @@ struct NoticeArticleDto: Decodable { case id case boardId = "board_id" case title, content, author, hit, url, attachments + case aiSummary = "ai_summary" case prevId = "prev_id" case nextId = "next_id" case registeredAt = "registered_at" @@ -74,13 +76,7 @@ extension NoticeArticleDto { return NoticeDataInfo( title: title ?? "", boardId: boardId, - aiSummary: .init( // TODO: API v2 - status: .success, - items: [ - .init(icon: "📅", text: "인공지능 및 컴퓨터공학 전공 학부생 또는 대학원생은 이력서와 연구계획서를 dice_lab@kaist.ac.kr로 제출해야 합니다."), - .init(icon: "🎯", text: "프로그래밍 역량이 필수이며, 대상은 인공지능 및 컴퓨터공학 전공 학부생 또는 대학원생입니다."), - .init(icon: "📝", text: "모집 공고는 2025-03-01에 게시되었습니다.") - ]), + aiSummary: aiSummary?.toDomain() ?? NoticeAISummary(status: .unavailable, items: []), content: content ?? "", author: author ?? "-", hit: hit, @@ -104,6 +100,7 @@ extension NoticeArticleDto { boardId: boardId, title: newTitle, content: modifyFontInHtml(html: content ?? ""), + aiSummary: aiSummary, author: author, hit: hit, url: url, diff --git a/Koin/Data/Service/Network/API/NoticeListAPI.swift b/Koin/Data/Service/Network/API/NoticeListAPI.swift index 5a3d84ae..dd4e086f 100644 --- a/Koin/Data/Service/Network/API/NoticeListAPI.swift +++ b/Koin/Data/Service/Network/API/NoticeListAPI.swift @@ -30,7 +30,7 @@ extension NoticeListAPI: Router, URLRequestConvertible { switch self { case .fetchNoticeArticles: return "/articles" case .searchNoticeArticle: return "/articles/search" - case .fetchNoticeData(let request): return "/articles/\(request.noticeId)" + case .fetchNoticeData(let request): return "/v2/articles/\(request.noticeId)" case .fetchHotNoticeArticles: return "/articles/hot" case .createNotificationKeyword: return "/articles/keyword?type=KOREATECH" case .deleteNotificationKeyword(let request): return "/articles/keyword/\(request)" diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 641381da..1a2a1da9 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -219,6 +219,7 @@ 7C7BD721302B2C25003C5A15 /* ReportLostItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD720302B2C25003C5A15 /* ReportLostItemUseCase.swift */; }; 7C7BD723302B3D8F003C5A15 /* NoticeAISummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */; }; 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */; }; + 7C7BD727302D8002003C5A15 /* NoticeAISummaryDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD726302D8002003C5A15 /* NoticeAISummaryDto.swift */; }; 7C7CCE4A2F834D4B00E3A54B /* CallVanModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */; }; 7C7CCE702F866E9400E3A54B /* AppPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */; }; 7C7CCE722F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */; }; @@ -1249,6 +1250,7 @@ 7C7BD720302B2C25003C5A15 /* ReportLostItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportLostItemUseCase.swift; sourceTree = ""; }; 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummary.swift; sourceTree = ""; }; 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummaryView.swift; sourceTree = ""; }; + 7C7BD726302D8002003C5A15 /* NoticeAISummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummaryDto.swift; sourceTree = ""; }; 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanModalViewController.swift; sourceTree = ""; }; 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPath.swift; sourceTree = ""; }; 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCallVanRestrictionUseCase.swift; sourceTree = ""; }; @@ -4410,6 +4412,7 @@ isa = PBXGroup; children = ( 833D9C212C6B4F6E00982145 /* NoticeListDto.swift */, + 7C7BD726302D8002003C5A15 /* NoticeAISummaryDto.swift */, 833D9C272C6B58ED00982145 /* NoticeKeywordDto.swift */, ); path = NoticeList; @@ -5986,6 +5989,7 @@ 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */, 7CCB1E4B2F29D87C00472669 /* PostChatDetailUseCase.swift in Sources */, 7CED90DA2EF2D1F900457128 /* RecentSearchTableView.swift in Sources */, + 7C7BD727302D8002003C5A15 /* NoticeAISummaryDto.swift in Sources */, 7C61D35F2F24000C005FAC3A /* LostItemDataButtonsView.swift in Sources */, 7CED90DB2EF2D1F900457128 /* ChatImageTableViewCell.swift in Sources */, 7CED90DC2EF2D1F900457128 /* ShopSummaryDeliveryButton.swift in Sources */, From f6b88e59feb6e45ddd98fd3657e08a1e4c50de06 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 16:16:43 +0900 Subject: [PATCH 014/101] =?UTF-8?q?delete:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/ColorSystem.swift | 277 ----------------------------------------- 1 file changed, 277 deletions(-) delete mode 100644 Koin/ColorSystem.swift diff --git a/Koin/ColorSystem.swift b/Koin/ColorSystem.swift deleted file mode 100644 index 9a6e1389..00000000 --- a/Koin/ColorSystem.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// ColorSystem.swift -// koin -// -// Created by 이은지 on 10/16/25. -// - -import UIKit - -extension UIColor { - - enum ColorSystem { - - // MARK: - Neutral - - enum Neutral { - static let gray0 = UIColor(hexCode: "FFFFFF") - static let gray50 = UIColor(hexCode: "FAFAFA") - static let gray100 = UIColor(hexCode: "F5F5F5") - static let gray200 = UIColor(hexCode: "EEEEEE") - static let gray300 = UIColor(hexCode: "E6E6E6") - static let gray400 = UIColor(hexCode: "D9D9D9") - static let gray500 = UIColor(hexCode: "A8A8A8") - static let gray600 = UIColor(hexCode: "6f6f6f") - static let gray700 = UIColor(hexCode: "4B4B4B") - static let gray800 = UIColor(hexCode: "323232") - static let gray900 = UIColor(hexCode: "1F1F1F") - } - - // MARK: - Danger - - enum Danger { - static let red100 = UIColor(hexCode: "FBEEEE") - static let red200 = UIColor(hexCode: "F5D1D1") - static let red300 = UIColor(hexCode: "F0B7B7") - static let red400 = UIColor(hexCode: "E99696") - static let red500 = UIColor(hexCode: "E37878") - static let red600 = UIColor(hexCode: "DE5F5F") - static let red700 = UIColor(hexCode: "D94A4A") - static let red800 = UIColor(hexCode: "D63939") - static let red900 = UIColor(hexCode: "C92A2A") - static let red1000 = UIColor(hexCode: "B12525") - static let red1100 = UIColor(hexCode: "982020") - static let red1200 = UIColor(hexCode: "871C1C") - static let red1300 = UIColor(hexCode: "721818") - static let red1400 = UIColor(hexCode: "541212") - } - - // MARK: - SubColor - - enum SubColor { - static let orange100 = UIColor(hexCode: "FEF2D1") - static let orange200 = UIColor(hexCode: "FEE1A4") - static let orange300 = UIColor(hexCode: "FED98B") - static let orange400 = UIColor(hexCode: "FCCC77") - static let orange500 = UIColor(hexCode: "FAB655") - static let orange600 = UIColor(hexCode: "F9AE43") - static let orange700 = UIColor(hexCode: "F7941E") - static let orange800 = UIColor(hexCode: "D47415") - static let orange900 = UIColor(hexCode: "B1580F") - static let orange1000 = UIColor(hexCode: "A4470D") - static let orange1100 = UIColor(hexCode: "8F3F09") - static let orange1200 = UIColor(hexCode: "7D3708") - static let orange1300 = UIColor(hexCode: "682C06") - static let orange1400 = UIColor(hexCode: "532004") - } - - // MARK: - Warning - - enum Warning { - static let yellow100 = UIColor(hexCode: "FDF3E2") - static let yellow200 = UIColor(hexCode: "FBEACC") - static let yellow300 = UIColor(hexCode: "F9DFB3") - static let yellow400 = UIColor(hexCode: "F6D38F") - static let yellow500 = UIColor(hexCode: "F3C873") - static let yellow600 = UIColor(hexCode: "F1BD5E") - static let yellow700 = UIColor(hexCode: "EDB345") - static let yellow800 = UIColor(hexCode: "E8A62A") - static let yellow900 = UIColor(hexCode: "DD9616") - static let yellow1000 = UIColor(hexCode: "C38312") - static let yellow1100 = UIColor(hexCode: "AF740F") - static let yellow1200 = UIColor(hexCode: "845B0D") - static let yellow1300 = UIColor(hexCode: "5E4209") - static let yellow1400 = UIColor(hexCode: "3D2A06") - } - - // MARK: - Chartreuse - - enum Chartreuse { - static let chartreuse100 = UIColor(hexCode: "DBFC6E") - static let chartreuse200 = UIColor(hexCode: "CBF443") - static let chartreuse300 = UIColor(hexCode: "BCE92A") - static let chartreuse400 = UIColor(hexCode: "AAD816") - static let chartreuse500 = UIColor(hexCode: "98C50A") - static let chartreuse600 = UIColor(hexCode: "87B103") - static let chartreuse700 = UIColor(hexCode: "769C00") - static let chartreuse800 = UIColor(hexCode: "678800") - static let chartreuse900 = UIColor(hexCode: "577400") - static let chartreuse1000 = UIColor(hexCode: "486000") - static let chartreuse1100 = UIColor(hexCode: "3A4D00") - static let chartreuse1200 = UIColor(hexCode: "2C3B00") - static let chartreuse1300 = UIColor(hexCode: "212C00") - static let chartreuse1400 = UIColor(hexCode: "181F00") - } - - // MARK: - Celery - - enum Celery { - static let celery100 = UIColor(hexCode: "CDFCBF") - static let celery200 = UIColor(hexCode: "AEF69D") - static let celery300 = UIColor(hexCode: "96EE85") - static let celery400 = UIColor(hexCode: "72E06A") - static let celery500 = UIColor(hexCode: "4ECF50") - static let celery600 = UIColor(hexCode: "27BB36") - static let celery700 = UIColor(hexCode: "07A721") - static let celery800 = UIColor(hexCode: "009112") - static let celery900 = UIColor(hexCode: "007C0F") - static let celery1000 = UIColor(hexCode: "00670F") - static let celery1100 = UIColor(hexCode: "00530D") - static let celery1200 = UIColor(hexCode: "00400A") - static let celery1300 = UIColor(hexCode: "003007") - static let celery1400 = UIColor(hexCode: "002205") - } - - // MARK: - Success - - enum Success { - static let green100 = UIColor(hexCode: "E5F4EC") - static let green200 = UIColor(hexCode: "D0EBDD") - static let green300 = UIColor(hexCode: "BAE9D1") - static let green400 = UIColor(hexCode: "A8E3C6") - static let green500 = UIColor(hexCode: "93DCB8") - static let green600 = UIColor(hexCode: "50CE83") - static let green700 = UIColor(hexCode: "36BF6E") - static let green800 = UIColor(hexCode: "2DA05C") - static let green900 = UIColor(hexCode: "288F52") - static let green1000 = UIColor(hexCode: "228149") - static let green1100 = UIColor(hexCode: "1F7743") - static let green1200 = UIColor(hexCode: "196137") - static let green1300 = UIColor(hexCode: "15512E") - static let green1400 = UIColor(hexCode: "0F3920") - } - - // MARK: - Seafoam - - enum Seafoam { - static let seafoam100 = UIColor(hexCode: "CEF7F3") - static let seafoam200 = UIColor(hexCode: "AAF1EA") - static let seafoam300 = UIColor(hexCode: "8CE9E2") - static let seafoam400 = UIColor(hexCode: "65DAD2") - static let seafoam500 = UIColor(hexCode: "3FC9C1") - static let seafoam600 = UIColor(hexCode: "0FB5AE") - static let seafoam700 = UIColor(hexCode: "00A19A") - static let seafoam800 = UIColor(hexCode: "008C87") - static let seafoam900 = UIColor(hexCode: "007772") - static let seafoam1000 = UIColor(hexCode: "00635F") - static let seafoam1100 = UIColor(hexCode: "0C4F4C") - static let seafoam1200 = UIColor(hexCode: "123C3A") - static let seafoam1300 = UIColor(hexCode: "122C2B") - static let seafoam1400 = UIColor(hexCode: "0F1F1E") - } - - // MARK: - Cyan - - enum Cyan { - static let cyan100 = UIColor(hexCode: "C5F8FF") - static let cyan200 = UIColor(hexCode: "A4F0FF") - static let cyan300 = UIColor(hexCode: "88E7FA") - static let cyan400 = UIColor(hexCode: "60D8F3") - static let cyan500 = UIColor(hexCode: "33C5E8") - static let cyan600 = UIColor(hexCode: "12B0DA") - static let cyan700 = UIColor(hexCode: "019CC8") - static let cyan800 = UIColor(hexCode: "0086B4") - static let cyan900 = UIColor(hexCode: "00719F") - static let cyan1000 = UIColor(hexCode: "005D89") - static let cyan1100 = UIColor(hexCode: "004A73") - static let cyan1200 = UIColor(hexCode: "00395D") - static let cyan1300 = UIColor(hexCode: "002A46") - static let cyan1400 = UIColor(hexCode: "001E33") - } - - // MARK: - Info - - enum Info { - static let blue100 = UIColor(hexCode: "F8FCFF") - static let blue200 = UIColor(hexCode: "D6EEFF") - static let blue300 = UIColor(hexCode: "CCE5FF") - static let blue400 = UIColor(hexCode: "BDDDFF") - static let blue500 = UIColor(hexCode: "A8D2FF") - static let blue600 = UIColor(hexCode: "75B7FF") - static let blue700 = UIColor(hexCode: "3394FF") - static let blue800 = UIColor(hexCode: "1A87FF") - static let blue900 = UIColor(hexCode: "195BE6") - static let blue1000 = UIColor(hexCode: "1A55CF") - static let blue1100 = UIColor(hexCode: "1B4FBB") - static let blue1200 = UIColor(hexCode: "1947A9") - static let blue1300 = UIColor(hexCode: "143885") - static let blue1400 = UIColor(hexCode: "0D2559") - } - - // MARK: - Indigo - - enum Indigo { - static let indigo100 = UIColor(hexCode: "EDEEFF") - static let indigo200 = UIColor(hexCode: "E0E2FF") - static let indigo300 = UIColor(hexCode: "D3D5FF") - static let indigo400 = UIColor(hexCode: "C1C4FF") - static let indigo500 = UIColor(hexCode: "ACAFFF") - static let indigo600 = UIColor(hexCode: "9599FF") - static let indigo700 = UIColor(hexCode: "7E84FC") - static let indigo800 = UIColor(hexCode: "686DF4") - static let indigo900 = UIColor(hexCode: "5258E4") - static let indigo1000 = UIColor(hexCode: "4046CA") - static let indigo1100 = UIColor(hexCode: "3236A8") - static let indigo1200 = UIColor(hexCode: "262986") - static let indigo1300 = UIColor(hexCode: "1B1E64") - static let indigo1400 = UIColor(hexCode: "141648") - } - - // MARK: - Primary - - enum Primary { - static let purple100 = UIColor(hexCode: "F5EBFF") - static let purple200 = UIColor(hexCode: "DDB1FE") - static let purple300 = UIColor(hexCode: "D39AFE") - static let purple400 = UIColor(hexCode: "CE86FD") - static let purple500 = UIColor(hexCode: "C969FC") - static let purple600 = UIColor(hexCode: "C358FC") - static let purple700 = UIColor(hexCode: "B611F5") - static let purple800 = UIColor(hexCode: "980AC9") - static let purple900 = UIColor(hexCode: "7D08A4") - static let purple1000 = UIColor(hexCode: "6F09A2") - static let purple1100 = UIColor(hexCode: "600481") - static let purple1200 = UIColor(hexCode: "550472") - static let purple1300 = UIColor(hexCode: "44025E") - static let purple1400 = UIColor(hexCode: "2F0141") - } - - // MARK: - Fuchsia - - enum Fuchsia { - static let fuchsia100 = UIColor(hexCode: "FFE9FC") - static let fuchsia200 = UIColor(hexCode: "FFDAFA") - static let fuchsia300 = UIColor(hexCode: "FEC7F8") - static let fuchsia400 = UIColor(hexCode: "FBAEF6") - static let fuchsia500 = UIColor(hexCode: "F592F3") - static let fuchsia600 = UIColor(hexCode: "ED74ED") - static let fuchsia700 = UIColor(hexCode: "E055E2") - static let fuchsia800 = UIColor(hexCode: "CD3ACE") - static let fuchsia900 = UIColor(hexCode: "B622B7") - static let fuchsia1000 = UIColor(hexCode: "9D039E") - static let fuchsia1100 = UIColor(hexCode: "800081") - static let fuchsia1200 = UIColor(hexCode: "640664") - static let fuchsia1300 = UIColor(hexCode: "470E46") - static let fuchsia1400 = UIColor(hexCode: "320D31") - } - - // MARK: - Magenta - - enum Magenta { - static let magenta100 = UIColor(hexCode: "FFEAF1") - static let magenta200 = UIColor(hexCode: "FFDCE8") - static let magenta300 = UIColor(hexCode: "FFCADD") - static let magenta400 = UIColor(hexCode: "FFB2CE") - static let magenta500 = UIColor(hexCode: "FF95BD") - static let magenta600 = UIColor(hexCode: "FA77AA") - static let magenta700 = UIColor(hexCode: "EF5A98") - static let magenta800 = UIColor(hexCode: "DE3D82") - static let magenta900 = UIColor(hexCode: "C82269") - static let magenta1000 = UIColor(hexCode: "AD0955") - static let magenta1100 = UIColor(hexCode: "8E0045") - static let magenta1200 = UIColor(hexCode: "700037") - static let magenta1300 = UIColor(hexCode: "54032A") - static let magenta1400 = UIColor(hexCode: "3C061D") - } - } -} From d2bb0ec7ab93ef9994866944a6766fb6a3634cd1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:00:16 +0900 Subject: [PATCH 015/101] =?UTF-8?q?refactor:=20ModalViewController,=20Moda?= =?UTF-8?q?lViewControllerB=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/View/ModalViewController.swift | 70 +++++- Koin/Core/View/ModalViewControllerB.swift | 230 ------------------ .../Chat/Chat/ChatViewController.swift | 5 +- .../LostItemDataViewController.swift | 21 +- .../LostItemKeywordViewController.swift | 5 +- .../LostItemListViewController.swift | 7 +- .../ManageNoticeKeywordViewController.swift | 4 +- koin.xcodeproj/project.pbxproj | 4 - 8 files changed, 73 insertions(+), 273 deletions(-) delete mode 100644 Koin/Core/View/ModalViewControllerB.swift diff --git a/Koin/Core/View/ModalViewController.swift b/Koin/Core/View/ModalViewController.swift index 5813df5f..459806fb 100644 --- a/Koin/Core/View/ModalViewController.swift +++ b/Koin/Core/View/ModalViewController.swift @@ -9,16 +9,20 @@ import Combine import UIKit class ModalViewController: UIViewController { - let rightButtonPublisher = PassthroughSubject() - let leftButtonPublisher = PassthroughSubject() + + // MARK: - Properties + let onLeftButtonTapped: (()->Void)? + let onRightButtonTapped: ()->Void + var containerWidth: CGFloat = 0 var containerHeight: CGFloat = 0 var paddingBetweenLabels: CGFloat = 0 var titleText: String = "" - var subTitleText: String = "" + var subTitleText: String? var titleColor: UIColor = .black - var subTitleColor: UIColor = .black + var subTitleColor: UIColor? = .black + // MARK: - UI Components private let messageLabel = UILabel().then { $0.numberOfLines = 0 } @@ -56,7 +60,21 @@ class ModalViewController: UIViewController { private var contentViewInContainer: UIView? - init(width: CGFloat, height: CGFloat, paddingBetweenLabels: CGFloat, title: String, subTitle: String, titleColor: UIColor, subTitleColor: UIColor, rightButtonText: String = "로그인하기") { + // MARK: - Initializer + init( + onLeftButtonTapped: (()->Void)? = nil, + onRightButtonTapped: @escaping ()->Void, + width: CGFloat, + height: CGFloat, + paddingBetweenLabels: CGFloat, + title: String, + subTitle: String?, + titleColor: UIColor, + subTitleColor: UIColor?, + rightButtonText: String = "로그인하기" + ) { + self.onLeftButtonTapped = onLeftButtonTapped + self.onRightButtonTapped = onRightButtonTapped super.init(nibName: nil, bundle: nil) self.containerWidth = width self.containerHeight = height @@ -68,10 +86,35 @@ class ModalViewController: UIViewController { self.rightButton.setTitle(rightButtonText, for: .normal) } + convenience init( + onLeftButtonTapped: (()->Void)? = nil, + onRightButtonTapped: @escaping ()->Void, + width: CGFloat, + height: CGFloat, + title: String, + titleColor: UIColor, + rightButtonText: String = "로그인하기" + ) { + + self.init( + onLeftButtonTapped: onLeftButtonTapped, + onRightButtonTapped: onRightButtonTapped, + width: width, + height: height, + paddingBetweenLabels: 0, + title: title, + subTitle: nil, + titleColor: titleColor, + subTitleColor: nil, + rightButtonText: rightButtonText + ) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() configureView() @@ -83,12 +126,12 @@ class ModalViewController: UIViewController { } @objc func closeButtonTapped() { - leftButtonPublisher.send() + onLeftButtonTapped?() dismiss(animated: true, completion: nil) } @objc func rightButtonTapped() { - rightButtonPublisher.send() + onRightButtonTapped() dismiss(animated: true, completion: nil) } @@ -129,12 +172,15 @@ class ModalViewController: UIViewController { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 6 paragraphStyle.alignment = alignment - let attributedString = NSMutableAttributedString(string: subTitleText) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.foregroundColor, value: subTitleColor, range: NSRange(location: 0, length: subTitleText.count)) - subMessageLabel.attributedText = attributedString + if let subTitleText { + let attributedString = NSMutableAttributedString(string: subTitleText) + attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: subTitleText.count)) + attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: subTitleText.count)) + attributedString.addAttribute(.foregroundColor, value: subTitleColor ?? .black, range: NSRange(location: 0, length: subTitleText.count)) + + subMessageLabel.attributedText = attributedString + } } func setContentViewInContainer(view: UIView, frame: CGRect) { diff --git a/Koin/Core/View/ModalViewControllerB.swift b/Koin/Core/View/ModalViewControllerB.swift deleted file mode 100644 index be3142f2..00000000 --- a/Koin/Core/View/ModalViewControllerB.swift +++ /dev/null @@ -1,230 +0,0 @@ -// -// ModalViewControllerB.swift -// koin -// -// Created by 홍기정 on 1/19/26. -// - -import Combine -import UIKit - -class ModalViewControllerB: UIViewController { - - // MARK: - Properties - private let onLeftButtonTapped: (()->Void)? - private let onRightButtonTapped: ()->Void - - var containerWidth: CGFloat = 0 - var containerHeight: CGFloat = 0 - var paddingBetweenLabels: CGFloat = 0 - var titleText: String = "" - var subTitleText: String? - var titleColor: UIColor = .black - var subTitleColor: UIColor? = .black - - // MARK: - UI Components - private let messageLabel = UILabel().then { - $0.numberOfLines = 0 - } - - private let subMessageLabel = UILabel().then { - $0.numberOfLines = 0 - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("닫기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let rightButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 8 - view.layer.masksToBounds = true - return view - }() - - private var contentViewInContainer: UIView? - - init(onLeftButtonTapped: (()->Void)? = nil, onRightButtonTapped: @escaping ()->Void, width: CGFloat, height: CGFloat, paddingBetweenLabels: CGFloat, title: String, subTitle: String?, titleColor: UIColor, subTitleColor: UIColor?, rightButtonText: String = "로그인하기") { - self.onLeftButtonTapped = onLeftButtonTapped - self.onRightButtonTapped = onRightButtonTapped - super.init(nibName: nil, bundle: nil) - self.containerWidth = width - self.containerHeight = height - self.paddingBetweenLabels = paddingBetweenLabels - self.titleText = title - self.subTitleText = subTitle - self.titleColor = titleColor - self.subTitleColor = subTitleColor - self.rightButton.setTitle(rightButtonText, for: .normal) - } - - convenience init(onLeftButtonTapped: (()->Void)? = nil, onRightButtonTapped: @escaping ()->Void, width: CGFloat, height: CGFloat, title: String, titleColor: UIColor, rightButtonText: String = "로그인하기") { - - self.init(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: width, height: height, paddingBetweenLabels: 0, title: title, subTitle: nil, titleColor: titleColor, subTitleColor: nil, rightButtonText: rightButtonText) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - rightButton.addTarget(self, action: #selector(rightButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOutsideOfContainerView)) - view.addGestureRecognizer(tapGesture) - } - - @objc func closeButtonTapped() { - onLeftButtonTapped?() - dismiss(animated: true, completion: nil) - } - - @objc func rightButtonTapped() { - onRightButtonTapped() - dismiss(animated: true, completion: nil) - } - - @objc func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { - let location = sender.location(in: view) - if !containerView.frame.contains(location) { - dismiss(animated: true, completion: nil) - } - } - - func updateRightButton(buttonColor: UIColor = .appColor(.primary500), borderWidth: CGFloat, title: String) { - rightButton.backgroundColor = buttonColor - rightButton.layer.borderWidth = borderWidth - rightButton.setTitle(title, for: .normal) - } - - func updateCloseButton(buttonColor: UIColor = .systemBackground, borderWidth: CGFloat, title: String) { - closeButton.backgroundColor = buttonColor - closeButton.layer.borderWidth = borderWidth - closeButton.setTitle(title, for: .normal) - } - - func updateMessageLabel(font: UIFont = .appFont(.pretendardMedium, size: 18), alignment: NSTextAlignment = .center, title: String? = nil) { - if let title = title { titleText = title } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 8 - paragraphStyle.alignment = alignment - let attributedString = NSMutableAttributedString(string: titleText) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: titleText.count)) - attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: titleText.count)) - attributedString.addAttribute(.foregroundColor, value: titleColor, range: NSRange(location: 0, length: titleText.count)) - - messageLabel.attributedText = attributedString - } - - func updateSubMessageLabel(font: UIFont = .appFont(.pretendardRegular, size: 14), alignment: NSTextAlignment = .center, title: String? = nil) { - if let title = title { subTitleText = title } - - if let subTitleText { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - paragraphStyle.alignment = alignment - let attributedString = NSMutableAttributedString(string: subTitleText) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.foregroundColor, value: subTitleColor, range: NSRange(location: 0, length: subTitleText.count)) - - subMessageLabel.attributedText = attributedString - } - else { - updateMessageLabel(font: .appFont(.pretendardMedium, size: 16)) - } - } - - func setContentViewInContainer(view: UIView, frame: CGRect) { - self.contentViewInContainer = view - self.contentViewInContainer?.frame = frame - - guard let contentViewInContainer = contentViewInContainer else { return } - containerView.addSubview(contentViewInContainer) - contentViewInContainer.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.trailing.equalToSuperview() - make.height.equalTo(contentViewInContainer.frame.height) - } - closeButton.snp.remakeConstraints { make in - make.top.equalTo(contentViewInContainer.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - rightButton.snp.remakeConstraints { make in - make.top.equalTo(contentViewInContainer.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - } -} - -extension ModalViewControllerB { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, closeButton, rightButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(containerWidth) - make.height.equalTo(containerHeight) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.leading.trailing.equalToSuperview().inset(24) - } - subMessageLabel.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(paddingBetweenLabels) - make.leading.trailing.equalToSuperview().inset(24) - } - closeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - rightButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - updateMessageLabel() - updateSubMessageLabel() - } -} diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/Chat/Chat/ChatViewController.swift index 840abb31..797a7972 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/Chat/Chat/ChatViewController.swift @@ -116,11 +116,10 @@ final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerVi let onRightButtonTapped: ()->Void = { [weak self] in self?.inputSubject.send(.blockUser) } - let modalViewController = ModalViewControllerB(onRightButtonTapped: onRightButtonTapped, width: 301, height: 179, paddingBetweenLabels: 8, title: "이 사용자를 차단하시겠습니까?", subTitle: "쪽지 수신 및 발신이 모두 차단됩니다.", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray), rightButtonText: "차단하기") + let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 179, paddingBetweenLabels: 8, title: "이 사용자를 차단하시겠습니까?", subTitle: "쪽지 수신 및 발신이 모두 차단됩니다.", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray), rightButtonText: "차단하기") modalViewController.modalTransitionStyle = .crossDissolve - modalViewController.modalPresentationStyle = .overFullScreen dismiss(animated: true) { [weak self] in - self?.present(modalViewController, animated: true) + self?.present(modalViewController, animated: false) } }.store(in: &subscriptions) diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift index 3b7c5adc..5fb8cc88 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift @@ -246,10 +246,8 @@ extension LostItemDataViewController { self?.inputSubject.send(.deleteData) self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.findUserDeleteConfirm, EventParameter.EventCategory.click, "확인")) } - let modalViewController = ModalViewControllerB(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") - modalViewController.modalPresentationStyle = .overFullScreen - modalViewController.modalTransitionStyle = .crossDissolve - navigationController?.present(modalViewController, animated: true) + let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + navigationController?.present(modalViewController, animated: false) } private func navigateToEdit() { @@ -271,10 +269,9 @@ extension LostItemDataViewController { inputSubject.send(.changeState(viewModel.id)) inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.lostItemFound, .click, "\(type.description)물")) } - let modalViewController = ModalViewControllerB(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") modalViewController.modalTransitionStyle = .crossDissolve - modalViewController.modalPresentationStyle = .overFullScreen - navigationController?.present(modalViewController, animated: true) + navigationController?.present(modalViewController, animated: false) } private func navigateToChat(_ createChatRoomResponse: CreateChatRoomResponse) { @@ -313,10 +310,9 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = ModalViewControllerB(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "쪽지를 보내려면\n로그인이 필요해요.", subTitle: "로그인 후 대화를 시작하세요!", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + let modalViewController = ModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "쪽지를 보내려면\n로그인이 필요해요.", subTitle: "로그인 후 대화를 시작하세요!", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) modalViewController.modalTransitionStyle = .crossDissolve - modalViewController.modalPresentationStyle = .overFullScreen - navigationController?.present(modalViewController, animated: true) + navigationController?.present(modalViewController, animated: false) } private func showLoginToReportModal() { @@ -340,10 +336,9 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = ModalViewControllerB(onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 신고하려면\n로그인이 필요해요.", subTitle: "로그인 후 이용해주세요.", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 신고하려면\n로그인이 필요해요.", subTitle: "로그인 후 이용해주세요.", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) modalViewController.modalTransitionStyle = .crossDissolve - modalViewController.modalPresentationStyle = .overFullScreen - navigationController?.present(modalViewController, animated: true) + navigationController?.present(modalViewController, animated: false) } private func navigateToReport() { diff --git a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift index b36d2e58..e52d9ea4 100644 --- a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift +++ b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift @@ -195,7 +195,7 @@ extension LostItemKeywordViewController { let onRightButtonTapped: ()->Void = { [weak self] in self?.navigateToLogin() } - let viewController = ModalViewControllerB( + let viewController = ModalViewController( onRightButtonTapped: onRightButtonTapped, width: 301, height: 228, @@ -206,8 +206,7 @@ extension LostItemKeywordViewController { subTitleColor: .appColor(.gray) ) viewController.modalTransitionStyle = .crossDissolve - viewController.modalPresentationStyle = .overFullScreen - navigationController?.present(viewController, animated: true) + navigationController?.present(viewController, animated: false) } private func updateMyKeywordCountLabel(_ count: Int) { diff --git a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift index 6ce92330..3b6e120a 100644 --- a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift +++ b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift @@ -227,11 +227,8 @@ extension LostItemListViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let loginModalViewController = ModalViewControllerB(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 작성하려면\n로그인이 필요해요.", subTitle: "로그인 후 글을 작성해주세요!", titleColor: UIColor.appColor(.neutral700), subTitleColor: UIColor.appColor(.gray)).then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - navigationController?.present(loginModalViewController, animated: true) + let loginModalViewController = ModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 작성하려면\n로그인이 필요해요.", subTitle: "로그인 후 글을 작성해주세요!", titleColor: UIColor.appColor(.neutral700), subTitleColor: UIColor.appColor(.gray)) + navigationController?.present(loginModalViewController, animated: false) } private func presentPostTypeModal() { diff --git a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift index 94cb5d61..2e1d744b 100644 --- a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift +++ b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift @@ -89,7 +89,6 @@ final class ManageNoticeKeywordViewController: UIViewController { } private let keywordLoginModalViewController = ModalViewController(width: 301, height: 230, paddingBetweenLabels: 8, title: "키워드 알림을 받으려면\n로그인이 필요해요.", subTitle: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray)).then { - $0.modalPresentationStyle = .overFullScreen $0.modalTransitionStyle = .crossDissolve } @@ -139,7 +138,7 @@ final class ManageNoticeKeywordViewController: UIViewController { case .showLoginModal: self.keywordNotificationSwtich.isOn = false self.keywordNotificationSwtich.isEnabled = true - self.present(self.keywordLoginModalViewController.self, animated: true, completion: nil) + self.present(self.keywordLoginModalViewController.self, animated: false, completion: nil) case let .updateSwitch(isOn): self.keywordNotificationSwtich.isOn = isOn self.keywordNotificationSwtich.isEnabled = true @@ -347,4 +346,3 @@ extension ManageNoticeKeywordViewController { self.view.backgroundColor = .systemBackground } } - diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 6adea6f6..a0a0f5bc 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -72,7 +72,6 @@ 7C372F292F1D6A7F00149729 /* LostItemDataViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372F282F1D6A7F00149729 /* LostItemDataViewModel.swift */; }; 7C372F322F1DB4C300149729 /* LostItemImagesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372F302F1DB4C300149729 /* LostItemImagesCollectionViewCell.swift */; }; 7C372F332F1DB4C300149729 /* LostItemImagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372F2F2F1DB4C300149729 /* LostItemImagesCollectionView.swift */; }; - 7C372F392F1DCFF800149729 /* ModalViewControllerB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372F382F1DCFF800149729 /* ModalViewControllerB.swift */; }; 7C3ECC7A2F41505000EE8F13 /* ErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3ECC792F41505000EE8F13 /* ErrorResponse.swift */; }; 7C457AE72FCDE7240011D338 /* SwiftUIViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C457AE62FCDE7240011D338 /* SwiftUIViewModelProtocol.swift */; }; 7C457B2B2FCE1BA80011D338 /* DiningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C457B092FCE1BA80011D338 /* DiningView.swift */; }; @@ -1098,7 +1097,6 @@ 7C372F282F1D6A7F00149729 /* LostItemDataViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemDataViewModel.swift; sourceTree = ""; }; 7C372F2F2F1DB4C300149729 /* LostItemImagesCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemImagesCollectionView.swift; sourceTree = ""; }; 7C372F302F1DB4C300149729 /* LostItemImagesCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemImagesCollectionViewCell.swift; sourceTree = ""; }; - 7C372F382F1DCFF800149729 /* ModalViewControllerB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewControllerB.swift; sourceTree = ""; }; 7C3ECC792F41505000EE8F13 /* ErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorResponse.swift; sourceTree = ""; }; 7C457AE62FCDE7240011D338 /* SwiftUIViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewModelProtocol.swift; sourceTree = ""; }; 7C457AFE2FCE1BA80011D338 /* CategoryFeaturedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFeaturedButton.swift; sourceTree = ""; }; @@ -5125,7 +5123,6 @@ 7CC42B062F56A7E900940CE1 /* ZoomedImageViewController */, 7CA0D6172EC744E700E9A282 /* ZoomedImageViewControllerB */, 835C991D2C7F1256002E02D3 /* ModalViewController.swift */, - 7C372F382F1DCFF800149729 /* ModalViewControllerB.swift */, D2FB23442BFB6D090098BA2B /* CustomNavigationController.swift */, D20AB96E2C574291006BC684 /* BottomSheetViewController.swift */, 7CC42B822F5D307C00940CE1 /* BottomSheetViewControllerB.swift */, @@ -6373,7 +6370,6 @@ 7CED92132EF2D1F900457128 /* SettingsViewController.swift in Sources */, 7CED92142EF2D1F900457128 /* LandCollectionViewCell.swift in Sources */, 7C0C18952F68A913001781B3 /* FetchCallVanChatUseCase.swift in Sources */, - 7C372F392F1DCFF800149729 /* ModalViewControllerB.swift in Sources */, 7CED92172EF2D1F900457128 /* DiningOperatingTimeFooterView.swift in Sources */, 7C372EE62F1C0CC400149729 /* LostItemListData.swift in Sources */, 7CED92192EF2D1F900457128 /* ReportLostItemViewModel.swift in Sources */, From 444fb586d4bce86360db83224e4e6c062ee80795 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 17:44:18 +0900 Subject: [PATCH 016/101] =?UTF-8?q?design:=20ModalViewController=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/View/ModalViewController.swift | 32 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Koin/Core/View/ModalViewController.swift b/Koin/Core/View/ModalViewController.swift index 459806fb..ae01752c 100644 --- a/Koin/Core/View/ModalViewController.swift +++ b/Koin/Core/View/ModalViewController.swift @@ -125,20 +125,45 @@ class ModalViewController: UIViewController { view.addGestureRecognizer(tapGesture) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + containerView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + containerView.alpha = 0 + view.backgroundColor = UIColor.clear + + UIView.animate(springDuration: 0.2) { + containerView.transform = .identity + containerView.alpha = 1 + view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) + } + } + + // MARK: - Public + func dismissWithAnimation() { + UIView.animate(springDuration: 0.2) { + containerView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + containerView.alpha = 0 + view.backgroundColor = UIColor.clear + } completion: { [weak self] _ in + self?.dismiss(animated: false) + } + } + @objc func closeButtonTapped() { onLeftButtonTapped?() - dismiss(animated: true, completion: nil) + dismissWithAnimation() } @objc func rightButtonTapped() { onRightButtonTapped() - dismiss(animated: true, completion: nil) + dismissWithAnimation() } @objc func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { let location = sender.location(in: view) if !containerView.frame.contains(location) { - dismiss(animated: true, completion: nil) + dismissWithAnimation() } } @@ -252,7 +277,6 @@ extension ModalViewController { private func configureView() { setUpLayOuts() setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) updateMessageLabel() updateSubMessageLabel() } From 8ec7ba74d00111b74843d7f4ca09733f8910675d Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 18:33:44 +0900 Subject: [PATCH 017/101] =?UTF-8?q?refactor:=20BusSearchResultViewControll?= =?UTF-8?q?er=EA=B0=80=20BusSearchDatePickerViewController=EB=A5=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=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 --- .../BusSearchResultViewController.swift | 58 +++++----- .../BusSearchResultViewModel.swift | 13 ++- .../BusSearchDatePickerViewController.swift | 102 +++++++++++++----- 3 files changed, 118 insertions(+), 55 deletions(-) diff --git a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift index 15b35c77..fee5a923 100644 --- a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift @@ -14,13 +14,12 @@ final class BusSearchResultViewController: UIViewController, UIGestureRecognizer private let viewModel: BusSearchResultViewModel private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] + private var datePickerSubTitle = "" // MARK: - UI Components private let tableView = BusSearchResultTableView(frame: .zero, style: .plain) - private var busSearchDatePickerViewController = BusSearchDatePickerViewController(width: 301, height: 347, paddingBetweenLabels: 10, title: "출발 시각 설정", subTitle: "현재는 정규학기(12월 20일까지)의\n시간표를 제공하고 있어요.", titleColor: .appColor(.neutral700), subTitleColor: .gray) - // MARK: - Initialization init(viewModel: BusSearchResultViewModel) { @@ -69,43 +68,24 @@ final class BusSearchResultViewController: UIViewController, UIGestureRecognizer outputSubject.receive(on: DispatchQueue.main).sink { [weak self] output in switch output { - case let .updateDatePickerData((dates, selectedDate)): - self?.busSearchDatePickerViewController.setPickerItems(items: dates, selectedItems: selectedDate) case let .udpatesSearchedResult(departTime, busSearchedResult): self?.updateSearchedResult(departTime: departTime, departInfo: busSearchedResult) case let .updateSemesterInfo(semesterInfo): self?.updateSemesterInfo(semesterInfo: semesterInfo) } }.store(in: &subscriptions) - - busSearchDatePickerViewController.leftButtonPublisher.sink { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureNow, .click, "지금 출발")) - }.store(in: &subscriptions) - - busSearchDatePickerViewController.changePickerDate.sink { [weak self] isChanged in - let logValue = isChanged != nil ? "Y" : "N" - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureTimeSettingDone, .click, logValue)) - }.store(in: &subscriptions) - + tableView.tapDepartTimeButtonPublisher .sink { [weak self] in guard let self = self else { return } self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.searchResultDepartureTime, .click, "출발 시간 설정")) - busSearchDatePickerViewController.modalPresentationStyle = .overFullScreen - present(busSearchDatePickerViewController, animated: true) + self.presentBusSearchDatePickerViewController() }.store(in: &subscriptions) tableView.tapDepartBusTypeButtonPublisher.sink { [weak self] busType in self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.searchResultBusType, .click, busType.koreanDescription)) self?.inputSubject.send(.getSearchedResult(nil, busType)) }.store(in: &subscriptions) - - busSearchDatePickerViewController.pickerSelectedItemsPublisher.sink { [weak self] selectedItem in - if selectedItem.count > 3 { - let time = "\(selectedItem[0]) \(selectedItem[1]) \(selectedItem[2]):\(selectedItem[3])" - self?.inputSubject.send(.getSearchedResult(time, nil)) - } - }.store(in: &subscriptions) } } @@ -143,7 +123,37 @@ extension BusSearchResultViewController { } private func updateSemesterInfo(semesterInfo: SemesterInfo) { - busSearchDatePickerViewController.updateSubMessageLabel(title: "\(semesterInfo.name)(\(semesterInfo.from) ~ \(semesterInfo.to))의\n시간표가 제공됩니다.") + datePickerSubTitle = "현재는 \(semesterInfo.name)(\(semesterInfo.to)까지)의 시간표를 제공하고 있어요." + } + + private func presentBusSearchDatePickerViewController() { + guard let datePickerData = viewModel.datePickerData else { return } + + let busSearchDatePickerViewController = BusSearchDatePickerViewController( + onDepartureNowTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureNow, .click, "지금 출발")) + }, + onPickerDateChanged: { [weak self] isChanged in + let logValue = isChanged != nil ? "Y" : "N" + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureTimeSettingDone, .click, logValue)) + }, + onPickerItemsSelected: { [weak self] selectedItems in + guard selectedItems.count > 3 else { return } + self?.inputSubject.send(.updateDatePickerSelectedItems(selectedItems)) + let time = "\(selectedItems[0]) \(selectedItems[1]) \(selectedItems[2]):\(selectedItems[3])" + self?.inputSubject.send(.getSearchedResult(time, nil)) + }, + width: 301, + height: 347, + paddingBetweenLabels: 10, + title: "출발 시각 설정", + subTitle: datePickerSubTitle, + titleColor: .appColor(.neutral700), + subTitleColor: .gray + ) + busSearchDatePickerViewController.setPickerItems(items: datePickerData.0, selectedItems: datePickerData.1) + busSearchDatePickerViewController.modalPresentationStyle = .overFullScreen + present(busSearchDatePickerViewController, animated: false) } } diff --git a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewModel.swift b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewModel.swift index 271bc17b..a849b35b 100644 --- a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewModel.swift +++ b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewModel.swift @@ -10,12 +10,12 @@ import Combine final class BusSearchResultViewModel: ViewModelProtocol { enum Input { case getDatePickerData + case updateDatePickerSelectedItems([String]) case getSearchedResult(String?, BusType?) case getSemesterInfo case logEvent(EventLabelType, EventParameter.EventCategory, Any) } enum Output { - case updateDatePickerData(([[String]], [String])) case updateSemesterInfo(SemesterInfo) case udpatesSearchedResult(String?, SearchBusInfoResult) } @@ -25,6 +25,7 @@ final class BusSearchResultViewModel: ViewModelProtocol { let busPlaces: (BusPlace, BusPlace) // navigationItem을 설정하기 위해 private 임시 없앰 private var departBusType: BusType = .noValue private var departBusTime: String = "" + private(set) var datePickerData: ([[String]], [String])? private let fetchDatePickerDataUseCase: FetchKoinPickerDataUseCase private let fetchSearchedResultUseCase: SearchBusInfoUseCase private let fetchSemesterInfoUseCase: FetchShuttleBusRoutesUseCase @@ -43,6 +44,8 @@ final class BusSearchResultViewModel: ViewModelProtocol { switch input { case .getDatePickerData: self?.getDatePickerData() + case let .updateDatePickerSelectedItems(selectedItems): + self?.updateDatePickerSelectedItems(selectedItems) case let .getSearchedResult(departDate, busType): self?.getSearchedResult(departDate: departDate, busType: busType) case let .logEvent(label, category, value): @@ -57,8 +60,12 @@ final class BusSearchResultViewModel: ViewModelProtocol { extension BusSearchResultViewModel { private func getDatePickerData() { - let data = fetchDatePickerDataUseCase.execute() - outputSubject.send(.updateDatePickerData(data)) + datePickerData = fetchDatePickerDataUseCase.execute() + } + + private func updateDatePickerSelectedItems(_ selectedItems: [String]) { + guard let datePickerData else { return } + self.datePickerData = (datePickerData.0, selectedItems) } private func getSearchedResult(departDate: String?, busType: BusType?) { diff --git a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift index 8f90c5c0..e42d3f56 100644 --- a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift @@ -5,48 +5,94 @@ // Created by JOOMINKYUNG on 11/17/24. // -import Combine import UIKit final class BusSearchDatePickerViewController: ModalViewController { - + + // MARK: - Properties + private let onDepartureNowTapped: () -> Void + private let onPickerDateChanged: (Bool?) -> Void + private let onPickerItemsSelected: ([String]) -> Void + + // MARK: - UI Components private let pickerView = KoinPickerView() - private var subscriptions: Set = [] - let pickerSelectedItemsPublisher = CurrentValueSubject<[String], Never>([]) - let changePickerDate = PassthroughSubject() - + + // MARK: - Initializer + init( + onDepartureNowTapped: @escaping () -> Void, + onPickerDateChanged: @escaping (Bool?) -> Void, + onPickerItemsSelected: @escaping ([String]) -> Void, + width: CGFloat, + height: CGFloat, + paddingBetweenLabels: CGFloat, + title: String, + subTitle: String, + titleColor: UIColor, + subTitleColor: UIColor + ) { + self.onDepartureNowTapped = onDepartureNowTapped + self.onPickerDateChanged = onPickerDateChanged + self.onPickerItemsSelected = onPickerItemsSelected + super.init( + onLeftButtonTapped: nil, + onRightButtonTapped: {}, + width: width, + height: height, + paddingBetweenLabels: paddingBetweenLabels, + title: title, + subTitle: subTitle, + titleColor: titleColor, + subTitleColor: subTitleColor + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() setContentViewInContainer(view: pickerView, frame: .init(x: 0, y: 0, width: 301, height: 122)) - - rightButtonPublisher.sink { [weak self] in - self?.pickerSelectedItemsPublisher.send(self?.pickerView.selectedItemPublisher.value ?? []) - self?.changePickerDate.send(self?.pickerView.changeSelectedItemPublisher.value) - self?.pickerView.changeSelectedItemPublisher.send(nil) - }.store(in: &subscriptions) configureView() - - leftButtonPublisher.sink { [weak self] in - let currentDate = Date() - let calendar = Calendar.current - let hour = calendar.component(.hour, from: currentDate) - let minute = calendar.component(.minute, from: currentDate) - let amPm = hour < 12 ? "오전" : "오후" - let adjustedHour = hour % 12 - let displayHour = adjustedHour == 0 ? 12 : adjustedHour - - let pickerSelectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] - self?.pickerView.setSelectedData(selectedItem: pickerSelectedItems) - self?.pickerSelectedItemsPublisher.send(pickerSelectedItems) - self?.pickerView.changeSelectedItemPublisher.send(nil) - }.store(in: &subscriptions) } + override func rightButtonTapped() { + let selectedItems = pickerView.selectedItemPublisher.value + onPickerItemsSelected(selectedItems) + onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) + pickerView.changeSelectedItemPublisher.send(nil) + dismissWithAnimation() + } + + override func closeButtonTapped() { + onDepartureNowTapped() + + let currentDate = Date() + let calendar = Calendar.current + let hour = calendar.component(.hour, from: currentDate) + let minute = calendar.component(.minute, from: currentDate) + let amPm = hour < 12 ? "오전" : "오후" + let adjustedHour = hour % 12 + let displayHour = adjustedHour == 0 ? 12 : adjustedHour + + let selectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] + pickerView.setSelectedData(selectedItem: selectedItems) + onPickerItemsSelected(selectedItems) + pickerView.changeSelectedItemPublisher.send(nil) + dismissWithAnimation() + } + + // MARK: - Public func setPickerItems(items: [[String]], selectedItems: [String]) { pickerView.changeSelectedItemPublisher.send(nil) pickerView.setPickerData(items: items, selectedItem: selectedItems) } - +} + +extension BusSearchDatePickerViewController { + // MARK: - Configure private func configureView() { updateMessageLabel(alignment: .left) updateSubMessageLabel(alignment: .left) From 017e1244b156b18212ba241c36de45e77f363835 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 18:56:57 +0900 Subject: [PATCH 018/101] =?UTF-8?q?refactor:=20ManageNoticeKeywordViewCont?= =?UTF-8?q?roller=EA=B0=80=20ModalViewController=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../ManageNoticeKeywordViewController.swift | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift index 2e1d744b..2361028e 100644 --- a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift +++ b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift @@ -88,10 +88,6 @@ final class ManageNoticeKeywordViewController: UIViewController { $0.text = "추천 키워드" } - private let keywordLoginModalViewController = ModalViewController(width: 301, height: 230, paddingBetweenLabels: 8, title: "키워드 알림을 받으려면\n로그인이 필요해요.", subTitle: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray)).then { - $0.modalTransitionStyle = .crossDissolve - } - private let myKeywordCollectionView = MyKeywordCollectionView(frame: .zero, collectionViewLayout: LeftAlignedCollectionViewFlowLayout()) private let recommendedKeywordCollectionView = RecommendedKeywordCollectionView(frame: .zero, collectionViewLayout: LeftAlignedCollectionViewFlowLayout()) @@ -138,7 +134,7 @@ final class ManageNoticeKeywordViewController: UIViewController { case .showLoginModal: self.keywordNotificationSwtich.isOn = false self.keywordNotificationSwtich.isEnabled = true - self.present(self.keywordLoginModalViewController.self, animated: false, completion: nil) + self.presentLoginModal() case let .updateSwitch(isOn): self.keywordNotificationSwtich.isOn = isOn self.keywordNotificationSwtich.isEnabled = true @@ -173,31 +169,6 @@ final class ManageNoticeKeywordViewController: UIViewController { self?.inputSubject.send(.addKeyword(keyword: keyword, isRecommended: true)) }.store(in: &subscriptions) - keywordLoginModalViewController.rightButtonPublisher.sink { [weak self] in - let userRepository = DefaultUserRepository(service: DefaultUserService()) - let analyticsRepository = GA4AnalyticsRepository(service: GA4AnalyticsService()) - let notiRepository = DefaultNotiRepository(service: DefaultNotiService()) - let loginUseCase = DefaultLoginUseCase(userRepository: userRepository) - let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: analyticsRepository) - let fetchUserDataUseCase = DefaultFetchUserDataUseCase(userRepository: userRepository) - let sendDeviceTokenIfNeededUseCase = DefaultSendDeviceTokenIfNeededUseCase( - userRepository: userRepository, - notiRepository: notiRepository - ) - let viewModel = LoginViewModel( - loginUseCase: loginUseCase, - logAnalyticsEventUseCase: logAnalyticsEventUseCase, - fetchUserDataUseCase: fetchUserDataUseCase, - sendDeviceTokenIfNeededUseCase: sendDeviceTokenIfNeededUseCase - ) - let loginViewController = LoginViewController(viewModel: viewModel) - self?.navigationController?.pushViewController(loginViewController, animated: true) - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPrompt, .click, "키워드 알림 팝업")) - }.store(in: &subscriptions) - - keywordLoginModalViewController.leftButtonPublisher.sink { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPopupKeyword, .click, "닫기")) - }.store(in: &subscriptions) } } @@ -240,6 +211,27 @@ extension ManageNoticeKeywordViewController { private func conductAddKeywordIllegalType(illegalType: String) { showToast(message: illegalType, success: false) } + + private func presentLoginModal() { + let modalViewController = ModalViewController( + onLeftButtonTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPopupKeyword, .click, "닫기")) + }, + onRightButtonTapped: { [weak self] in + self?.navigateToLogin() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPrompt, .click, "키워드 알림 팝업")) + }, + width: 301, + height: 230, + paddingBetweenLabels: 8, + title: "키워드 알림을 받으려면\n로그인이 필요해요.", + subTitle: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!", + titleColor: .appColor(.neutral700), + subTitleColor: .appColor(.gray) + ) + + present(modalViewController, animated: false) + } override func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let text = textField.text { From 0e803b464cd27c24327e410d9e0568dd31c37368 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 18:43:55 +0900 Subject: [PATCH 019/101] =?UTF-8?q?delete:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=8B=9D=EB=8B=A8=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EB=AA=A8=EB=8B=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dining/Dining/DiningViewController.swift | 11 ----------- Koin/Presentation/Dining/Dining/DiningViewModel.swift | 1 - 2 files changed, 12 deletions(-) diff --git a/Koin/Presentation/Dining/Dining/DiningViewController.swift b/Koin/Presentation/Dining/Dining/DiningViewController.swift index 21d81b16..c856e292 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewController.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewController.swift @@ -90,11 +90,6 @@ final class DiningViewController: UIViewController { private let diningNotiContentViewController = DiningNotiContentViewController() - private let diningLikeLoginModalViewController = ModalViewController(width: 301, height: 230, paddingBetweenLabels: 8, title: "더 맛있는 학식을 먹는 방법,\n로그인하고 좋아요를 남겨주세요!", subTitle: "여러분의 좋아요가 영양사님이 더 나은,\n식단을 제공할 수 있도록 도와줍니다.", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray)).then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - // MARK: - Initialization init(viewModel: DiningViewModel) { @@ -168,8 +163,6 @@ final class DiningViewController: UIViewController { case let .showBottomSheet((soldOutIsOn, imageUplloadisOn)): self?.showBottomSheet((soldOutIsOn, imageUplloadisOn)) UserDefaults.standard.set(true, forKey: "hasShownBottomSheet") - case .showLoginModal: - self?.present(strongSelf.diningLikeLoginModalViewController, animated: true, completion: nil) } }.store(in: &subscriptions) @@ -213,10 +206,6 @@ final class DiningViewController: UIViewController { self?.diningNotiContentViewController.dissmissView() }.store(in: &subscriptions) - diningLikeLoginModalViewController.rightButtonPublisher.sink { [weak self] in - self?.navigateToLogin() - }.store(in: &subscriptions) - NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification).sink { [weak self] _ in self?.diningListCollectionView.startToolTipImageViewAnimation() } diff --git a/Koin/Presentation/Dining/Dining/DiningViewModel.swift b/Koin/Presentation/Dining/Dining/DiningViewModel.swift index b574f5b3..5564d33a 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewModel.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewModel.swift @@ -24,7 +24,6 @@ final class DiningViewModel: ViewModelProtocol { case updateDiningList([DiningItem], DiningType) case initCalendar(Date) case showBottomSheet((Bool, Bool)) - case showLoginModal } private let outputSubject = PassthroughSubject() From f60bec7af946919fde28a9dc10ebc4a2cf6d648a Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:15:53 +0900 Subject: [PATCH 020/101] =?UTF-8?q?refactor:=20BusSearchViewController?= =?UTF-8?q?=EA=B0=80=20BusAreaSelectedViewController=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../BusSearch/BusSearchViewController.swift | 81 +++++++++---------- .../BusAreaSelectdViewController.swift | 56 +++++-------- 2 files changed, 58 insertions(+), 79 deletions(-) diff --git a/Koin/Presentation/Bus/BusSearch/BusSearchViewController.swift b/Koin/Presentation/Bus/BusSearch/BusSearchViewController.swift index f618ad3b..d68d4590 100644 --- a/Koin/Presentation/Bus/BusSearch/BusSearchViewController.swift +++ b/Koin/Presentation/Bus/BusSearch/BusSearchViewController.swift @@ -66,8 +66,6 @@ final class BusSearchViewController: UIViewController { $0.isEnabled = false } - private let busAreaViewController = BusAreaSelectedViewController() - // MARK: - Initialization init(viewModel: BusSearchViewModel) { @@ -92,13 +90,6 @@ final class BusSearchViewController: UIViewController { busNoticeWrappedView.addGestureRecognizer(tapGesture) bind() inputSubject.send(.fetchBusNotice) - - NotificationCenter.default.addObserver( - self, - selector: #selector(self.didDismissDetailNotification(_:)), - name: NSNotification.Name("DismissBusAreaSelectedView"), - object: nil - ) } override func viewWillAppear(_ animated: Bool) { @@ -106,12 +97,6 @@ final class BusSearchViewController: UIViewController { configureNavigationBar(style: .empty) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self, name: NSNotification.Name("DismissBusAreaSelectedView"), object: nil) - } - - // MARK: - Bind private func bind() { @@ -124,35 +109,10 @@ final class BusSearchViewController: UIViewController { self?.updateEmergencyNotice(notice: notice) } }.store(in: &subscriptions) - - busAreaViewController.departureBusAreaPublisher.sink { [weak self] departureArea in - guard let self = self else { return } - changeBusAreaButton(sender: departAreaSelectedButton, title: departureArea) - if departAreaSelectedButton.tag != 0 && arrivedAreaSelectedButton.tag != 0 { - manageSearchButton(isActivated: true) - } - self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureLocationConfirm, .click, departureArea.koreanDescription)) - }.store(in: &subscriptions) - - busAreaViewController.arrivalBusAreaPublisher.sink { [weak self] arrivedArea in - guard let self = self else { return } - changeBusAreaButton(sender: arrivedAreaSelectedButton, title: arrivedArea) - if departAreaSelectedButton.tag != 0 && arrivedAreaSelectedButton.tag != 0 { - manageSearchButton(isActivated: true) - } - self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.arrivalLocationConfirm, .click, arrivedArea.koreanDescription)) - }.store(in: &subscriptions) } } extension BusSearchViewController { - @objc func didDismissDetailNotification(_ notification: Notification) { - let departure = departAreaSelectedButton.tag != 0 ? BusPlace.allCases[departAreaSelectedButton.tag - 1] : nil - let arrival = arrivedAreaSelectedButton.tag != 0 ? BusPlace.allCases[arrivedAreaSelectedButton.tag - 1] : nil - - busAreaViewController.dismissWithoutConfirmPublisher.send(((departure, arrival), notification.object)) - } - @objc private func tapSearchButton(sender: UIButton) { inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.searchBus, .click, "조회하기")) let repository = DefaultBusRepository(service: DefaultBusService()) @@ -197,7 +157,6 @@ extension BusSearchViewController { let arrival = BusPlace.allCases[arrivedAreaSelectedButton.tag - 1] changeBusAreaButton(sender: departAreaSelectedButton, title: arrival) changeBusAreaButton(sender: arrivedAreaSelectedButton, title: departure) - busAreaViewController.swap(departure: departure, arrival: arrival) } private func updateSelectedBusArea(buttonState: BusAreaButtonState, busPlace: BusPlace?) { @@ -211,12 +170,50 @@ extension BusSearchViewController { } } - busAreaViewController.configure(busAreaLists: busAreaList, buttonState: buttonState) + let departure = selectedBusPlace(from: departAreaSelectedButton) + let arrival = selectedBusPlace(from: arrivedAreaSelectedButton) + let busAreaViewController = BusAreaSelectedViewController( + onDepartureBusAreaSelected: { [weak self] departureArea in + self?.selectDepartureBusArea(departureArea) + }, + onArrivalBusAreaSelected: { [weak self] arrivalArea in + self?.selectArrivalBusArea(arrivalArea) + } + ) + busAreaViewController.configure( + busAreaLists: busAreaList, + buttonState: buttonState, + departure: departure, + arrival: arrival + ) let bottomSheet = BottomSheetViewController(contentViewController: busAreaViewController, defaultHeight: 312.5 + UIApplication.bottomSafeAreaHeight(), cornerRadius: 32, isPannedable: false) bottomSheet.modalPresentationStyle = .overFullScreen bottomSheet.modalTransitionStyle = .crossDissolve present(bottomSheet, animated: true) } + + private func selectedBusPlace(from button: UIButton) -> BusPlace? { + guard button.tag != 0 else { return nil } + return BusPlace.allCases[button.tag - 1] + } + + private func selectDepartureBusArea(_ departureArea: BusPlace) { + changeBusAreaButton(sender: departAreaSelectedButton, title: departureArea) + updateSearchButtonIfNeeded() + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureLocationConfirm, .click, departureArea.koreanDescription)) + } + + private func selectArrivalBusArea(_ arrivalArea: BusPlace) { + changeBusAreaButton(sender: arrivedAreaSelectedButton, title: arrivalArea) + updateSearchButtonIfNeeded() + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.arrivalLocationConfirm, .click, arrivalArea.koreanDescription)) + } + + private func updateSearchButtonIfNeeded() { + if departAreaSelectedButton.tag != 0 && arrivedAreaSelectedButton.tag != 0 { + manageSearchButton(isActivated: true) + } + } private func changeBusAreaButton(sender: UIButton, title: BusPlace) { var configuration = UIButton.Configuration.plain() diff --git a/Koin/Presentation/Bus/BusSearch/SubViews/BusAreaSelected/BusAreaSelectdViewController.swift b/Koin/Presentation/Bus/BusSearch/SubViews/BusAreaSelected/BusAreaSelectdViewController.swift index 24b94f45..5fa00c74 100644 --- a/Koin/Presentation/Bus/BusSearch/SubViews/BusAreaSelected/BusAreaSelectdViewController.swift +++ b/Koin/Presentation/Bus/BusSearch/SubViews/BusAreaSelected/BusAreaSelectdViewController.swift @@ -11,12 +11,10 @@ import UIKit final class BusAreaSelectedViewController: UIViewController { //MARK: - Properties - let departureBusAreaPublisher = PassthroughSubject() - let arrivalBusAreaPublisher = PassthroughSubject() - let dismissWithoutConfirmPublisher = PassthroughSubject<((BusPlace?, BusPlace?), Any?), Never>() + private let onDepartureBusAreaSelected: (BusPlace) -> Void + private let onArrivalBusAreaSelected: (BusPlace) -> Void private var buttonState: BusAreaButtonState = .departureSelect private var busRouteType: BusAreaButtonType = .departure - private var subscriptions = Set() //MARK: - UI Components private let busRouteDescriptionlabel = UILabel().then { @@ -41,7 +39,12 @@ final class BusAreaSelectedViewController: UIViewController { } //MARK: - Initialization - init() { + init( + onDepartureBusAreaSelected: @escaping (BusPlace) -> Void, + onArrivalBusAreaSelected: @escaping (BusPlace) -> Void + ) { + self.onDepartureBusAreaSelected = onDepartureBusAreaSelected + self.onArrivalBusAreaSelected = onArrivalBusAreaSelected super.init(nibName: nil, bundle: nil) } @@ -55,34 +58,19 @@ final class BusAreaSelectedViewController: UIViewController { super.viewDidLoad() configureView() confirmButton.addTarget(self, action: #selector(tapConfirmButton), for: .touchUpInside) - - dismissWithoutConfirmPublisher.sink { [weak self] busPlace, currentBusPlace in - let departure = busPlace.0 - let arrival = busPlace.1 - if departure != self?.busAreaCollectionView.departureBusAreaPublisher.value { - self?.busAreaCollectionView.departureBusAreaPublisher.send(departure) - } - - if arrival != self?.busAreaCollectionView.arrivalBusAreaPublisher.value { - self?.busAreaCollectionView.arrivalBusAreaPublisher.send(arrival) - } - - if (departure != nil && self?.busRouteType == .arrival) || (departure == nil && self?.busRouteType == .departure) { - self?.busRouteType = departure != nil ? .departure : .arrival - self?.buttonState = departure != nil ? .departureSelect : .arrivalSelect - } - - }.store(in: &subscriptions) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.post(name: NSNotification.Name("DismissBusAreaSelectedView"), object: busRouteType, userInfo: nil) } } extension BusAreaSelectedViewController { - func configure(busAreaLists: [(BusPlace, Bool)], buttonState: BusAreaButtonState) { + func configure( + busAreaLists: [(BusPlace, Bool)], + buttonState: BusAreaButtonState, + departure: BusPlace?, + arrival: BusPlace? + ) { + busAreaCollectionView.departureBusAreaPublisher.send(departure) + busAreaCollectionView.arrivalBusAreaPublisher.send(arrival) + if buttonState == .departureSelect { busRouteType = .departure } @@ -96,11 +84,6 @@ extension BusAreaSelectedViewController { busAreaCollectionView.configure(busAreaLists: busAreaLists, buttonState: busRouteType) } - func swap(departure: BusPlace, arrival: BusPlace) { - busAreaCollectionView.departureBusAreaPublisher.send(arrival) - busAreaCollectionView.arrivalBusAreaPublisher.send(departure) - } - private func setUpView(buttonState: BusAreaButtonState) { let attributeContainer: [NSAttributedString.Key: Any] = [.font: UIFont.appFont(.pretendardMedium, size: 15), .foregroundColor: UIColor.appColor(.neutral0)] let confirmButtonTitle: String @@ -117,11 +100,11 @@ extension BusAreaSelectedViewController { @objc private func tapConfirmButton() { if let departure = busAreaCollectionView.departureBusAreaPublisher.value, busRouteType == .departure { - departureBusAreaPublisher.send(departure) + onDepartureBusAreaSelected(departure) } if let arrival = busAreaCollectionView.arrivalBusAreaPublisher.value, busRouteType == .arrival { - arrivalBusAreaPublisher.send(arrival) + onArrivalBusAreaSelected(arrival) } if buttonState == .allSelected { @@ -174,4 +157,3 @@ extension BusAreaSelectedViewController { self.view.backgroundColor = .systemBackground } } - From df0cdc04efa46bc7e79606c27d44d0df431856d1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:18:43 +0900 Subject: [PATCH 021/101] =?UTF-8?q?refactor:=20CallVanPostViewController?= =?UTF-8?q?=EA=B0=80=20BottomSheetViewControllerB=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../CallVanPostViewController.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index df74f8b3..ec4cd39f 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -33,12 +33,6 @@ final class CallVanPostViewController: UIViewController { private let postButton = UIButton() private let bottomSheetContentView = CallVanPostPlaceBottomSheetView() - private lazy var bottomSheetViewController = BottomSheetViewControllerB( - contentView: bottomSheetContentView, - dimColor: .black, - dimAlpha: 0.7, - backgroundColor: UIColor.appColor(.neutral0) - ) // MARK: - Initializer init(viewModel: CallVanPostViewModel) { @@ -56,7 +50,6 @@ final class CallVanPostViewController: UIViewController { configureNavigationBar(style: .empty) configureView() setAddTargets() - setDelegates() bind() dateView.update(Date()) timeView.update(Date()) @@ -139,11 +132,6 @@ extension CallVanPostViewController: PopLoggable { } extension CallVanPostViewController { - - private func setDelegates() { - bottomSheetContentView.delegate = bottomSheetViewController - } - private func setAddTargets() { postButton.addTarget(self, action: #selector(postButtonTapped), for: .touchUpInside) } @@ -193,8 +181,9 @@ extension CallVanPostViewController { customPlace: viewModel.request.departureCustomName, onApplyButtonTapped: onApplyButtonTapped ) - present(bottomSheetViewController, animated: false) + presentPlaceBottomSheet() } + private func presentArrivalPlaceBottomSheet() { let onApplyButtonTapped: (CallVanPlace, String?)->Void = { [weak self] (place, customPlace) in guard let self else { return } @@ -210,6 +199,17 @@ extension CallVanPostViewController { customPlace: viewModel.request.arrivalCustomName, onApplyButtonTapped: onApplyButtonTapped ) + presentPlaceBottomSheet() + } + + private func presentPlaceBottomSheet() { + let bottomSheetViewController = BottomSheetViewControllerB( + contentView: bottomSheetContentView, + dimColor: .black, + dimAlpha: 0.7, + backgroundColor: UIColor.appColor(.neutral0) + ) + bottomSheetContentView.delegate = bottomSheetViewController present(bottomSheetViewController, animated: false) } } From 8f29cda2b22a782ec2d70a65705589e6c2bad8d1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:21:35 +0900 Subject: [PATCH 022/101] =?UTF-8?q?refactor:=20ChangeMyProfileViewControll?= =?UTF-8?q?er=EA=B0=80=20RevokeModalViewController=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../ChangeMyProfileViewController.swift | 15 +++++---------- .../RevokeModalViewController.swift | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift index 8eb028b0..87851c11 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift @@ -27,11 +27,6 @@ final class ChangeMyProfileViewController: UIViewController { private let scrollView = UIScrollView().then { scrollView in } - private let revokeModalViewController = RevokeModalViewController().then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - private lazy var deptButton = UIButton().then { $0.isHidden = userType == .general ? true : false } @@ -281,11 +276,6 @@ final class ChangeMyProfileViewController: UIViewController { // MARK: - Bind private func bind() { - - revokeModalViewController.revokeButtonPublisher.sink { [weak self] _ in - self?.viewModel.revoke() - }.store(in: &subscriptions) - viewModel.nicknameMessagePublisher.receive(on: DispatchQueue.main).sink { [weak self] response in self?.nicknameStateView.isHidden = false self?.nicknameStateView.setState(state: response.1 ? .success : .warning, message: response.0) @@ -387,6 +377,11 @@ final class ChangeMyProfileViewController: UIViewController { extension ChangeMyProfileViewController { @objc private func revokeButtonTapped() { + let revokeModalViewController = RevokeModalViewController(onRevokeButtonTapped: { [weak self] in + self?.viewModel.revoke() + }) + revokeModalViewController.modalPresentationStyle = .overFullScreen + revokeModalViewController.modalTransitionStyle = .crossDissolve present(revokeModalViewController, animated: true, completion: nil) } @objc private func inquryButtonTapped() { diff --git a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift index b98f5d3c..ff47fe8c 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift @@ -6,12 +6,10 @@ // -import Combine import UIKit final class RevokeModalViewController: UIViewController { - - let revokeButtonPublisher = PassthroughSubject() + private let onRevokeButtonTapped: () -> Void private let messageLabel = UILabel().then { $0.font = UIFont.appFont(.pretendardMedium, size: 18) @@ -67,6 +65,16 @@ final class RevokeModalViewController: UIViewController { view.layer.masksToBounds = true return view }() + + init(onRevokeButtonTapped: @escaping () -> Void) { + self.onRevokeButtonTapped = onRevokeButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() @@ -81,7 +89,7 @@ final class RevokeModalViewController: UIViewController { @objc private func revokeButtonTapped() { dismiss(animated: true, completion: nil) - revokeButtonPublisher.send(()) + onRevokeButtonTapped() } } From 98daff718c8d192faf488b4eb773e663316b1c86 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:25:49 +0900 Subject: [PATCH 023/101] =?UTF-8?q?refactor:=20ChatViewController=EA=B0=80?= =?UTF-8?q?=20BlockCheckModalViewController=EB=A5=BC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=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 --- .../Chat/BlockCheckModalViewController.swift | 16 +++++-- .../Chat/Chat/ChatViewController.swift | 43 +++++++++++-------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift b/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift index ceb75a9c..927726fd 100644 --- a/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift +++ b/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift @@ -5,11 +5,10 @@ // Created by 김나훈 on 2/18/25. // -import Combine import UIKit final class BlockCheckModalViewController: UIViewController { - let buttonPublihser = PassthroughSubject() + private let onBlockButtonTapped: () -> Void private let blockButton = UIButton().then { var configuration = UIButton.Configuration.plain() @@ -30,6 +29,16 @@ final class BlockCheckModalViewController: UIViewController { $0.layer.shadowOffset = CGSize(width: 0, height: 2) $0.layer.shadowRadius = 4 } + + init(onBlockButtonTapped: @escaping () -> Void) { + self.onBlockButtonTapped = onBlockButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() @@ -40,8 +49,7 @@ final class BlockCheckModalViewController: UIViewController { } @objc func blockButtonTapped() { - buttonPublihser.send() - dismiss(animated: true, completion: nil) + dismiss(animated: true, completion: onBlockButtonTapped) } @objc func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/Chat/Chat/ChatViewController.swift index 797a7972..79f111e1 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/Chat/Chat/ChatViewController.swift @@ -44,11 +44,6 @@ final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerVi $0.setImage(UIImage.appImage(asset: .send), for: .normal) } - private let blockCheckModalViewController = BlockCheckModalViewController().then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - private let chatHistoryTableView = ChatHistoryTableView().then { $0.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) } @@ -111,18 +106,6 @@ final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerVi } }.store(in: &subscriptions) - blockCheckModalViewController.buttonPublihser.sink { [weak self] in - guard let self else { return } - let onRightButtonTapped: ()->Void = { [weak self] in - self?.inputSubject.send(.blockUser) - } - let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 179, paddingBetweenLabels: 8, title: "이 사용자를 차단하시겠습니까?", subTitle: "쪽지 수신 및 발신이 모두 차단됩니다.", titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray), rightButtonText: "차단하기") - modalViewController.modalTransitionStyle = .crossDissolve - dismiss(animated: true) { [weak self] in - self?.present(modalViewController, animated: false) - } - }.store(in: &subscriptions) - chatHistoryTableView.imageTapPublisher.sink { [weak self] imageUrl in self?.dismissKeyboard() @@ -180,9 +163,33 @@ extension ChatViewController{ @objc private func rightButtonTapped() { dismissKeyboard() - + + let blockCheckModalViewController = BlockCheckModalViewController(onBlockButtonTapped: { [weak self] in + self?.presentBlockUserConfirmationModal() + }) + blockCheckModalViewController.modalPresentationStyle = .overFullScreen + blockCheckModalViewController.modalTransitionStyle = .crossDissolve present(blockCheckModalViewController, animated: true) } + + private func presentBlockUserConfirmationModal() { + let modalViewController = ModalViewController( + onRightButtonTapped: { [weak self] in + self?.inputSubject.send(.blockUser) + }, + width: 301, + height: 179, + paddingBetweenLabels: 8, + title: "이 사용자를 차단하시겠습니까?", + subTitle: "쪽지 수신 및 발신이 모두 차단됩니다.", + titleColor: .appColor(.neutral700), + subTitleColor: .appColor(.gray), + rightButtonText: "차단하기" + ) + modalViewController.modalTransitionStyle = .crossDissolve + present(modalViewController, animated: false) + } + @objc private func keyboardWillShow(_ notification: Notification) { guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } let keyboardHeight = keyboardFrame.height From 9dd6f21c78a35c2ad846f16ea95501358ecd2ca2 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:28:41 +0900 Subject: [PATCH 024/101] =?UTF-8?q?refactor:=20DiningViewController?= =?UTF-8?q?=EA=B0=80=20DiningNotiContentViewController=EB=A5=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=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 --- .../Dining/Dining/DiningViewController.swift | 28 ++++++++---------- .../DiningNotiContentViewController.swift | 29 +++++++++---------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/Koin/Presentation/Dining/Dining/DiningViewController.swift b/Koin/Presentation/Dining/Dining/DiningViewController.swift index c856e292..d288e123 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewController.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewController.swift @@ -88,8 +88,6 @@ final class DiningViewController: UIViewController { $0.textAlignment = .center } - private let diningNotiContentViewController = DiningNotiContentViewController() - // MARK: - Initialization init(viewModel: DiningViewModel) { @@ -193,19 +191,6 @@ final class DiningViewController: UIViewController { self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.getCurrentDiningType())) }.store(in: &subscriptions) - diningNotiContentViewController.soldOutSwitchPublisher.sink { [weak self] isOn in - self?.inputSubject.send(.changeNoti(isOn, .diningSoldOut)) - }.store(in: &subscriptions) - - diningNotiContentViewController.imageUploadSwitchPublisher.sink { [weak self] isOn in - self?.inputSubject.send(.changeNoti(isOn, .diningImageUpload)) - }.store(in: &subscriptions) - - diningNotiContentViewController.shortcutButtonPublisher.sink { [weak self] in - self?.navigateToNoti() - self?.diningNotiContentViewController.dissmissView() - }.store(in: &subscriptions) - NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification).sink { [weak self] _ in self?.diningListCollectionView.startToolTipImageViewAnimation() } @@ -275,9 +260,20 @@ extension DiningViewController { } private func showBottomSheet(_ isOn: (Bool, Bool)) { + let diningNotiContentViewController = DiningNotiContentViewController( + onSoldOutSwitchChanged: { [weak self] isOn in + self?.inputSubject.send(.changeNoti(isOn, .diningSoldOut)) + }, + onImageUploadSwitchChanged: { [weak self] isOn in + self?.inputSubject.send(.changeNoti(isOn, .diningImageUpload)) + }, + onShortcutButtonTapped: { [weak self] in + self?.navigateToNoti() + } + ) let bottomSheetViewController = BottomSheetViewController(contentViewController: diningNotiContentViewController, defaultHeight: 332, cornerRadius: 16, dimmedAlpha: 0.4, isPannedable: false) diningNotiContentViewController.updateButtonIsOn(isOn) - self.present(bottomSheetViewController, animated: true) + present(bottomSheetViewController, animated: true) } @objc private func navigationButtonTapped() { diff --git a/Koin/Presentation/Dining/Dining/SubViews/DiningNotiContentViewController.swift b/Koin/Presentation/Dining/Dining/SubViews/DiningNotiContentViewController.swift index 0e20d576..189ccf3a 100644 --- a/Koin/Presentation/Dining/Dining/SubViews/DiningNotiContentViewController.swift +++ b/Koin/Presentation/Dining/Dining/SubViews/DiningNotiContentViewController.swift @@ -5,16 +5,13 @@ // Created by 김나훈 on 7/29/24. // -import Combine import Then import UIKit final class DiningNotiContentViewController: UIViewController { - - - let soldOutSwitchPublisher = PassthroughSubject() - let imageUploadSwitchPublisher = PassthroughSubject() - let shortcutButtonPublisher = PassthroughSubject() + private let onSoldOutSwitchChanged: (Bool) -> Void + private let onImageUploadSwitchChanged: (Bool) -> Void + private let onShortcutButtonTapped: () -> Void private let diningNotiLabel = UILabel().then { $0.font = UIFont.appFont(.pretendardBold, size: 18) @@ -67,7 +64,14 @@ final class DiningNotiContentViewController: UIViewController { $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 14) } - init() { + init( + onSoldOutSwitchChanged: @escaping (Bool) -> Void, + onImageUploadSwitchChanged: @escaping (Bool) -> Void, + onShortcutButtonTapped: @escaping () -> Void + ) { + self.onSoldOutSwitchChanged = onSoldOutSwitchChanged + self.onImageUploadSwitchChanged = onImageUploadSwitchChanged + self.onShortcutButtonTapped = onShortcutButtonTapped super.init(nibName: nil, bundle: nil) } @@ -92,21 +96,17 @@ extension DiningNotiContentViewController { imageUploadSwitch.isOn = isOn.1 } - func dissmissView() { - dismiss(animated: true, completion: nil) - } - @objc private func buttonTapped(_ sender: UIButton) { switch sender { - case notiShortcutButton: shortcutButtonPublisher.send(()) + case notiShortcutButton: dismiss(animated: true, completion: onShortcutButtonTapped) default: dismiss(animated: true, completion: nil) } } @objc private func switchToggled(_ sender: UISwitch) { switch sender { - case soldOutSwitch: soldOutSwitchPublisher.send(sender.isOn) - default: imageUploadSwitchPublisher.send(sender.isOn) + case soldOutSwitch: onSoldOutSwitchChanged(sender.isOn) + default: onImageUploadSwitchChanged(sender.isOn) } } } @@ -165,4 +165,3 @@ extension DiningNotiContentViewController { self.view.backgroundColor = .systemBackground } } - From 7043ec1c7ee71aa587c34201b0504aa4681f7279 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:31:46 +0900 Subject: [PATCH 025/101] =?UTF-8?q?refactor:=20ForceUpdateViewController?= =?UTF-8?q?=EA=B0=80=20UpdateModalViewController=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../ForceUpdateViewController.swift | 28 ++++++++----------- .../UpdateModalViewController.swift | 24 ++++++++++++---- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift index 4b5d4e00..b0c16e34 100644 --- a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift +++ b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift @@ -86,11 +86,6 @@ final class ForceUpdateViewController: UIViewController, LottieAnimationManageab $0.backgroundColor = .clear } - private let updateModalViewController = UpdateModalViewController().then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - // MARK: - Initialization init(viewModel: ForceUpdateViewModel) { self.viewModel = viewModel @@ -140,19 +135,7 @@ final class ForceUpdateViewController: UIViewController, LottieAnimationManageab } }.store(in: &subscriptions) - updateModalViewController.openStoreButtonPublisher.sink { [weak self] in - self?.openStore() - }.store(in: &subscriptions) - setupCustomNotificationObservers() - - updateModalViewController.openStoreButtonPublisher.sink { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) - }.store(in: &subscriptions) - - updateModalViewController.cancelButtonPublisher.sink { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) - }.store(in: &subscriptions) } private func setAddTarget() { @@ -197,6 +180,17 @@ extension ForceUpdateViewController { } @objc private func errorCheckButtonTapped() { + let updateModalViewController = UpdateModalViewController( + onOpenStoreButtonTapped: { [weak self] in + self?.openStore() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) + }, + onCloseButtonTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) + } + ) + updateModalViewController.modalPresentationStyle = .overFullScreen + updateModalViewController.modalTransitionStyle = .crossDissolve present(updateModalViewController, animated: true, completion: nil) inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.forceUpdateAlreadyDone, .click, "이미업데이트")) } diff --git a/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift b/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift index f4eefe28..4adb8308 100644 --- a/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift +++ b/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift @@ -5,15 +5,13 @@ // Created by 김나훈 on 10/1/24. // -import Combine import UIKit import SnapKit final class UpdateModalViewController: UIViewController { - // MARK: - Properties - let openStoreButtonPublisher = PassthroughSubject() - let cancelButtonPublisher = PassthroughSubject() + private let onOpenStoreButtonTapped: () -> Void + private let onCloseButtonTapped: () -> Void // MARK: - UI Components private let messageLabel = UILabel().then { @@ -59,6 +57,20 @@ final class UpdateModalViewController: UIViewController { $0.layer.cornerRadius = 4 $0.layer.masksToBounds = true } + + init( + onOpenStoreButtonTapped: @escaping () -> Void, + onCloseButtonTapped: @escaping () -> Void + ) { + self.onOpenStoreButtonTapped = onOpenStoreButtonTapped + self.onCloseButtonTapped = onCloseButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - Life Cycle override func viewDidLoad() { @@ -77,12 +89,12 @@ final class UpdateModalViewController: UIViewController { extension UpdateModalViewController { @objc private func closeButtonTapped() { dismiss(animated: true, completion: nil) - cancelButtonPublisher.send() + onCloseButtonTapped() } @objc private func openStoreButtonTapped() { dismiss(animated: true, completion: nil) - openStoreButtonPublisher.send(()) + onOpenStoreButtonTapped() } } From cdecb296e21e4bed69d13ffc6d138dffbbb2df8f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:50:27 +0900 Subject: [PATCH 026/101] =?UTF-8?q?refactor:=20LoginViewController?= =?UTF-8?q?=EA=B0=80=20ModifyUserModalViewController=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=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 --- .../Login/Login/LoginViewController.swift | 45 ++++++++++--------- .../Login/ModifyUserModalViewController.swift | 19 ++++---- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index f0a80c9f..c3dd2df4 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -109,8 +109,6 @@ final class LoginViewController: UIViewController { $0.textAlignment = .center } - private let modifyUserModalViewController = ModifyUserModalViewController() - // MARK: - Initialization init(viewModel: LoginViewModel) { self.viewModel = viewModel @@ -144,24 +142,6 @@ final class LoginViewController: UIViewController { } private func bind() { - modifyUserModalViewController.cancelButtonPublisher.sink { [weak self] in - self?.navigationController?.popViewController(animated: true) - }.store(in: &subscriptions) - - modifyUserModalViewController.navigateButtonPublisher.sink { [weak self] in - guard let self = self else { return } - let homeViewController = makeHomeTabBarController() - - let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let modifyUseCase = DefaultModifyUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) - let fetchDeptListUseCase = DefaultFetchDeptListUseCase(timetableRepository: DefaultTimetableRepository(service: DefaultTimetableService())) - let fetchUserDataUseCase = DefaultFetchUserDataUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) - let checkDuplicatedNicknameUseCase = DefaultCheckDuplicatedNicknameUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) - let changeMyProfileViewController = ChangeMyProfileViewController(viewModel: ChangeMyProfileViewModel(modifyUseCase: modifyUseCase, fetchDeptListUseCase: fetchDeptListUseCase, fetchUserDataUseCase: fetchUserDataUseCase, checkDuplicatedNicknameUseCase: checkDuplicatedNicknameUseCase, logAnalyticsEventUseCase: logAnalyticsEventUseCase), userType: .student) - navigationController?.setViewControllers([homeViewController, changeMyProfileViewController], animated: true) - - }.store(in: &subscriptions) - let outputSubject = viewModel.transform(with: inputSubject.eraseToAnyPublisher()) outputSubject.receive(on: DispatchQueue.main).sink { [weak self] output in switch output { @@ -176,13 +156,36 @@ final class LoginViewController: UIViewController { case .showForceModal: self?.navigationController?.setViewControllers([ForceModifyUserViewController()], animated: true) case .showModifyModal: - self?.present(self?.modifyUserModalViewController ?? UIViewController(), animated: true) + self?.presentModifyUserModal() } }.store(in: &subscriptions) } } extension LoginViewController { + private func presentModifyUserModal() { + let modifyUserModalViewController = ModifyUserModalViewController( + onCancelButtonTapped: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onNavigateButtonTapped: { [weak self] in + self?.navigateToChangeMyProfile() + } + ) + present(modifyUserModalViewController, animated: true) + } + + private func navigateToChangeMyProfile() { + let homeViewController = makeHomeTabBarController() + let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) + let modifyUseCase = DefaultModifyUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) + let fetchDeptListUseCase = DefaultFetchDeptListUseCase(timetableRepository: DefaultTimetableRepository(service: DefaultTimetableService())) + let fetchUserDataUseCase = DefaultFetchUserDataUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) + let checkDuplicatedNicknameUseCase = DefaultCheckDuplicatedNicknameUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) + let changeMyProfileViewController = ChangeMyProfileViewController(viewModel: ChangeMyProfileViewModel(modifyUseCase: modifyUseCase, fetchDeptListUseCase: fetchDeptListUseCase, fetchUserDataUseCase: fetchUserDataUseCase, checkDuplicatedNicknameUseCase: checkDuplicatedNicknameUseCase, logAnalyticsEventUseCase: logAnalyticsEventUseCase), userType: .student) + navigationController?.setViewControllers([homeViewController, changeMyProfileViewController], animated: true) + } + @objc private func changeSecureButtonTapped() { passwordTextField.isSecureTextEntry.toggle() changeSecureButton.setImage(passwordTextField.isSecureTextEntry ? UIImage.appImage(asset: .visibility) : UIImage.appImage(asset: .visibilityNon), for: .normal) diff --git a/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift b/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift index 8d6cd601..231173f1 100644 --- a/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift +++ b/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift @@ -5,13 +5,11 @@ // Created by 김나훈 on 7/14/25. // -import Combine import UIKit final class ModifyUserModalViewController: UIViewController { - - let cancelButtonPublisher = PassthroughSubject() - let navigateButtonPublisher = PassthroughSubject() + private let onCancelButtonTapped: () -> Void + private let onNavigateButtonTapped: () -> Void private let messageLabel = UILabel().then { $0.text = "아직 입력되지 않은 정보가 있어요." @@ -55,7 +53,12 @@ final class ModifyUserModalViewController: UIViewController { return view }() - init() { + init( + onCancelButtonTapped: @escaping () -> Void, + onNavigateButtonTapped: @escaping () -> Void + ) { + self.onCancelButtonTapped = onCancelButtonTapped + self.onNavigateButtonTapped = onNavigateButtonTapped super.init(nibName: nil, bundle: nil) self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve @@ -72,12 +75,10 @@ final class ModifyUserModalViewController: UIViewController { navigateButton.addTarget(self, action: #selector(navigateButtonTapped), for: .touchUpInside) } @objc private func cancelButtonTapped() { - cancelButtonPublisher.send() - dismiss(animated: true) + dismiss(animated: true, completion: onCancelButtonTapped) } @objc private func navigateButtonTapped() { - navigateButtonPublisher.send() - dismiss(animated: true) + dismiss(animated: true, completion: onNavigateButtonTapped) } } From 937d49589b0c1158cc52fb3361eda269f233b131 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:51:50 +0900 Subject: [PATCH 027/101] =?UTF-8?q?refactor:=20ReviewListViewController?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A8=EB=8B=AC=20ViewController=EB=A5=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=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 --- .../ReviewListViewController.swift | 110 +++++------------- .../DeleteReviewModalViewController.swift | 23 +++- .../ReviewLoginModalViewController.swift | 20 ++-- 3 files changed, 59 insertions(+), 94 deletions(-) diff --git a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift index b3646657..66110023 100644 --- a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift @@ -66,23 +66,6 @@ final class ReviewListViewController: UIViewController { $0.hidesWhenStopped = true } - // MARK: - Modal ViewControllers - - private lazy var reviewWriteLoginModalViewController = ReviewLoginModalViewController(message: "작성").then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - - private lazy var reviewReportLoginModalViewController = ReviewLoginModalViewController(message: "신고").then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - - private lazy var deleteReviewModalViewController = DeleteReviewModalViewController().then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - // MARK: - Initialize init(viewModel: ReviewListViewModel) { @@ -120,7 +103,6 @@ final class ReviewListViewController: UIViewController { private func bind() { bindViewModel() bindCollectionView() - bindModalViewControllers() } } @@ -144,10 +126,10 @@ extension ReviewListViewController { switch output { case .showWriteReviewLoginModal: - self.present(self.reviewWriteLoginModalViewController, animated: true) + self.presentReviewLoginModal(message: "작성") case .showReportReviewLoginModal: - self.present(self.reviewReportLoginModalViewController, animated: true) + self.presentReviewLoginModal(message: "신고") case .showMyReviewFilterError: self.nonReviewListView.isHidden = false @@ -235,66 +217,6 @@ extension ReviewListViewController { .store(in: &cancellables) } - private func bindModalViewControllers() { - reviewWriteLoginModalViewController.loginButtonPublisher - .sink { [weak self] in - guard let self else { return } - self.inputSubject.send(.logEvent( - EventParameter.EventLabel.Business.loginPrompt, - .click, - "리뷰 작성 팝업" - )) - self.showLoginScreen() - } - .store(in: &cancellables) - - reviewWriteLoginModalViewController.cancelButtonPublisher - .sink { [weak self] in - guard let self else { return } - self.inputSubject.send(.logEvent( - EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, - .click, - self.viewModel.getShopName() - )) - } - .store(in: &cancellables) - - deleteReviewModalViewController.deleteButtonPublisher - .sink { [weak self] in - guard let self else { return } - self.inputSubject.send(.logEvent( - EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, - .click, - "O" - )) - self.deleteReview() - } - .store(in: &cancellables) - - deleteReviewModalViewController.cancelButtonPublisher - .sink { [weak self] in - guard let self else { return } - self.inputSubject.send(.logEvent( - EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, - .click, - "X" - )) - } - .store(in: &cancellables) - - reviewReportLoginModalViewController.loginButtonPublisher - .sink { [weak self] in - guard let self else { return } - self.inputSubject.send(.logEvent( - EventParameter.EventLabel.Business.loginPrompt, - .click, - "리뷰 신고 팝업" - )) - self.showLoginScreen() - } - .store(in: &cancellables) - } - private func setAddTarget() { writeReviewButton.addTarget(self, action: #selector(writeReviewButtonTapped), for: .touchUpInside) } @@ -388,8 +310,36 @@ extension ReviewListViewController { .click, viewModel.getShopName() )) + let deleteReviewModalViewController = DeleteReviewModalViewController( + onDeleteButtonTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) + self?.deleteReview() + }, + onCancelButtonTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "X")) + } + ) + deleteReviewModalViewController.modalPresentationStyle = .overFullScreen + deleteReviewModalViewController.modalTransitionStyle = .crossDissolve present(deleteReviewModalViewController, animated: true) } + + private func presentReviewLoginModal(message: String) { + let reviewLoginModalViewController = ReviewLoginModalViewController( + message: message, + onLoginButtonTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) + self?.showLoginScreen() + }, + onCancelButtonTapped: { [weak self] in + guard message == "작성", let self else { return } + inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, .click, viewModel.getShopName())) + } + ) + reviewLoginModalViewController.modalPresentationStyle = .overFullScreen + reviewLoginModalViewController.modalTransitionStyle = .crossDissolve + present(reviewLoginModalViewController, animated: true) + } private func deleteReview() { let (reviewId, shopId) = viewModel.deleteParameter diff --git a/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift b/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift index 98f48bd7..c057af53 100644 --- a/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift @@ -5,15 +5,14 @@ // Created by 김나훈 on 8/13/24. // -import Combine import UIKit import SnapKit final class DeleteReviewModalViewController: UIViewController { // MARK: - Properties - let deleteButtonPublisher = PassthroughSubject() - let cancelButtonPublisher = PassthroughSubject() + private let onDeleteButtonTapped: () -> Void + private let onCancelButtonTapped: () -> Void // MARK: - UI Components private let containerView = UIView().then { @@ -58,6 +57,20 @@ final class DeleteReviewModalViewController: UIViewController { $0.layer.cornerRadius = 6 $0.layer.masksToBounds = true } + + init( + onDeleteButtonTapped: @escaping () -> Void, + onCancelButtonTapped: @escaping () -> Void + ) { + self.onDeleteButtonTapped = onDeleteButtonTapped + self.onCancelButtonTapped = onCancelButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - Life Cycle override func viewDidLoad() { @@ -74,13 +87,13 @@ final class DeleteReviewModalViewController: UIViewController { extension DeleteReviewModalViewController { @objc private func closeButtonTapped() { - cancelButtonPublisher.send() + onCancelButtonTapped() dismiss(animated: true, completion: nil) } @objc private func deleteButtonTapped() { dismiss(animated: true, completion: nil) - deleteButtonPublisher.send(()) + onDeleteButtonTapped() } } diff --git a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift b/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift index a0421b61..5b9be193 100644 --- a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift @@ -6,7 +6,6 @@ // -import Combine import UIKit final class ReviewLoginModalViewController: UIViewController { @@ -14,11 +13,8 @@ final class ReviewLoginModalViewController: UIViewController { // MARK: - Properties private let message: String - - // MARK: - Publisher - - let loginButtonPublisher = PassthroughSubject() - let cancelButtonPublisher = PassthroughSubject() + private let onLoginButtonTapped: () -> Void + private let onCancelButtonTapped: () -> Void // MARK: - UI Components @@ -64,8 +60,14 @@ final class ReviewLoginModalViewController: UIViewController { // MARK: - Initializer - init(message: String) { + init( + message: String, + onLoginButtonTapped: @escaping () -> Void, + onCancelButtonTapped: @escaping () -> Void + ) { self.message = message + self.onLoginButtonTapped = onLoginButtonTapped + self.onCancelButtonTapped = onCancelButtonTapped super.init(nibName: nil, bundle: nil) } @@ -125,12 +127,12 @@ final class ReviewLoginModalViewController: UIViewController { @objc private func closeButtonTapped() { dismiss(animated: true, completion: nil) - cancelButtonPublisher.send() + onCancelButtonTapped() } @objc private func loginButtonTapped() { dismiss(animated: true, completion: nil) - loginButtonPublisher.send(()) + onLoginButtonTapped() } } From dc5b21c1ce1f9769fdacbb516d6a8c669c480de3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:53:25 +0900 Subject: [PATCH 028/101] =?UTF-8?q?refactor:=20TimetableViewController?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A8=EB=8B=AC=20ViewController=EB=A5=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=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 --- .../DeleteLectureModalViewController.swift | 13 ++- .../SelectDeptModalViewController.swift | 13 ++- ...bstituteTimetableModalViewController.swift | 15 ++- .../Timetable/TimetableViewController.swift | 102 ++++++++++-------- 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift index 12305c60..62517a39 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift @@ -5,12 +5,11 @@ // Created by 김나훈 on 12/10/24. // -import Combine import UIKit final class DeleteLectureModalViewController: UIViewController { - let deleteButtonPublisher = PassthroughSubject() + private let onDeleteButtonTapped: (LectureData) -> Void private var lectureData: LectureData? = nil private let messageLabel = UILabel().then { @@ -47,6 +46,14 @@ final class DeleteLectureModalViewController: UIViewController { return view }() + init(onDeleteButtonTapped: @escaping (LectureData) -> Void) { + self.onDeleteButtonTapped = onDeleteButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { super.viewDidLoad() modalPresentationStyle = .overFullScreen @@ -63,7 +70,7 @@ final class DeleteLectureModalViewController: UIViewController { @objc private func deleteButtonTapped() { dismiss(animated: true, completion: nil) if let lectureData = lectureData { - deleteButtonPublisher.send(lectureData) + onDeleteButtonTapped(lectureData) } } diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift index d41ff7ac..9c4054f8 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift @@ -5,12 +5,19 @@ // Created by 김나훈 on 12/6/24. // -import Combine import UIKit final class SelectDeptModalViewController: UIViewController { - let selectedDeptPublisher = PassthroughSubject() + private let onDepartmentSelected: (String?) -> Void + + init(onDepartmentSelected: @escaping (String?) -> Void) { + self.onDepartmentSelected = onDepartmentSelected + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var departmentButtons: [UIButton] = [] private let departments = [ "디자인ㆍ건축공학부", @@ -68,7 +75,7 @@ final class SelectDeptModalViewController: UIViewController { } @objc private func completeButtonTapped() { - selectedDeptPublisher.send(selectedDepartment) + onDepartmentSelected(selectedDepartment) dismiss(animated: true, completion: nil) } diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift index 6c45482c..1cf03747 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift @@ -5,12 +5,11 @@ // Created by 김나훈 on 12/4/24. // -import Combine import UIKit final class SubstituteTimetableModalViewController: UIViewController { - let substituteButtonPublisher = PassthroughSubject() + private let onSubstituteButtonTapped: (Any) -> Void private var lectureData: LectureData? private var customLecture: (String, [Int])? @@ -63,6 +62,14 @@ final class SubstituteTimetableModalViewController: UIViewController { return view }() + init(onSubstituteButtonTapped: @escaping (Any) -> Void) { + self.onSubstituteButtonTapped = onSubstituteButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { super.viewDidLoad() configureView() @@ -77,9 +84,9 @@ final class SubstituteTimetableModalViewController: UIViewController { @objc private func substituteButtonTapped() { dismiss(animated: true, completion: nil) if let lectureData = lectureData { - substituteButtonPublisher.send(lectureData) + onSubstituteButtonTapped(lectureData) } else if let customLecture = customLecture { - substituteButtonPublisher.send(customLecture) + onSubstituteButtonTapped(customLecture) } } diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 802c5819..287ecdb4 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -89,17 +89,6 @@ final class TimetableViewController: UIViewController { $0.isHidden = true } - private let substituteTimetableModalViewController = SubstituteTimetableModalViewController().then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve - } - - private let selectDeptModalViewController = SelectDeptModalViewController().then { _ in - } - - private let deleteLectureModalViewController: DeleteLectureModalViewController = DeleteLectureModalViewController().then { _ in - } - private let containerView = UIView().then { _ in } @@ -179,16 +168,14 @@ final class TimetableViewController: UIViewController { $0.removeFromSuperview() } if lecture.1 && viewModel.checkDuplicatedClassTime(classTime: lecture.0.classTime){ - substituteTimetableModalViewController.configure(lectureData: lecture.0) - self.present(substituteTimetableModalViewController, animated: true) + self.presentSubstituteTimetableModal(lectureData: lecture.0) } else { self.inputSubject.send(.modifyLecture(lecture.0, lecture.1)) } }.store(in: &subscriptions) addClassCollectionView.filterButtonPublisher.sink { [weak self] in - guard let self = self else { return } - self.present(self.selectDeptModalViewController, animated: false) + self?.presentSelectDeptModal() }.store(in: &subscriptions) addClassCollectionView.didTapCellPublisher.sink { [weak self] (selectedLecture, filteredLectures) in @@ -219,8 +206,7 @@ final class TimetableViewController: UIViewController { self.toggleCollectionView(collectionView: self.addDirectCollectionView, animate: true) if viewModel.checkDuplicatedClassTime(classTime: item.1) { - substituteTimetableModalViewController.configure(customLecture: (item.0, item.1)) - self.present(substituteTimetableModalViewController, animated: true) + self.presentSubstituteTimetableModal(customLecture: (item.0, item.1)) } else { self.inputSubject.send(.postCustomLecture(item.0, item.1)) } @@ -237,8 +223,7 @@ final class TimetableViewController: UIViewController { deleteLectureView.deleteButtonPublisher.sink { [weak self] lecture in guard let self = self else { return } self.deleteLectureView.isHidden = true - deleteLectureModalViewController.setMessageLabelText(lectureData: lecture) - self.present(deleteLectureModalViewController, animated: false) + self.presentDeleteLectureModal(lecture: lecture) }.store(in: &subscriptions) timetableCollectionView.heightChangedPublisher.sink { [weak self] in @@ -248,34 +233,59 @@ final class TimetableViewController: UIViewController { } }.store(in: &subscriptions) - selectDeptModalViewController.selectedDeptPublisher.sink { [weak self] dept in - self?.addClassCollectionView.setUpSelectedDept(dept: dept) - }.store(in: &subscriptions) - - deleteLectureModalViewController.deleteButtonPublisher.sink { [weak self] lecture in + } +} + +extension TimetableViewController { + private func presentSubstituteTimetableModal(lectureData: LectureData) { + let viewController = makeSubstituteTimetableModal() + viewController.configure(lectureData: lectureData) + present(viewController, animated: true) + } + + private func presentSubstituteTimetableModal(customLecture: (String, [Int])) { + let viewController = makeSubstituteTimetableModal() + viewController.configure(customLecture: customLecture) + present(viewController, animated: true) + } + + private func makeSubstituteTimetableModal() -> SubstituteTimetableModalViewController { + SubstituteTimetableModalViewController(onSubstituteButtonTapped: { [weak self] response in + self?.handleSubstituteResponse(response) + }).then { + $0.modalPresentationStyle = .overFullScreen + $0.modalTransitionStyle = .crossDissolve + } + } + + private func handleSubstituteResponse(_ response: Any) { + let completion: (Subscribers.Completion) -> Void = { [weak self] _ in + self?.viewModel.selectedFrameId = self?.viewModel.selectedFrameId + } + if let lectureData = response as? LectureData { + viewModel.performLectureModification(lectureData: lectureData) + .sink(receiveCompletion: completion, receiveValue: { _ in }) + .store(in: &subscriptions) + } else if let customLecture = response as? (String, [Int]) { + viewModel.performCustomLectureModification(lectureName: customLecture.0, lectureTime: customLecture.1) + .sink(receiveCompletion: completion, receiveValue: { _ in }) + .store(in: &subscriptions) + } + } + + private func presentSelectDeptModal() { + let viewController = SelectDeptModalViewController(onDepartmentSelected: { [weak self] department in + self?.addClassCollectionView.setUpSelectedDept(dept: department) + }) + present(viewController, animated: false) + } + + private func presentDeleteLectureModal(lecture: LectureData) { + let viewController = DeleteLectureModalViewController(onDeleteButtonTapped: { [weak self] lecture in self?.inputSubject.send(._deleteLecture(lecture)) - }.store(in: &subscriptions) - - substituteTimetableModalViewController.substituteButtonPublisher.sink { [weak self] response in - guard let self = self else { return } - - if let lectureData = response as? LectureData { - self.viewModel.performLectureModification(lectureData: lectureData).sink( - receiveCompletion: { _ in - self.viewModel.selectedFrameId = self.viewModel.selectedFrameId - }, - receiveValue: { _ in } - ).store(in: &self.subscriptions) - - } else if let customLecture = response as? (String, [Int]) { - self.viewModel.performCustomLectureModification(lectureName: customLecture.0, lectureTime: customLecture.1).sink( - receiveCompletion: { _ in - self.viewModel.selectedFrameId = self.viewModel.selectedFrameId - }, - receiveValue: { _ in } - ).store(in: &self.subscriptions) - } - }.store(in: &subscriptions) + }) + viewController.setMessageLabelText(lectureData: lecture) + present(viewController, animated: false) } } From 90991b6fcd1e01136a406a7db6a182d67102007e Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 19:36:13 +0900 Subject: [PATCH 029/101] =?UTF-8?q?refactor:=20FrameListViewController?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A8=EB=8B=AC=20ViewController=EB=A5=BC=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A1=9C=20=EA=B0=96?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=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 --- .../FrameList/FrameListViewController.swift | 83 +++++++++---------- .../DeleteFrameModalViewController.swift | 16 +++- .../DeleteSemesterModalViewController.swift | 16 +++- .../ModifyFrameModalViewController.swift | 20 +++-- .../ModifySemesterModalViewController.swift | 14 ++-- 5 files changed, 82 insertions(+), 67 deletions(-) diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index c30c5323..45e27f49 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -20,10 +20,6 @@ final class FrameListViewController: UIViewController { // MARK: - UI Components private let tableView = UITableView(frame: .zero, style: .plain) - private let modifyFrameModalViewController: ModifyFrameModalViewController = ModifyFrameModalViewController(width: 327, height: 216) - - private let modifySemesterModalViewController: ModifySemesterModalViewController = ModifySemesterModalViewController(width: 327, height: 232) - private let emptyFrameLabel = UILabel().then { $0.text = "우측 상단의 버튼으로 학기를 추가해\n시간표 기능을 사용해 보세요!" $0.textAlignment = .center @@ -32,12 +28,6 @@ final class FrameListViewController: UIViewController { $0.font = UIFont.appFont(.pretendardMedium, size: 13) } - private let deleteSemesterModalViewController = DeleteSemesterModalViewController().then { _ in - } - - private let deleteFrameModalViewController = DeleteFrameModalViewController().then { _ in - } - // MARK: - Initialization func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return .leastNormalMagnitude // 기본 여백 제거 @@ -96,33 +86,6 @@ final class FrameListViewController: UIViewController { } .store(in: &subscriptions) - modifyFrameModalViewController.deleteButtonPublisher.sink(receiveValue: { [weak self] frame in - guard let self = self else { return } - deleteFrameModalViewController.configure(frame: frame) - present(deleteFrameModalViewController, animated: false) - }).store(in: &subscriptions) - - modifyFrameModalViewController.saveButtonPublisher.sink(receiveValue: { [weak self] frame in - self?.inputSubject.send(.modifyFrame(frame)) - - }).store(in: &subscriptions) - - modifySemesterModalViewController.applyButtonPublisher.sink { [weak self] addedSemester, removedSemester in - guard let self = self else { return } - inputSubject.send(.modifySemester(addedSemester, [])) - if !removedSemester.isEmpty { - deleteSemesterModalViewController.setSemesters(semesters: removedSemester) - present(deleteSemesterModalViewController, animated: false) - } - }.store(in: &subscriptions) - - deleteSemesterModalViewController.deleteButtonPublisher.sink { [weak self] semesters in - self?.inputSubject.send(.modifySemester([], semesters)) - }.store(in: &subscriptions) - - deleteFrameModalViewController.deleteButtonPublisher.sink { [weak self] frame in - self?.inputSubject.send(.deleteFrame(frame)) - }.store(in: &subscriptions) } } @@ -130,10 +93,15 @@ final class FrameListViewController: UIViewController { extension FrameListViewController: TimetableCellDelegate { @objc private func modifySemesterButtonTapped() { - + let modifySemesterModalViewController = ModifySemesterModalViewController( + onApplyButtonTapped: { [weak self] addedSemesters, removedSemesters in + self?.applySemesterChanges(addedSemesters: addedSemesters, removedSemesters: removedSemesters) + }, + width: 327, + height: 232 + ) modifySemesterModalViewController.configre(frameList: viewModel.frameData) - self.present(modifySemesterModalViewController, animated: true) - + present(modifySemesterModalViewController, animated: true) } @objc private func addTimetableTapped(_ sender: UIButton) { @@ -152,13 +120,38 @@ extension FrameListViewController: TimetableCellDelegate { let row = indexPath.row // 추가 동작 (예: 삭제 모달 띄우기) - let timetable = viewModel.frameData[section].frame[row] + let modifyFrameModalViewController = ModifyFrameModalViewController( + onDeleteButtonTapped: { [weak self] frame in + self?.presentDeleteFrameModal(frame: frame) + }, + onSaveButtonTapped: { [weak self] frame in + self?.inputSubject.send(.modifyFrame(frame)) + }, + width: 327, + height: 216 + ) modifyFrameModalViewController.configure(frame: viewModel.frameData[section].frame[row]) - self.present(modifyFrameModalViewController, animated: true) - - + present(modifyFrameModalViewController, animated: true) + } + + private func applySemesterChanges(addedSemesters: [String], removedSemesters: [String]) { + inputSubject.send(.modifySemester(addedSemesters, [])) + guard !removedSemesters.isEmpty else { return } + + let deleteSemesterModalViewController = DeleteSemesterModalViewController(onDeleteButtonTapped: { [weak self] semesters in + self?.inputSubject.send(.modifySemester([], semesters)) + }) + deleteSemesterModalViewController.setSemesters(semesters: removedSemesters) + present(deleteSemesterModalViewController, animated: false) + } + + private func presentDeleteFrameModal(frame: FrameDto) { + let deleteFrameModalViewController = DeleteFrameModalViewController(onDeleteButtonTapped: { [weak self] frame in + self?.inputSubject.send(.deleteFrame(frame)) + }) + deleteFrameModalViewController.configure(frame: frame) + present(deleteFrameModalViewController, animated: false) } - } extension FrameListViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift index 60d535a5..57b3029c 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift @@ -5,12 +5,10 @@ // Created by 김나훈 on 12/10/24. // -import Combine import UIKit final class DeleteFrameModalViewController: UIViewController { - - let deleteButtonPublisher = PassthroughSubject() + private let onDeleteButtonTapped: (FrameDto) -> Void private var frame: FrameDto? = nil private let messageLabel = UILabel().then { @@ -43,6 +41,16 @@ final class DeleteFrameModalViewController: UIViewController { $0.layer.cornerRadius = 4 $0.layer.masksToBounds = true } + + init(onDeleteButtonTapped: @escaping (FrameDto) -> Void) { + self.onDeleteButtonTapped = onDeleteButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() @@ -53,7 +61,7 @@ final class DeleteFrameModalViewController: UIViewController { @objc private func deleteButtonTapped() { if let frame = frame { - deleteButtonPublisher.send(frame) + onDeleteButtonTapped(frame) } dismiss(animated: true, completion: nil) } diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift index c6d8ffb0..1aa44f39 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift @@ -5,12 +5,10 @@ // Created by 김나훈 on 12/10/24. // -import Combine import UIKit final class DeleteSemesterModalViewController: UIViewController { - - let deleteButtonPublisher = PassthroughSubject<[String], Never>() + private let onDeleteButtonTapped: ([String]) -> Void private var semesters: [String]? = nil private let messageLabel = UILabel().then { @@ -54,6 +52,16 @@ final class DeleteSemesterModalViewController: UIViewController { view.layer.masksToBounds = true return view }() + + init(onDeleteButtonTapped: @escaping ([String]) -> Void) { + self.onDeleteButtonTapped = onDeleteButtonTapped + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() @@ -70,7 +78,7 @@ final class DeleteSemesterModalViewController: UIViewController { @objc private func deleteButtonTapped() { if let semesters = semesters { - deleteButtonPublisher.send(semesters) + onDeleteButtonTapped(semesters) } dismiss(animated: true, completion: nil) } diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift index 5a8ddcca..59eff308 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift @@ -5,14 +5,11 @@ // Created by 김나훈 on 11/21/24. // -import Combine import UIKit final class ModifyFrameModalViewController: UIViewController { - - let deleteButtonPublisher = PassthroughSubject() - let cancelButtonPublisher = PassthroughSubject() - let saveButtonPublisher = PassthroughSubject() + private let onDeleteButtonTapped: (FrameDto) -> Void + private let onSaveButtonTapped: (FrameDto) -> Void var containerWidth: CGFloat var containerHeight: CGFloat var frame: FrameDto = FrameDto(id: 0, timetableName: "", isMain: false) @@ -70,7 +67,14 @@ final class ModifyFrameModalViewController: UIViewController { return view }() - init(width: CGFloat, height: CGFloat) { + init( + onDeleteButtonTapped: @escaping (FrameDto) -> Void, + onSaveButtonTapped: @escaping (FrameDto) -> Void, + width: CGFloat, + height: CGFloat + ) { + self.onDeleteButtonTapped = onDeleteButtonTapped + self.onSaveButtonTapped = onSaveButtonTapped self.containerWidth = width self.containerHeight = height super.init(nibName: nil, bundle: nil) @@ -109,14 +113,14 @@ final class ModifyFrameModalViewController: UIViewController { } @objc private func deleteButtonTapped() { dismiss(animated: true, completion: nil) - deleteButtonPublisher.send(frame) + onDeleteButtonTapped(frame) } @objc private func cancelButtonTapped() { dismiss(animated: true, completion: nil) } @objc private func saveButtonTapped() { - saveButtonPublisher.send(frame) + onSaveButtonTapped(frame) dismiss(animated: true, completion: nil) } override func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift index 2702daaa..e66c3705 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift @@ -5,12 +5,10 @@ // Created by 김나훈 on 11/21/24. // -import Combine import UIKit final class ModifySemesterModalViewController: UIViewController { - - let applyButtonPublisher = PassthroughSubject<([String], [String]), Never>() + private let onApplyButtonTapped: ([String], [String]) -> Void var frameList: [FrameData] = [] var containerWidth: CGFloat var containerHeight: CGFloat @@ -64,7 +62,12 @@ final class ModifySemesterModalViewController: UIViewController { $0.setTitle("적용하기", for: .normal) } - init(width: CGFloat, height: CGFloat) { + init( + onApplyButtonTapped: @escaping ([String], [String]) -> Void, + width: CGFloat, + height: CGFloat + ) { + self.onApplyButtonTapped = onApplyButtonTapped self.containerWidth = width self.containerHeight = height super.init(nibName: nil, bundle: nil) @@ -177,8 +180,7 @@ extension ModifySemesterModalViewController { } print(addedSemesters) print(removedSemesters) - // Publish 결과 - applyButtonPublisher.send((addedSemesters, removedSemesters)) + onApplyButtonTapped(addedSemesters, removedSemesters) updateSemesterButtons() } From 3565e210172b7c230e972d63d56917cfbdae9810 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 13 Aug 2026 23:54:59 +0900 Subject: [PATCH 030/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameList/SubViews/ModifyFrameModalViewController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift index 59eff308..90b33fd5 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift @@ -112,8 +112,10 @@ final class ModifyFrameModalViewController: UIViewController { checkButton.setImage(UIImage.appImage(asset: frame.isMain ? .checkFill : .checkEmpty), for: .normal) } @objc private func deleteButtonTapped() { - dismiss(animated: true, completion: nil) - onDeleteButtonTapped(frame) + dismiss(animated: true) { [weak self] in + guard let self else { return } + onDeleteButtonTapped(frame) + } } @objc private func cancelButtonTapped() { dismiss(animated: true, completion: nil) From 3f459c3554d4db4c1e7f9f6834442c6beeecf15f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 14 Aug 2026 12:30:02 +0900 Subject: [PATCH 031/101] =?UTF-8?q?refactor:=20ModalViewController=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=A7=80=EC=85=98=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalViewController.swift} | 80 +++++++++------- .../Transition/KoinModalAnimator.swift | 95 +++++++++++++++++++ .../KoinModalPresentationController.swift | 81 ++++++++++++++++ .../BusSearchResultViewController.swift | 3 +- .../BusSearchDatePickerViewController.swift | 6 +- .../Chat/Chat/ChatViewController.swift | 5 +- .../LostItemDataViewController.swift | 19 ++-- .../LostItemKeywordViewController.swift | 5 +- .../LostItemListViewController.swift | 4 +- .../ManageNoticeKeywordViewController.swift | 5 +- koin.xcodeproj/project.pbxproj | 32 ++++++- 11 files changed, 271 insertions(+), 64 deletions(-) rename Koin/Core/View/{ModalViewController.swift => KoinModalViewController/KoinModalViewController.swift} (87%) create mode 100644 Koin/Core/View/KoinModalViewController/Transition/KoinModalAnimator.swift create mode 100644 Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift diff --git a/Koin/Core/View/ModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift similarity index 87% rename from Koin/Core/View/ModalViewController.swift rename to Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index ae01752c..17800ee4 100644 --- a/Koin/Core/View/ModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -1,5 +1,5 @@ // -// LoginModalViewController.swift +// KoinModalViewController.swift // koin // // Created by JOOMINKYUNG on 8/28/24. @@ -8,7 +8,7 @@ import Combine import UIKit -class ModalViewController: UIViewController { +class KoinModalViewController: UIViewController { // MARK: - Properties let onLeftButtonTapped: (()->Void)? @@ -30,7 +30,7 @@ class ModalViewController: UIViewController { private let subMessageLabel = UILabel().then { $0.numberOfLines = 0 } - + private let closeButton = UIButton().then { $0.backgroundColor = UIColor.appColor(.neutral0) $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor @@ -84,6 +84,8 @@ class ModalViewController: UIViewController { self.titleColor = titleColor self.subTitleColor = subTitleColor self.rightButton.setTitle(rightButtonText, for: .normal) + + configureTransition() } convenience init( @@ -125,45 +127,23 @@ class ModalViewController: UIViewController { view.addGestureRecognizer(tapGesture) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - containerView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) - containerView.alpha = 0 - view.backgroundColor = UIColor.clear - - UIView.animate(springDuration: 0.2) { - containerView.transform = .identity - containerView.alpha = 1 - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } - } - // MARK: - Public - func dismissWithAnimation() { - UIView.animate(springDuration: 0.2) { - containerView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) - containerView.alpha = 0 - view.backgroundColor = UIColor.clear - } completion: { [weak self] _ in - self?.dismiss(animated: false) - } - } - @objc func closeButtonTapped() { - onLeftButtonTapped?() - dismissWithAnimation() + dismiss(animated: true) { [weak self] in + self?.onLeftButtonTapped?() + } } @objc func rightButtonTapped() { - onRightButtonTapped() - dismissWithAnimation() + dismiss(animated: true) { [weak self] in + self?.onRightButtonTapped() + } } @objc func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { let location = sender.location(in: view) if !containerView.frame.contains(location) { - dismissWithAnimation() + dismiss(animated: true) } } @@ -234,7 +214,7 @@ class ModalViewController: UIViewController { } } -extension ModalViewController { +extension KoinModalViewController { private func setUpLayOuts() { [containerView].forEach { @@ -275,9 +255,43 @@ extension ModalViewController { } private func configureView() { + view.backgroundColor = UIColor.clear setUpLayOuts() setUpConstraints() updateMessageLabel() updateSubMessageLabel() } } + +extension KoinModalViewController: UIViewControllerTransitioningDelegate { + func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> (any UIViewControllerAnimatedTransitioning)? { + KoinModalAnimator(transitionType: .present) + } + + func animationController( + forDismissed dismissed: UIViewController + ) -> (any UIViewControllerAnimatedTransitioning)? { + KoinModalAnimator(transitionType: .dismiss) + } + + func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController + ) -> UIPresentationController? { + KoinModalPresentationController( + presentedViewController: presented, + presenting: presenting + ) + } + + private func configureTransition() { + modalPresentationStyle = .custom + transitioningDelegate = self + } +} + diff --git a/Koin/Core/View/KoinModalViewController/Transition/KoinModalAnimator.swift b/Koin/Core/View/KoinModalViewController/Transition/KoinModalAnimator.swift new file mode 100644 index 00000000..ca8a2056 --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Transition/KoinModalAnimator.swift @@ -0,0 +1,95 @@ +// +// KoinModalAnimator.swift +// koin +// +// Created by 홍기정 on 8/14/26. +// + +import UIKit + +final class KoinModalAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + enum TransitionType { + case present + case dismiss + } + + // MARK: - Properties + private let duration: TimeInterval = 0.2 + private let deflatedTransition = CGAffineTransform(scaleX: 0.9, y: 0.9) + private let transitionType: TransitionType + + // MARK: - Initializer + init(transitionType: TransitionType) { + self.transitionType = transitionType + } + + // MARK: - Transition + func transitionDuration( + using transitionContext: (any UIViewControllerContextTransitioning)? + ) -> TimeInterval { + duration + } + + func animateTransition( + using transitionContext: any UIViewControllerContextTransitioning + ) { + switch transitionType { + case .present: + animatePresentation(using: transitionContext) + case .dismiss: + animateDismissal(using: transitionContext) + } + } + + private func animatePresentation( + using transitionContext: any UIViewControllerContextTransitioning + ) { + guard + let presentedViewController = transitionContext.viewController(forKey: .to), + let presentedView = transitionContext.view(forKey: .to) + else { + transitionContext.completeTransition(false) + return + } + + let containerView = transitionContext.containerView + presentedView.frame = transitionContext.finalFrame(for: presentedViewController) + containerView.addSubview(presentedView) + + presentedView.alpha = 0 + presentedView.transform = deflatedTransition + + UIView.animate(springDuration: duration) { + presentedView.alpha = 1 + presentedView.transform = .identity + } completion: { _ in + let didComplete = !transitionContext.transitionWasCancelled + if !didComplete { + presentedView.removeFromSuperview() + } + transitionContext.completeTransition(didComplete) + } + } + + private func animateDismissal( + using transitionContext: any UIViewControllerContextTransitioning + ) { + guard let presentedView = transitionContext.view(forKey: .from) else { + transitionContext.completeTransition(false) + return + } + + UIView.animate(springDuration: duration) { + presentedView.alpha = 0 + presentedView.transform = self.deflatedTransition + } completion: { _ in + let didComplete = !transitionContext.transitionWasCancelled + if !didComplete { + presentedView.alpha = 1 + presentedView.transform = .identity + } + transitionContext.completeTransition(didComplete) + } + } +} diff --git a/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift b/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift new file mode 100644 index 00000000..86979962 --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift @@ -0,0 +1,81 @@ +// +// KoinModalPresentationController.swift +// koin +// +// Created by 홍기정 on 8/14/26. +// + +import UIKit +import Then + +final class KoinModalPresentationController: UIPresentationController { + + // MARK: - UI Components + private let dimmingView = UIView().then { + $0.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) + $0.alpha = 0 + } + + // MARK: - Layout + override var frameOfPresentedViewInContainerView: CGRect { + containerView?.bounds ?? .zero + } + + override func containerViewWillLayoutSubviews() { + super.containerViewWillLayoutSubviews() + dimmingView.frame = containerView?.bounds ?? .zero + presentedView?.frame = frameOfPresentedViewInContainerView + } + + // MARK: - Present + override func presentationTransitionWillBegin() { + super.presentationTransitionWillBegin() + + guard let containerView else { return } + + dimmingView.frame = containerView.bounds +// containerView.insertSubview(dimmingView, at: 0) + containerView.addSubview(dimmingView) + + guard let transitionCoordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 1 + return + } + + transitionCoordinator.animate { [weak self] _ in + self?.dimmingView.alpha = 1 + } + } + + override func presentationTransitionDidEnd(_ completed: Bool) { + super.presentationTransitionDidEnd(completed) + + if !completed { + dimmingView.removeFromSuperview() + } + } + + // MARK: - Dismiss + override func dismissalTransitionWillBegin() { + super.dismissalTransitionWillBegin() + + guard let transitionCoordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 0 + return + } + + transitionCoordinator.animate { [weak self] _ in + self?.dimmingView.alpha = 0 + } + } + + override func dismissalTransitionDidEnd(_ completed: Bool) { + super.dismissalTransitionDidEnd(completed) + + if completed { + dimmingView.removeFromSuperview() + } else { + dimmingView.alpha = 1 + } + } +} diff --git a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift index fee5a923..fb12f317 100644 --- a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift @@ -152,8 +152,7 @@ extension BusSearchResultViewController { subTitleColor: .gray ) busSearchDatePickerViewController.setPickerItems(items: datePickerData.0, selectedItems: datePickerData.1) - busSearchDatePickerViewController.modalPresentationStyle = .overFullScreen - present(busSearchDatePickerViewController, animated: false) + present(busSearchDatePickerViewController, animated: true) } } diff --git a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift index e42d3f56..3db9eb3b 100644 --- a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift @@ -7,7 +7,7 @@ import UIKit -final class BusSearchDatePickerViewController: ModalViewController { +final class BusSearchDatePickerViewController: KoinModalViewController { // MARK: - Properties private let onDepartureNowTapped: () -> Void @@ -63,7 +63,7 @@ final class BusSearchDatePickerViewController: ModalViewController { onPickerItemsSelected(selectedItems) onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) pickerView.changeSelectedItemPublisher.send(nil) - dismissWithAnimation() + dismiss(animated: true) } override func closeButtonTapped() { @@ -81,7 +81,7 @@ final class BusSearchDatePickerViewController: ModalViewController { pickerView.setSelectedData(selectedItem: selectedItems) onPickerItemsSelected(selectedItems) pickerView.changeSelectedItemPublisher.send(nil) - dismissWithAnimation() + dismiss(animated: true) } // MARK: - Public diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/Chat/Chat/ChatViewController.swift index 79f111e1..538f8ed1 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/Chat/Chat/ChatViewController.swift @@ -173,7 +173,7 @@ extension ChatViewController{ } private func presentBlockUserConfirmationModal() { - let modalViewController = ModalViewController( + let modalViewController = KoinModalViewController( onRightButtonTapped: { [weak self] in self?.inputSubject.send(.blockUser) }, @@ -186,8 +186,7 @@ extension ChatViewController{ subTitleColor: .appColor(.gray), rightButtonText: "차단하기" ) - modalViewController.modalTransitionStyle = .crossDissolve - present(modalViewController, animated: false) + present(modalViewController, animated: true) } @objc private func keyboardWillShow(_ notification: Notification) { diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift index 5fb8cc88..1e6cdaa4 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift @@ -246,8 +246,8 @@ extension LostItemDataViewController { self?.inputSubject.send(.deleteData) self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.findUserDeleteConfirm, EventParameter.EventCategory.click, "확인")) } - let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") - navigationController?.present(modalViewController, animated: false) + let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + navigationController?.present(modalViewController, animated: true) } private func navigateToEdit() { @@ -269,9 +269,8 @@ extension LostItemDataViewController { inputSubject.send(.changeState(viewModel.id)) inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.lostItemFound, .click, "\(type.description)물")) } - let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") - modalViewController.modalTransitionStyle = .crossDissolve - navigationController?.present(modalViewController, animated: false) + let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + navigationController?.present(modalViewController, animated: true) } private func navigateToChat(_ createChatRoomResponse: CreateChatRoomResponse) { @@ -310,9 +309,8 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = ModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "쪽지를 보내려면\n로그인이 필요해요.", subTitle: "로그인 후 대화를 시작하세요!", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) - modalViewController.modalTransitionStyle = .crossDissolve - navigationController?.present(modalViewController, animated: false) + let modalViewController = KoinModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "쪽지를 보내려면\n로그인이 필요해요.", subTitle: "로그인 후 대화를 시작하세요!", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + navigationController?.present(modalViewController, animated: true) } private func showLoginToReportModal() { @@ -336,9 +334,8 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = ModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 신고하려면\n로그인이 필요해요.", subTitle: "로그인 후 이용해주세요.", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) - modalViewController.modalTransitionStyle = .crossDissolve - navigationController?.present(modalViewController, animated: false) + let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 신고하려면\n로그인이 필요해요.", subTitle: "로그인 후 이용해주세요.", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + navigationController?.present(modalViewController, animated: true) } private func navigateToReport() { diff --git a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift index e52d9ea4..dea7ef55 100644 --- a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift +++ b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift @@ -195,7 +195,7 @@ extension LostItemKeywordViewController { let onRightButtonTapped: ()->Void = { [weak self] in self?.navigateToLogin() } - let viewController = ModalViewController( + let viewController = KoinModalViewController( onRightButtonTapped: onRightButtonTapped, width: 301, height: 228, @@ -205,8 +205,7 @@ extension LostItemKeywordViewController { titleColor: .appColor(.neutral800), subTitleColor: .appColor(.gray) ) - viewController.modalTransitionStyle = .crossDissolve - navigationController?.present(viewController, animated: false) + navigationController?.present(viewController, animated: true) } private func updateMyKeywordCountLabel(_ count: Int) { diff --git a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift index 3b6e120a..4743cf3f 100644 --- a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift +++ b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift @@ -227,8 +227,8 @@ extension LostItemListViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let loginModalViewController = ModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 작성하려면\n로그인이 필요해요.", subTitle: "로그인 후 글을 작성해주세요!", titleColor: UIColor.appColor(.neutral700), subTitleColor: UIColor.appColor(.gray)) - navigationController?.present(loginModalViewController, animated: false) + let loginModalViewController = KoinModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 작성하려면\n로그인이 필요해요.", subTitle: "로그인 후 글을 작성해주세요!", titleColor: UIColor.appColor(.neutral700), subTitleColor: UIColor.appColor(.gray)) + navigationController?.present(loginModalViewController, animated: true) } private func presentPostTypeModal() { diff --git a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift index 2361028e..3f31189a 100644 --- a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift +++ b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift @@ -213,7 +213,7 @@ extension ManageNoticeKeywordViewController { } private func presentLoginModal() { - let modalViewController = ModalViewController( + let modalViewController = KoinModalViewController( onLeftButtonTapped: { [weak self] in self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPopupKeyword, .click, "닫기")) }, @@ -229,8 +229,7 @@ extension ManageNoticeKeywordViewController { titleColor: .appColor(.neutral700), subTitleColor: .appColor(.gray) ) - - present(modalViewController, animated: false) + present(modalViewController, animated: true) } override func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a0a0f5bc..a164f206 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ 7C17EEB02EBF708B008BCA89 /* ShopSearchDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C17EEAF2EBF708B008BCA89 /* ShopSearchDto.swift */; }; 7C17EEB22EBF7175008BCA89 /* FetchSearchShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C17EEB12EBF7175008BCA89 /* FetchSearchShopUseCase.swift */; }; 7C281CFD2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C281CFC2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift */; }; + 7C333E7C302E285B009D2B89 /* KoinModalAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E7B302E285B009D2B89 /* KoinModalAnimator.swift */; }; + 7C333E7E302E28AA009D2B89 /* KoinModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E7D302E28AA009D2B89 /* KoinModalPresentationController.swift */; }; 7C372ED92F1B7F3900149729 /* FetchLostItemListRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372ED82F1B7F3900149729 /* FetchLostItemListRequest.swift */; }; 7C372EDD2F1B8DC900149729 /* LostItemListFilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372EDC2F1B8DC900149729 /* LostItemListFilterButton.swift */; }; 7C372EE62F1C0CC400149729 /* LostItemListData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372EE52F1C0CC400149729 /* LostItemListData.swift */; }; @@ -682,7 +684,7 @@ 8354BE522C7A0566009D4D7A /* CoreDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354BE512C7A0566009D4D7A /* CoreDataService.swift */; }; 835C99172C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C99162C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift */; }; 835C991A2C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C99192C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift */; }; - 835C991E2C7F1256002E02D3 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C991D2C7F1256002E02D3 /* ModalViewController.swift */; }; + 835C991E2C7F1256002E02D3 /* KoinModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */; }; 835DC1F92C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC1F82C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift */; }; 835DC1FB2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC1FA2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift */; }; 835DC2042C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC2032C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift */; }; @@ -1083,6 +1085,8 @@ 7C17EEAF2EBF708B008BCA89 /* ShopSearchDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSearchDto.swift; sourceTree = ""; }; 7C17EEB12EBF7175008BCA89 /* FetchSearchShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSearchShopUseCase.swift; sourceTree = ""; }; 7C281CFC2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopDetailFromShopUseCase.swift; sourceTree = ""; }; + 7C333E7B302E285B009D2B89 /* KoinModalAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalAnimator.swift; sourceTree = ""; }; + 7C333E7D302E28AA009D2B89 /* KoinModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalPresentationController.swift; sourceTree = ""; }; 7C372ED82F1B7F3900149729 /* FetchLostItemListRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchLostItemListRequest.swift; sourceTree = ""; }; 7C372EDC2F1B8DC900149729 /* LostItemListFilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemListFilterButton.swift; sourceTree = ""; }; 7C372EE52F1C0CC400149729 /* LostItemListData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemListData.swift; sourceTree = ""; }; @@ -1688,7 +1692,7 @@ 8354BE512C7A0566009D4D7A /* CoreDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataService.swift; sourceTree = ""; }; 835C99162C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNotificationKeywordUseCase.swift; sourceTree = ""; }; 835C99192C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchNotificationKeywordUseCase.swift; sourceTree = ""; }; - 835C991D2C7F1256002E02D3 /* ModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = ""; }; + 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalViewController.swift; sourceTree = ""; }; 835DC1F82C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRecommendedKeywordUseCase.swift; sourceTree = ""; }; 835DC1FA2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchHotSearchingKeywordUseCase.swift; sourceTree = ""; }; 835DC2032C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRecentSearchedWordUseCase.swift; sourceTree = ""; }; @@ -2186,6 +2190,24 @@ path = CoreData; sourceTree = ""; }; + 7C333E7A302E280C009D2B89 /* KoinModalViewController */ = { + isa = PBXGroup; + children = ( + 7C333E7F302E317C009D2B89 /* Transition */, + 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */, + ); + path = KoinModalViewController; + sourceTree = ""; + }; + 7C333E7F302E317C009D2B89 /* Transition */ = { + isa = PBXGroup; + children = ( + 7C333E7B302E285B009D2B89 /* KoinModalAnimator.swift */, + 7C333E7D302E28AA009D2B89 /* KoinModalPresentationController.swift */, + ); + path = Transition; + sourceTree = ""; + }; 7C372ED72F1B7F3100149729 /* LostItem */ = { isa = PBXGroup; children = ( @@ -5122,7 +5144,7 @@ 7CED8ED32EF2D1F900457128 /* CancelableImageView.swift */, 7CC42B062F56A7E900940CE1 /* ZoomedImageViewController */, 7CA0D6172EC744E700E9A282 /* ZoomedImageViewControllerB */, - 835C991D2C7F1256002E02D3 /* ModalViewController.swift */, + 7C333E7A302E280C009D2B89 /* KoinModalViewController */, D2FB23442BFB6D090098BA2B /* CustomNavigationController.swift */, D20AB96E2C574291006BC684 /* BottomSheetViewController.swift */, 7CC42B822F5D307C00940CE1 /* BottomSheetViewControllerB.swift */, @@ -6455,6 +6477,7 @@ D2D422722BB2A7BE00A8AF04 /* Confirmable.swift in Sources */, 7CA60D672F6619F4007C08C1 /* CallVanService.swift in Sources */, 7CC42B8F2F5DCE7400940CE1 /* CallVanChatViewController.swift in Sources */, + 7C333E7C302E285B009D2B89 /* KoinModalAnimator.swift in Sources */, 7C58FEAB2F6020A900A2737B /* CallVanReportImagesCollectionView.swift in Sources */, D29F21FC2C508A9C00994554 /* CheckPasswordRequest.swift in Sources */, D85939772DC12D0900CE4CB2 /* CheckDuplicatedIdRequest.swift in Sources */, @@ -6538,7 +6561,7 @@ 7C58FEAD2F6020AD00A2737B /* CallVanReportImagesCollectionViewCell.swift in Sources */, 7C016548300AA1520013DD7B /* DepartmentCategoryViewModel.swift in Sources */, D249CD282C12DCCC000813F9 /* DiningService.swift in Sources */, - 835C991E2C7F1256002E02D3 /* ModalViewController.swift in Sources */, + 835C991E2C7F1256002E02D3 /* KoinModalViewController.swift in Sources */, D2DA83952C181A3B00A2F156 /* FetchShopEventListUseCase.swift in Sources */, D29F21F82C5085B900994554 /* RefreshTokenRequest.swift in Sources */, 7CF77B3A300254BE003642C1 /* ProfileHostingController.swift in Sources */, @@ -6578,6 +6601,7 @@ D2FB23452BFB6D090098BA2B /* CustomNavigationController.swift in Sources */, D21572812CEF062A0061E725 /* DeleteSemesterUseCase.swift in Sources */, 7C6790B52FCC5B460045E163 /* ImageAsset.swift in Sources */, + 7C333E7E302E28AA009D2B89 /* KoinModalPresentationController.swift in Sources */, D874DB4F2DBF1FEE0098EED0 /* SendVerificationCodeRequest.swift in Sources */, 7CA60D772F663F7B007C08C1 /* PostNotificationReadUseCase.swift in Sources */, 7C8A941E2FD0A94F00DEA6F5 /* NotificationViewModel.swift in Sources */, From 2ba4ad8e34c5e2a7031865c063d379cc4447045c Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 16 Aug 2026 20:42:06 +0900 Subject: [PATCH 032/101] =?UTF-8?q?refactor:=20KoinModalViewController=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalConfiguration.swift | 216 ++++++++++++ .../Configuration/KoinModalStyle.swift | 79 +++++ .../KoinModalViewController.swift | 333 ++++++------------ .../Subviews/ModalButtonView.swift | 129 +++++++ .../Subviews/ModalContentView.swift | 144 ++++++++ .../Chat/Chat/ChatViewController.swift | 27 +- .../LostItemDataViewController.swift | 47 ++- .../LostItemKeywordViewController.swift | 24 +- .../LostItemListViewController.swift | 16 +- .../ManageNoticeKeywordViewController.swift | 41 ++- koin.xcodeproj/project.pbxproj | 40 ++- 11 files changed, 810 insertions(+), 286 deletions(-) create mode 100644 Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift create mode 100644 Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift create mode 100644 Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift create mode 100644 Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift diff --git a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift new file mode 100644 index 00000000..63293038 --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift @@ -0,0 +1,216 @@ +// +// KoinModalConfiguration.swift +// koin +// +// Created by 홍기정 on 8/15/26. +// + +import UIKit + +struct KoinModalConfiguration { + let appearance: Appearance + let content: Content + let button: Button? + let layout: Layout + + init( + appearance: Appearance, + content: Content, + button: Button?, + layout: Layout = .init() + ) { + self.appearance = appearance + self.content = content + self.button = button + self.layout = layout + } + + enum Appearance { + case primary + case new + case destructive + } + + enum Content { + case titles( + mainTitleText: String, + mainTitleStyle: KoinModalStyle.TitleStyle? = nil, + subTitleText: String, + subTitleStyle : KoinModalStyle.TitleStyle? = nil + ) + case singleTitle( + text: String, + style: KoinModalStyle.TitleStyle? = nil + ) + case attributedTitles( + mainTitle: NSAttributedString, + subTitle: NSAttributedString + ) + case attributedSingleTitle( + title: NSAttributedString + ) + case custom( + customView: UIView + ) + } + + struct Button { + let leftButtonTitle: String + let leftButtonAction: (()->Void)? + let leftButtonStyle: KoinModalStyle.ButtonStyle? + let rightButtonTitle: String + let rightButtonAction: ()->Void + let rightButtonStyle: KoinModalStyle.ButtonStyle? + + init( + leftButtonTitle: String, + leftButtonAction: (() -> Void)? = nil, + leftButtonStyle: KoinModalStyle.ButtonStyle? = nil, + rightButtonTitle: String, + rightButtonAction: @escaping () -> Void, + rightButtonStyle: KoinModalStyle.ButtonStyle? = nil + ) { + self.leftButtonTitle = leftButtonTitle + self.leftButtonAction = leftButtonAction + self.leftButtonStyle = leftButtonStyle + self.rightButtonTitle = rightButtonTitle + self.rightButtonAction = rightButtonAction + self.rightButtonStyle = rightButtonStyle + } + } + + struct Layout { + let width: CGFloat + let contentTopPadding: CGFloat + let contentHorizontalPadding: CGFloat + let contentBottomPadding: CGFloat + let paddingBetweenContentAndButton: CGFloat + let buttonHorizontalPadding: CGFloat + let buttonBottomPadding: CGFloat + + init( + width: CGFloat = 301, + contentTopPadding: CGFloat = 24, + contentHorizontalPadding: CGFloat = 32, + contentBottomPadding: CGFloat = 0, + paddingBetweenContentAndButton: CGFloat = 24, + buttonHorizontalPadding: CGFloat = 32, + buttonBottomPadding: CGFloat = 24 + ) { + self.width = width + self.contentTopPadding = contentTopPadding + self.contentHorizontalPadding = contentHorizontalPadding + self.contentBottomPadding = contentBottomPadding + self.paddingBetweenContentAndButton = paddingBetweenContentAndButton + self.buttonHorizontalPadding = buttonHorizontalPadding + self.buttonBottomPadding = buttonBottomPadding + } + } +} + +extension KoinModalConfiguration { + var style: KoinModalStyle { + switch appearance { + case .primary: + KoinModalStyle( + mainTitle: .init( + textColor: .neutral700, + font: .pretendardMedium, + fontSize: 18 + ), + subTitle: .init( + textColor: .neutral500, + font: .pretendardRegular, + fontSize: 14 + ), + singleTitle: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 14 + ), + leftButton: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 15, + borderColor: .neutral500, + borderWidth: 1, + cornerRadius: 8 + ), + rightButton: .init( + textColor: .neutral0, + font: .pretendardMedium, + fontSize: 15, + backgroundColor: .primary500, + cornerRadius: 8 + ) + ) + case .new: + KoinModalStyle( + mainTitle: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 18), + subTitle: .init( + textColor: .neutral500, + font: .pretendardRegular, + fontSize: 14 + ), + singleTitle: .init( + textColor: .neutral600, + font: .pretendardRegular, + fontSize: 15 + ), + leftButton: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 15, + borderColor: .neutral400, + borderWidth: 1, + cornerRadius: 6 + ), + rightButton: .init( + textColor: .neutral0, + font: .pretendardMedium, + fontSize: 15, + backgroundColor: .new500, + cornerRadius: 6 + ) + ) + case .destructive: + KoinModalStyle( + mainTitle: .init( + textColor: .neutral700, + font: .pretendardMedium, + fontSize: 18 + ), + subTitle: .init( + textColor: .neutral500, + font: .pretendardRegular, + fontSize: 14 + ), + singleTitle: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 14 + ), + leftButton: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 15, + borderColor: .neutral500, + borderWidth: 1, + cornerRadius: 4 + ), + rightButton: .init( + textColor: .neutral0, + font: .pretendardMedium, + fontSize: 15, + backgroundColor: .danger700, + cornerRadius: 4 + ) + ) + } + } +} + + diff --git a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift new file mode 100644 index 00000000..4417f2d0 --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift @@ -0,0 +1,79 @@ +// +// KoinModalStyle.swift +// koin +// +// Created by 홍기정 on 8/15/26. +// + +import UIKit + +struct KoinModalStyle { + let mainTitle: TitleStyle + let subTitle: TitleStyle + let singleTitle: TitleStyle + let leftButton: ButtonStyle + let rightButton: ButtonStyle + + struct TitleStyle { + let textColor: ColorAsset + let font: FontAsset + let fontSize: Int + } + + struct ButtonStyle { + let textColor: ColorAsset + let font: FontAsset + let fontSize: Int + let backgroundColor: ColorAsset? + let borderColor: ColorAsset? + let borderWidth: CGFloat? + let cornerRadius: CGFloat? + + init( + textColor: ColorAsset, + font: FontAsset, + fontSize: Int + ) { + self.textColor = textColor + self.font = font + self.fontSize = fontSize + self.backgroundColor = nil + self.borderColor = nil + self.borderWidth = nil + self.cornerRadius = nil + } + + init( + textColor: ColorAsset, + font: FontAsset, + fontSize: Int, + backgroundColor: ColorAsset, + cornerRadius: CGFloat + ) { + self.textColor = textColor + self.font = font + self.fontSize = fontSize + self.backgroundColor = backgroundColor + self.borderColor = nil + self.borderWidth = nil + self.cornerRadius = cornerRadius + } + + init( + textColor: ColorAsset, + font: FontAsset, + fontSize: Int, + borderColor: ColorAsset, + borderWidth: CGFloat, + cornerRadius: CGFloat + ) { + self.textColor = textColor + self.font = font + self.fontSize = fontSize + self.backgroundColor = nil + self.borderColor = borderColor + self.borderWidth = borderWidth + self.cornerRadius = cornerRadius + } + } +} diff --git a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index 17800ee4..c6acd0db 100644 --- a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -2,116 +2,34 @@ // KoinModalViewController.swift // koin // -// Created by JOOMINKYUNG on 8/28/24. +// Created by 홍기정 on 8/15/26. // -import Combine import UIKit +import Combine +import SnapKit +import Then class KoinModalViewController: UIViewController { // MARK: - Properties - let onLeftButtonTapped: (()->Void)? - let onRightButtonTapped: ()->Void - - var containerWidth: CGFloat = 0 - var containerHeight: CGFloat = 0 - var paddingBetweenLabels: CGFloat = 0 - var titleText: String = "" - var subTitleText: String? - var titleColor: UIColor = .black - var subTitleColor: UIColor? = .black + private let configuration: KoinModalConfiguration + private var subscriptions = Set() // MARK: - UI Components - private let messageLabel = UILabel().then { - $0.numberOfLines = 0 - } - - private let subMessageLabel = UILabel().then { - $0.numberOfLines = 0 - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("닫기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let rightButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 8 - view.layer.masksToBounds = true - return view - }() - - private var contentViewInContainer: UIView? + private let containerView = UIView() + private var contentView: ModalContentView + private let buttonView: ModalButtonView // MARK: - Initializer - init( - onLeftButtonTapped: (()->Void)? = nil, - onRightButtonTapped: @escaping ()->Void, - width: CGFloat, - height: CGFloat, - paddingBetweenLabels: CGFloat, - title: String, - subTitle: String?, - titleColor: UIColor, - subTitleColor: UIColor?, - rightButtonText: String = "로그인하기" - ) { - self.onLeftButtonTapped = onLeftButtonTapped - self.onRightButtonTapped = onRightButtonTapped + init(configuration: KoinModalConfiguration) { + self.configuration = configuration + self.contentView = ModalContentView(configuration: configuration) + self.buttonView = ModalButtonView(configuration: configuration) super.init(nibName: nil, bundle: nil) - self.containerWidth = width - self.containerHeight = height - self.paddingBetweenLabels = paddingBetweenLabels - self.titleText = title - self.subTitleText = subTitle - self.titleColor = titleColor - self.subTitleColor = subTitleColor - self.rightButton.setTitle(rightButtonText, for: .normal) - configureTransition() } - convenience init( - onLeftButtonTapped: (()->Void)? = nil, - onRightButtonTapped: @escaping ()->Void, - width: CGFloat, - height: CGFloat, - title: String, - titleColor: UIColor, - rightButtonText: String = "로그인하기" - ) { - - self.init( - onLeftButtonTapped: onLeftButtonTapped, - onRightButtonTapped: onRightButtonTapped, - width: width, - height: height, - paddingBetweenLabels: 0, - title: title, - subTitle: nil, - titleColor: titleColor, - subTitleColor: nil, - rightButtonText: rightButtonText - ) - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -119,151 +37,33 @@ class KoinModalViewController: UIViewController { // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() + setUpGestureRecognizer() configureView() - rightButton.addTarget(self, action: #selector(rightButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOutsideOfContainerView)) - view.addGestureRecognizer(tapGesture) - } - - // MARK: - Public - @objc func closeButtonTapped() { - dismiss(animated: true) { [weak self] in - self?.onLeftButtonTapped?() - } - } - - @objc func rightButtonTapped() { - dismiss(animated: true) { [weak self] in - self?.onRightButtonTapped() - } - } - - @objc func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { - let location = sender.location(in: view) - if !containerView.frame.contains(location) { - dismiss(animated: true) - } - } - - func updaterightButton(buttonColor: UIColor = .appColor(.primary500), borderWidth: CGFloat, title: String) { - rightButton.backgroundColor = buttonColor - rightButton.layer.borderWidth = borderWidth - rightButton.setTitle(title, for: .normal) + bind() } - func updateCloseButton(buttonColor: UIColor = .systemBackground, borderWidth: CGFloat, title: String) { - closeButton.backgroundColor = buttonColor - closeButton.layer.borderWidth = borderWidth - closeButton.setTitle(title, for: .normal) - } - - func updateMessageLabel(font: UIFont = .appFont(.pretendardMedium, size: 18), alignment: NSTextAlignment = .center, title: String? = nil) { - if let title = title { titleText = title } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 8 - paragraphStyle.alignment = alignment - let attributedString = NSMutableAttributedString(string: titleText) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: titleText.count)) - attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: titleText.count)) - attributedString.addAttribute(.foregroundColor, value: titleColor, range: NSRange(location: 0, length: titleText.count)) + // MARK: - Bind + private func bind() { + buttonView.leftButtonTappedPublisher.sink { [weak self] in + self?.dismiss(animated: true) { [weak self] in + self?.configuration.button?.leftButtonAction?() + } + }.store(in: &subscriptions) - messageLabel.attributedText = attributedString - } - - func updateSubMessageLabel(font: UIFont = .appFont(.pretendardRegular, size: 14), alignment: NSTextAlignment = .center, title: String? = nil) { - if let title = title { subTitleText = title } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - paragraphStyle.alignment = alignment - - if let subTitleText { - let attributedString = NSMutableAttributedString(string: subTitleText) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: subTitleText.count)) - attributedString.addAttribute(.foregroundColor, value: subTitleColor ?? .black, range: NSRange(location: 0, length: subTitleText.count)) - - subMessageLabel.attributedText = attributedString - } - } - - func setContentViewInContainer(view: UIView, frame: CGRect) { - self.contentViewInContainer = view - self.contentViewInContainer?.frame = frame - - guard let contentViewInContainer = contentViewInContainer else { return } - containerView.addSubview(contentViewInContainer) - contentViewInContainer.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.trailing.equalToSuperview() - make.height.equalTo(contentViewInContainer.frame.height) - } - closeButton.snp.remakeConstraints { make in - make.top.equalTo(contentViewInContainer.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - rightButton.snp.remakeConstraints { make in - make.top.equalTo(contentViewInContainer.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } + buttonView.rightButtonTappedPublisher.sink { [weak self] in + self?.dismiss(animated: true) { [weak self] in + self?.configuration.button?.rightButtonAction() + } + }.store(in: &subscriptions) } } -extension KoinModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, closeButton, rightButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(containerWidth) - make.height.equalTo(containerHeight) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.leading.trailing.equalToSuperview().inset(24) - } - subMessageLabel.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(paddingBetweenLabels) - make.leading.trailing.equalToSuperview().inset(24) - } - closeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - rightButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } +extension KoinModalViewController: UIViewControllerTransitioningDelegate { + private func configureTransition() { + modalPresentationStyle = .custom + transitioningDelegate = self } - private func configureView() { - view.backgroundColor = UIColor.clear - setUpLayOuts() - setUpConstraints() - updateMessageLabel() - updateSubMessageLabel() - } -} - -extension KoinModalViewController: UIViewControllerTransitioningDelegate { func animationController( forPresented presented: UIViewController, presenting: UIViewController, @@ -288,10 +88,75 @@ extension KoinModalViewController: UIViewControllerTransitioningDelegate { presenting: presenting ) } +} + +extension KoinModalViewController { + private func setUpGestureRecognizer() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOutsideOfContainerView)) + view.addGestureRecognizer(tapGesture) + } - private func configureTransition() { - modalPresentationStyle = .custom - transitioningDelegate = self + @objc private func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { + let location = sender.location(in: view) + if !containerView.frame.contains(location) { + dismiss(animated: true) + } } } +extension KoinModalViewController { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + view.backgroundColor = .clear + + containerView.do { + $0.backgroundColor = .appColor(.neutral0) + $0.layer.cornerRadius = 8 + } + } + + private func setUpLayouts() { + [contentView, buttonView].forEach { + containerView.addSubview($0) + } + [containerView].forEach { + view.addSubview($0) + } + + if configuration.button == nil { + buttonView.removeFromSuperview() + } + } + private func setUpConstraints() { + if let _ = configuration.button { + contentView.snp.makeConstraints { + $0.top.equalToSuperview().offset(configuration.layout.contentTopPadding) + $0.leading.equalToSuperview().offset(configuration.layout.contentHorizontalPadding) + $0.trailing.equalToSuperview().offset(-configuration.layout.contentHorizontalPadding) + } + buttonView.snp.makeConstraints { + $0.top.equalTo(contentView.snp.bottom).offset(configuration.layout.paddingBetweenContentAndButton) + $0.leading.equalToSuperview().offset(configuration.layout.buttonHorizontalPadding) + $0.trailing.equalToSuperview().offset(-configuration.layout.buttonHorizontalPadding) + $0.bottom.equalToSuperview().offset(-configuration.layout.buttonBottomPadding) + } + } else { + contentView.snp.makeConstraints { + $0.top.equalToSuperview().offset(configuration.layout.contentTopPadding) + $0.leading.equalToSuperview().offset(configuration.layout.contentHorizontalPadding) + $0.trailing.equalToSuperview().offset(-configuration.layout.contentHorizontalPadding) + $0.bottom.equalToSuperview().offset(-configuration.layout.contentBottomPadding) + } + } + + containerView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.equalTo(configuration.layout.width) + } + } +} diff --git a/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift b/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift new file mode 100644 index 00000000..2bccd40a --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift @@ -0,0 +1,129 @@ +// +// ModalButtonView.swift +// koin +// +// Created by 홍기정 on 8/15/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +final class ModalButtonView: UIView { + + private enum Layout { + static let PaddingBetweenButtons: CGFloat = 8 + static let ButtonHeight: CGFloat = 48 + } + + // MARK: - Properties + private let configuration: KoinModalConfiguration + let leftButtonTappedPublisher = PassthroughSubject() + let rightButtonTappedPublisher = PassthroughSubject() + + // MARK: - UI Components + private let leftButton = UIButton() + private let rightButton = UIButton() + + // MARK: - Initializer + init(configuration: KoinModalConfiguration) { + self.configuration = configuration + super.init(frame: .zero) + + configureView() + setUpAddTargets() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ModalButtonView { + + // MARK: - Objc + @objc private func onLeftButtonTapped() { + leftButtonTappedPublisher.send() + } + @objc private func onRightButtonTapped() { + rightButtonTappedPublisher.send() + } +} + +extension ModalButtonView { + private func setUpAddTargets() { + leftButton.addTarget(self, action: #selector(onLeftButtonTapped), for: .touchUpInside) + rightButton.addTarget(self, action: #selector(onRightButtonTapped), for: .touchUpInside) + } +} + +extension ModalButtonView { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + guard let button = configuration.button else { return } + + let leftButtonTitle = button.leftButtonTitle + let leftButtonStyle = button.leftButtonStyle ?? configuration.style.leftButton + let rightButtonTitle = button.rightButtonTitle + let rightButtonStyle = button.rightButtonStyle ?? configuration.style.rightButton + + apply(title: leftButtonTitle, style: leftButtonStyle, to: leftButton) + apply(title: rightButtonTitle, style: rightButtonStyle, to: rightButton) + } + + private func setUpLayouts() { + [leftButton, rightButton].forEach { + addSubview($0) + } + } + + private func setUpConstraints() { + leftButton.snp.makeConstraints { + $0.height.equalTo(Layout.ButtonHeight) + $0.top.leading.bottom.equalToSuperview() + } + rightButton.snp.makeConstraints { + $0.height.equalTo(Layout.ButtonHeight) + $0.width.equalTo(leftButton.snp.width) + $0.leading.equalTo(leftButton.snp.trailing).offset(Layout.PaddingBetweenButtons) + $0.top.trailing.bottom.equalToSuperview() + } + } +} + +extension ModalButtonView { + private func apply(title: String, style: KoinModalStyle.ButtonStyle, to button: UIButton) { + button.do { + var configuration = UIButton.Configuration.plain() + + configuration.attributedTitle = AttributedString( + title, + attributes: AttributeContainer([ + .font: UIFont.appFont(style.font, size: style.fontSize), + .foregroundColor: UIColor.appColor(style.textColor) + ]) + ) + $0.configuration = configuration + + if let backgroundColor = style.backgroundColor { + $0.backgroundColor = .appColor(backgroundColor) + } + if let borderColor = style.borderColor { + $0.layer.borderColor = UIColor.appColor(borderColor).cgColor + } + if let borderWidth = style.borderWidth { + $0.layer.borderWidth = borderWidth + } + if let cornerRadius = style.cornerRadius { + $0.layer.cornerRadius = cornerRadius + } + + $0.clipsToBounds = true + } + } +} diff --git a/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift b/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift new file mode 100644 index 00000000..d7ef89fa --- /dev/null +++ b/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift @@ -0,0 +1,144 @@ +// +// ModalContentView.swift +// koin +// +// Created by 홍기정 on 8/15/26. +// + +import UIKit +import SnapKit +import Then + +final class ModalContentView: UIView { + private static let paddingBetweenTitles: CGFloat = 8 + + // MARK: - Porperties + private let configuration: KoinModalConfiguration + + // MARK: - UI Components + private let mainTitleLabel = UILabel() + private let subTitleLabel = UILabel() + + private let singleTitleLabel = UILabel() + + private var customView: UIView? + + // MARK: - Initializer + init(configuration: KoinModalConfiguration) { + self.configuration = configuration + super.init(frame: .zero) + configureView() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ModalContentView { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + switch configuration.content { + case .titles(let mainTitleText, let mainTitleStyle, let subTitleText, let subTitleStyle): + let mainTitleStyle = mainTitleStyle ?? configuration.style.mainTitle + let subTitleStyle = subTitleStyle ?? configuration.style.subTitle + apply(text: mainTitleText, style: mainTitleStyle, to: mainTitleLabel) + apply(text: subTitleText, style: subTitleStyle, to: subTitleLabel) + case .singleTitle(let text, let style): + let style = style ?? configuration.style.singleTitle + apply(text: text, style: style, to: singleTitleLabel) + case .attributedTitles(let mainTitle, let subTitle): + apply(title: mainTitle, to: mainTitleLabel) + apply(title: subTitle, to: subTitleLabel) + case .attributedSingleTitle(let title): + apply(title: title, to: singleTitleLabel) + case .custom(let customView): + self.customView = customView + } + } + + private func setUpLayouts() { + switch configuration.content { + case .titles, .attributedTitles: + [mainTitleLabel, subTitleLabel].forEach { + addSubview($0) + } + case .singleTitle, .attributedSingleTitle: + [singleTitleLabel].forEach { + addSubview($0) + } + case .custom(let customView): + [customView].forEach { + addSubview($0) + } + } + } + + private func setUpConstraints() { + switch configuration.content { + case .titles, .attributedTitles: + let mainTitleLabelTopOffset: CGFloat = mainTitleLabel.font.pointSize * 0.4 + mainTitleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(-mainTitleLabelTopOffset) + $0.leading.greaterThanOrEqualToSuperview() + $0.trailing.lessThanOrEqualToSuperview() + $0.centerX.equalToSuperview() + } + subTitleLabel.snp.makeConstraints { + $0.top.equalTo(mainTitleLabel.snp.bottom).offset(Self.paddingBetweenTitles) + $0.leading.greaterThanOrEqualToSuperview() + $0.trailing.lessThanOrEqualToSuperview() + $0.bottom.equalToSuperview() + $0.centerX.equalToSuperview() + } + case .singleTitle, .attributedSingleTitle: + let singleTitleLabelTopOffset: CGFloat = singleTitleLabel.font.pointSize * 0.4 + singleTitleLabel.snp.makeConstraints { + $0.top.bottom.equalToSuperview().offset(-singleTitleLabelTopOffset) + $0.leading.greaterThanOrEqualToSuperview() + $0.trailing.lessThanOrEqualToSuperview() + $0.centerX.equalToSuperview() + } + case .custom: + customView?.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + } +} + +extension ModalContentView { + private func apply( + text: String, + style: KoinModalStyle.TitleStyle, + to label: UILabel + ) { + label.do { + $0.font = .appFont(style.font, size: style.fontSize) + $0.textColor = .appColor(style.textColor) + $0.setLineHeight(lineHeight: 1.6, text: text) + $0.textAlignment = .center + $0.numberOfLines = 0 + } + } + + private func apply( + title attributedString: NSAttributedString, + to label: UILabel + ) { + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) + let paragraphStyle = NSMutableParagraphStyle().then { + $0.lineHeightMultiple = 1.6 + } + let fullRange = { + let text = attributedString.string + return (text as NSString).range(of: text) + }() + mutableAttributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange) + label.attributedText = mutableAttributedString + } +} diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/Chat/Chat/ChatViewController.swift index 538f8ed1..68561f97 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/Chat/Chat/ChatViewController.swift @@ -173,19 +173,20 @@ extension ChatViewController{ } private func presentBlockUserConfirmationModal() { - let modalViewController = KoinModalViewController( - onRightButtonTapped: { [weak self] in - self?.inputSubject.send(.blockUser) - }, - width: 301, - height: 179, - paddingBetweenLabels: 8, - title: "이 사용자를 차단하시겠습니까?", - subTitle: "쪽지 수신 및 발신이 모두 차단됩니다.", - titleColor: .appColor(.neutral700), - subTitleColor: .appColor(.gray), - rightButtonText: "차단하기" - ) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "이 사용자를 차단하시겠습니까?", + subTitleText: "쪽지 수신 및 발신이 모두 차단됩니다." + ), + button: .init( + leftButtonTitle: "닫기", + rightButtonTitle: "차단하기", + rightButtonAction: { [weak self] in + self?.inputSubject.send(.blockUser) + } + ) + )) present(modalViewController, animated: true) } diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift index 1e6cdaa4..97d3514d 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift @@ -246,7 +246,15 @@ extension LostItemDataViewController { self?.inputSubject.send(.deleteData) self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.findUserDeleteConfirm, EventParameter.EventCategory.click, "확인")) } - let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .singleTitle(text: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?"), + button: .init( + leftButtonTitle: "취소", + rightButtonTitle: "확인", + rightButtonAction: onRightButtonTapped + ) + )) navigationController?.present(modalViewController, animated: true) } @@ -269,7 +277,15 @@ extension LostItemDataViewController { inputSubject.send(.changeState(viewModel.id)) inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.lostItemFound, .click, "\(type.description)물")) } - let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 162, title: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?", titleColor: .appColor(.neutral600), rightButtonText: "확인") + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .singleTitle(text: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?"), + button: .init( + leftButtonTitle: "취소", + rightButtonTitle: "확인", + rightButtonAction: onRightButtonTapped + ) + )) navigationController?.present(modalViewController, animated: true) } @@ -309,7 +325,19 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = KoinModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "쪽지를 보내려면\n로그인이 필요해요.", subTitle: "로그인 후 대화를 시작하세요!", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "쪽지를 보내려면\n로그인이 필요해요.", + subTitleText: "로그인 후 대화를 시작하세요!" + ), + button: .init( + leftButtonTitle: "닫기", + leftButtonAction: onLeftButtonTapped, + rightButtonTitle: "로그인하기", + rightButtonAction: onRightButtonTapped + ) + )) navigationController?.present(modalViewController, animated: true) } @@ -334,7 +362,18 @@ extension LostItemDataViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let modalViewController = KoinModalViewController(onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 신고하려면\n로그인이 필요해요.", subTitle: "로그인 후 이용해주세요.", titleColor: .appColor(.neutral600), subTitleColor: .appColor(.gray)) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "게시글을 신고하려면\n로그인이 필요해요.", + subTitleText: "로그인 후 이용해주세요." + ), + button: .init( + leftButtonTitle: "닫기", + rightButtonTitle: "로그인하기", + rightButtonAction: onRightButtonTapped + ) + )) navigationController?.present(modalViewController, animated: true) } diff --git a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift index dea7ef55..47129ffe 100644 --- a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift +++ b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift @@ -195,17 +195,19 @@ extension LostItemKeywordViewController { let onRightButtonTapped: ()->Void = { [weak self] in self?.navigateToLogin() } - let viewController = KoinModalViewController( - onRightButtonTapped: onRightButtonTapped, - width: 301, - height: 228, - paddingBetweenLabels: 16, - title: "키워드 알림을 받으려면\n로그인이 필요해요.", - subTitle: "로그인 후 간편하게 분실물 키워드\n알림을 받아보세요!", - titleColor: .appColor(.neutral800), - subTitleColor: .appColor(.gray) - ) - navigationController?.present(viewController, animated: true) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "키워드 알림을 받으려면\n로그인이 필요해요.", + subTitleText: "로그인 후 간편하게 분실물 키워드\n알림을 받아보세요!" + ), + button: .init( + leftButtonTitle: "닫기", + rightButtonTitle: "로그인하기", + rightButtonAction: onRightButtonTapped + ) + )) + navigationController?.present(modalViewController, animated: true) } private func updateMyKeywordCountLabel(_ count: Int) { diff --git a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift index 4743cf3f..8ef699f6 100644 --- a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift +++ b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift @@ -227,8 +227,20 @@ extension LostItemListViewController { let loginViewController = LoginViewController(viewModel: viewModel) self?.navigationController?.pushViewController(loginViewController, animated: true) } - let loginModalViewController = KoinModalViewController(onLeftButtonTapped: onLeftButtonTapped, onRightButtonTapped: onRightButtonTapped, width: 301, height: 208, paddingBetweenLabels: 16, title: "게시글을 작성하려면\n로그인이 필요해요.", subTitle: "로그인 후 글을 작성해주세요!", titleColor: UIColor.appColor(.neutral700), subTitleColor: UIColor.appColor(.gray)) - navigationController?.present(loginModalViewController, animated: true) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "게시글을 작성하려면\n로그인이 필요해요.", + subTitleText: "로그인 후 글을 작성해주세요!" + ), + button: .init( + leftButtonTitle: "닫기", + leftButtonAction: onLeftButtonTapped, + rightButtonTitle: "로그인하기", + rightButtonAction: onRightButtonTapped + ) + )) + navigationController?.present(modalViewController, animated: true) } private func presentPostTypeModal() { diff --git a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift index 3f31189a..61b7219b 100644 --- a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift +++ b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift @@ -211,27 +211,32 @@ extension ManageNoticeKeywordViewController { private func conductAddKeywordIllegalType(illegalType: String) { showToast(message: illegalType, success: false) } - + private func presentLoginModal() { - let modalViewController = KoinModalViewController( - onLeftButtonTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPopupKeyword, .click, "닫기")) - }, - onRightButtonTapped: { [weak self] in - self?.navigateToLogin() - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPrompt, .click, "키워드 알림 팝업")) - }, - width: 301, - height: 230, - paddingBetweenLabels: 8, - title: "키워드 알림을 받으려면\n로그인이 필요해요.", - subTitle: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!", - titleColor: .appColor(.neutral700), - subTitleColor: .appColor(.gray) - ) + let onLeftButtonTapped: ()->Void = { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPopupKeyword, .click, "닫기")) + } + let onRightButtonTapped = { [weak self] in + self?.navigateToLogin() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.loginPrompt, .click, "키워드 알림 팝업")) + } + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "키워드 알림을 받으려면\n로그인이 필요해요.", + subTitleText: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!" + ), + button: .init( + leftButtonTitle: "닫기", + leftButtonAction: onLeftButtonTapped, + rightButtonTitle: "로그인하기", + rightButtonAction: onRightButtonTapped + ), + layout: .init(width: 301) + )) present(modalViewController, animated: true) } - + override func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let text = textField.text { textField.text = "" diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a164f206..182d24ee 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -60,6 +60,11 @@ 7C281CFD2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C281CFC2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift */; }; 7C333E7C302E285B009D2B89 /* KoinModalAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E7B302E285B009D2B89 /* KoinModalAnimator.swift */; }; 7C333E7E302E28AA009D2B89 /* KoinModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E7D302E28AA009D2B89 /* KoinModalPresentationController.swift */; }; + 7C333E9330301B3D009D2B89 /* KoinModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E9230301B3D009D2B89 /* KoinModalViewController.swift */; }; + 7C333E9530301CB7009D2B89 /* KoinModalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E9430301CB7009D2B89 /* KoinModalConfiguration.swift */; }; + 7C333E9C303026B6009D2B89 /* KoinModalStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333E9B303026B6009D2B89 /* KoinModalStyle.swift */; }; + 7C333EA330303678009D2B89 /* ModalContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333EA230303678009D2B89 /* ModalContentView.swift */; }; + 7C333EA53030429B009D2B89 /* ModalButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C333EA43030429B009D2B89 /* ModalButtonView.swift */; }; 7C372ED92F1B7F3900149729 /* FetchLostItemListRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372ED82F1B7F3900149729 /* FetchLostItemListRequest.swift */; }; 7C372EDD2F1B8DC900149729 /* LostItemListFilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372EDC2F1B8DC900149729 /* LostItemListFilterButton.swift */; }; 7C372EE62F1C0CC400149729 /* LostItemListData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C372EE52F1C0CC400149729 /* LostItemListData.swift */; }; @@ -684,7 +689,6 @@ 8354BE522C7A0566009D4D7A /* CoreDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8354BE512C7A0566009D4D7A /* CoreDataService.swift */; }; 835C99172C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C99162C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift */; }; 835C991A2C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C99192C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift */; }; - 835C991E2C7F1256002E02D3 /* KoinModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */; }; 835DC1F92C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC1F82C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift */; }; 835DC1FB2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC1FA2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift */; }; 835DC2042C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835DC2032C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift */; }; @@ -1087,6 +1091,11 @@ 7C281CFC2EB3ED4B00BD6B4E /* FetchOrderShopDetailFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopDetailFromShopUseCase.swift; sourceTree = ""; }; 7C333E7B302E285B009D2B89 /* KoinModalAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalAnimator.swift; sourceTree = ""; }; 7C333E7D302E28AA009D2B89 /* KoinModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalPresentationController.swift; sourceTree = ""; }; + 7C333E9230301B3D009D2B89 /* KoinModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalViewController.swift; sourceTree = ""; }; + 7C333E9430301CB7009D2B89 /* KoinModalConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalConfiguration.swift; sourceTree = ""; }; + 7C333E9B303026B6009D2B89 /* KoinModalStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalStyle.swift; sourceTree = ""; }; + 7C333EA230303678009D2B89 /* ModalContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalContentView.swift; sourceTree = ""; }; + 7C333EA43030429B009D2B89 /* ModalButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalButtonView.swift; sourceTree = ""; }; 7C372ED82F1B7F3900149729 /* FetchLostItemListRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchLostItemListRequest.swift; sourceTree = ""; }; 7C372EDC2F1B8DC900149729 /* LostItemListFilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemListFilterButton.swift; sourceTree = ""; }; 7C372EE52F1C0CC400149729 /* LostItemListData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemListData.swift; sourceTree = ""; }; @@ -1692,7 +1701,6 @@ 8354BE512C7A0566009D4D7A /* CoreDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataService.swift; sourceTree = ""; }; 835C99162C7DF89A002E02D3 /* DeleteNotificationKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNotificationKeywordUseCase.swift; sourceTree = ""; }; 835C99192C7EDB63002E02D3 /* FetchNotificationKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchNotificationKeywordUseCase.swift; sourceTree = ""; }; - 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinModalViewController.swift; sourceTree = ""; }; 835DC1F82C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRecommendedKeywordUseCase.swift; sourceTree = ""; }; 835DC1FA2C84B96500488506 /* FetchHotSearchingKeywordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchHotSearchingKeywordUseCase.swift; sourceTree = ""; }; 835DC2032C84C39E00488506 /* ManageRecentSearchedWordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRecentSearchedWordUseCase.swift; sourceTree = ""; }; @@ -2193,8 +2201,10 @@ 7C333E7A302E280C009D2B89 /* KoinModalViewController */ = { isa = PBXGroup; children = ( + 7C333E9F3030364F009D2B89 /* Subviews */, + 7C333E82302EC656009D2B89 /* Configuration */, 7C333E7F302E317C009D2B89 /* Transition */, - 835C991D2C7F1256002E02D3 /* KoinModalViewController.swift */, + 7C333E9230301B3D009D2B89 /* KoinModalViewController.swift */, ); path = KoinModalViewController; sourceTree = ""; @@ -2208,6 +2218,24 @@ path = Transition; sourceTree = ""; }; + 7C333E82302EC656009D2B89 /* Configuration */ = { + isa = PBXGroup; + children = ( + 7C333E9430301CB7009D2B89 /* KoinModalConfiguration.swift */, + 7C333E9B303026B6009D2B89 /* KoinModalStyle.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + 7C333E9F3030364F009D2B89 /* Subviews */ = { + isa = PBXGroup; + children = ( + 7C333EA230303678009D2B89 /* ModalContentView.swift */, + 7C333EA43030429B009D2B89 /* ModalButtonView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 7C372ED72F1B7F3100149729 /* LostItem */ = { isa = PBXGroup; children = ( @@ -6165,6 +6193,7 @@ 7C016561300AAD810013DD7B /* DepartmentSearchView.swift in Sources */, 7C016563300AB60B0013DD7B /* DepartmentRow.swift in Sources */, 7C457B452FCE1BA80011D338 /* HomeView.swift in Sources */, + 7C333E9C303026B6009D2B89 /* KoinModalStyle.swift in Sources */, 7C457B492FCE1BA80011D338 /* ShopView.swift in Sources */, 7C8A00012FD000000011D338 /* HomeViewModel.swift in Sources */, 7CED917A2EF2D1F900457128 /* ShopSummaryTableView.swift in Sources */, @@ -6455,6 +6484,7 @@ B68D2CE52EA505AA00F3B479 /* ToastIntent.swift in Sources */, D2FB236E2C0410510098BA2B /* LogAnalyticsService.swift in Sources */, D22C54752C6A2A80000826DA /* PostReviewUseCase.swift in Sources */, + 7C333E9330301B3D009D2B89 /* KoinModalViewController.swift in Sources */, D20AD6EF2E029B250067760A /* CheckVerificationEmailRequest.swift in Sources */, D261BB982D79418600A67F06 /* UserDataManager.swift in Sources */, D20AB96F2C574291006BC684 /* BottomSheetViewController.swift in Sources */, @@ -6508,6 +6538,7 @@ 7CD411EA2FEB92280079467B /* HomeTabBarItem.swift in Sources */, D208A7AC2CA112D0007040E7 /* AssignAbTestResponse.swift in Sources */, D2D462732D63CACC00C60864 /* ChatDetailDto.swift in Sources */, + 7C333EA330303678009D2B89 /* ModalContentView.swift in Sources */, D20AD6FF2E02A4AB0067760A /* FindIdEmailUseCase.swift in Sources */, D215724E2CEDBEC60061E725 /* FrameDto.swift in Sources */, 83DD0D842C4FA7FF00278ABD /* DefaultBusRepository.swift in Sources */, @@ -6541,6 +6572,7 @@ 7CC42B3C2F58284D00940CE1 /* CallVanNotificationTableViewCell.swift in Sources */, D20AD6F52E029F8D0067760A /* CheckVerificationEmailUseCase.swift in Sources */, 7C64040F300F428200A9589F /* DepartmentService.swift in Sources */, + 7C333EA53030429B009D2B89 /* ModalButtonView.swift in Sources */, D22C54712C6A2A60000826DA /* FetchShopReviewUseCase.swift in Sources */, 7C6403D3300BE8A100A9589F /* DepartmentEmptyView.swift in Sources */, 7C8A94222FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift in Sources */, @@ -6561,7 +6593,6 @@ 7C58FEAD2F6020AD00A2737B /* CallVanReportImagesCollectionViewCell.swift in Sources */, 7C016548300AA1520013DD7B /* DepartmentCategoryViewModel.swift in Sources */, D249CD282C12DCCC000813F9 /* DiningService.swift in Sources */, - 835C991E2C7F1256002E02D3 /* KoinModalViewController.swift in Sources */, D2DA83952C181A3B00A2F156 /* FetchShopEventListUseCase.swift in Sources */, D29F21F82C5085B900994554 /* RefreshTokenRequest.swift in Sources */, 7CF77B3A300254BE003642C1 /* ProfileHostingController.swift in Sources */, @@ -6619,6 +6650,7 @@ D22C547B2C6A2A9D000826DA /* ReportReviewUseCase.swift in Sources */, 7C8ADD452F20C3FB00F85BDE /* FetchLostItemListUseCase.swift in Sources */, B68D2CE72EA505B000F3B479 /* ToastMessageView.swift in Sources */, + 7C333E9530301CB7009D2B89 /* KoinModalConfiguration.swift in Sources */, D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */, 83E770FA2D05B5EC00EB3F84 /* BusNoticeDto.swift in Sources */, D2FB23742C0432CD0098BA2B /* GA4AnalyticsRepository.swift in Sources */, From 0b46ad887fdae9676bb04608ebd0e4541780ddc3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 16 Aug 2026 20:42:14 +0900 Subject: [PATCH 033/101] =?UTF-8?q?refactor:=20BusSearchDatePickerViewCont?= =?UTF-8?q?roller=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/View/KoinPickerView.swift | 6 +- .../BusSearchResultViewController.swift | 14 +- .../BusSearchDatePickerViewController.swift | 161 +++++++++++------- 3 files changed, 112 insertions(+), 69 deletions(-) diff --git a/Koin/Core/View/KoinPickerView.swift b/Koin/Core/View/KoinPickerView.swift index 6db1e108..2992959b 100644 --- a/Koin/Core/View/KoinPickerView.swift +++ b/Koin/Core/View/KoinPickerView.swift @@ -24,7 +24,11 @@ final class KoinPickerView: UIView, UIPickerViewDelegate, UIPickerViewDataSource private let pickerView = UIPickerView(frame: .zero) // MARK: - Initialization - init(font: UIFont = .appFont(.pretendardMedium, size: 16), selectedColor: UIColor = .appColor(.primary500), deselectedColor: UIColor = .appColor(.neutral800)) { + init( + font: UIFont = .appFont(.pretendardMedium, size: 16), + selectedColor: UIColor = .appColor(.primary500), + deselectedColor: UIColor = .appColor(.neutral800) + ) { self.font = font self.selectedColor = selectedColor self.deselectedColor = deselectedColor diff --git a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift index fb12f317..a5aa008c 100644 --- a/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/BusSearchResultViewController.swift @@ -130,9 +130,7 @@ extension BusSearchResultViewController { guard let datePickerData = viewModel.datePickerData else { return } let busSearchDatePickerViewController = BusSearchDatePickerViewController( - onDepartureNowTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureNow, .click, "지금 출발")) - }, + subTitle: datePickerSubTitle, onPickerDateChanged: { [weak self] isChanged in let logValue = isChanged != nil ? "Y" : "N" self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureTimeSettingDone, .click, logValue)) @@ -143,13 +141,9 @@ extension BusSearchResultViewController { let time = "\(selectedItems[0]) \(selectedItems[1]) \(selectedItems[2]):\(selectedItems[3])" self?.inputSubject.send(.getSearchedResult(time, nil)) }, - width: 301, - height: 347, - paddingBetweenLabels: 10, - title: "출발 시각 설정", - subTitle: datePickerSubTitle, - titleColor: .appColor(.neutral700), - subTitleColor: .gray + onDepartureNowTapped: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.departureNow, .click, "지금 출발")) + } ) busSearchDatePickerViewController.setPickerItems(items: datePickerData.0, selectedItems: datePickerData.1) present(busSearchDatePickerViewController, animated: true) diff --git a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift index 3db9eb3b..59d09303 100644 --- a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift @@ -10,80 +10,106 @@ import UIKit final class BusSearchDatePickerViewController: KoinModalViewController { // MARK: - Properties - private let onDepartureNowTapped: () -> Void private let onPickerDateChanged: (Bool?) -> Void private let onPickerItemsSelected: ([String]) -> Void + private let onDepartureNowTapped: ()->Void // MARK: - UI Components - private let pickerView = KoinPickerView() + private let customView = UIView() + private let mainTitleLabel = UILabel().then { + $0.text = "출발 시각 설정" + $0.textColor = .appColor(.neutral700) + $0.font = .appFont(.pretendardMedium, size: 18) + $0.numberOfLines = 1 + $0.textAlignment = .left + } + private let subTitleLabel = UILabel().then { + $0.numberOfLines = 2 + } + let pickerView: KoinPickerView // MARK: - Initializer init( - onDepartureNowTapped: @escaping () -> Void, + subTitle: String, onPickerDateChanged: @escaping (Bool?) -> Void, onPickerItemsSelected: @escaping ([String]) -> Void, - width: CGFloat, - height: CGFloat, - paddingBetweenLabels: CGFloat, - title: String, - subTitle: String, - titleColor: UIColor, - subTitleColor: UIColor + onDepartureNowTapped: @escaping ()->Void ) { - self.onDepartureNowTapped = onDepartureNowTapped + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.do { + $0.alignment = .left + $0.lineSpacing = 14 * 0.6 + $0.lineBreakStrategy = .hangulWordPriority + } + subTitleLabel.attributedText = NSAttributedString( + string: subTitle, + attributes: [ + .foregroundColor: UIColor.appColor(.neutral500), + .font: UIFont.appFont(.pretendardRegular, size: 14), + .paragraphStyle: paragraphStyle, + ] + ) + self.onPickerDateChanged = onPickerDateChanged self.onPickerItemsSelected = onPickerItemsSelected - super.init( - onLeftButtonTapped: nil, - onRightButtonTapped: {}, - width: width, - height: height, - paddingBetweenLabels: paddingBetweenLabels, - title: title, - subTitle: subTitle, - titleColor: titleColor, - subTitleColor: subTitleColor - ) + self.onDepartureNowTapped = onDepartureNowTapped + + let pickerView = KoinPickerView().then { + $0.backgroundColor = .appColor(.neutral50) + } + self.pickerView = pickerView + + let onLeftButtonTapped: ()->Void = { + let currentDate = Date() + let calendar = Calendar.current + let hour = calendar.component(.hour, from: currentDate) + let minute = calendar.component(.minute, from: currentDate) + let amPm = hour < 12 ? "오전" : "오후" + let adjustedHour = hour % 12 + let displayHour = adjustedHour == 0 ? 12 : adjustedHour + + let selectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] + pickerView.setSelectedData(selectedItem: selectedItems) + onPickerItemsSelected(selectedItems) + pickerView.changeSelectedItemPublisher.send(nil) + } + + let onRightButtonTapped: ()->Void = { + onPickerItemsSelected(pickerView.selectedItemPublisher.value) + onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) + pickerView.changeSelectedItemPublisher.send(nil) + } + + super.init(configuration: .init( + appearance: .primary, + content: .custom(customView: customView), + button: .init( + leftButtonTitle: "지금 출발", + leftButtonAction: onLeftButtonTapped, + leftButtonStyle: .init( + textColor: .neutral600, + font: .pretendardMedium, + fontSize: 15 + ), + rightButtonTitle: "완료", + rightButtonAction: onRightButtonTapped + ), + layout: .init( + contentTopPadding: 24, + contentHorizontalPadding: 0 + ) + )) } - - @available(*, unavailable) - required init?(coder: NSCoder) { + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() - setContentViewInContainer(view: pickerView, frame: .init(x: 0, y: 0, width: 301, height: 122)) configureView() } - override func rightButtonTapped() { - let selectedItems = pickerView.selectedItemPublisher.value - onPickerItemsSelected(selectedItems) - onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) - pickerView.changeSelectedItemPublisher.send(nil) - dismiss(animated: true) - } - - override func closeButtonTapped() { - onDepartureNowTapped() - - let currentDate = Date() - let calendar = Calendar.current - let hour = calendar.component(.hour, from: currentDate) - let minute = calendar.component(.minute, from: currentDate) - let amPm = hour < 12 ? "오전" : "오후" - let adjustedHour = hour % 12 - let displayHour = adjustedHour == 0 ? 12 : adjustedHour - - let selectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] - pickerView.setSelectedData(selectedItem: selectedItems) - onPickerItemsSelected(selectedItems) - pickerView.changeSelectedItemPublisher.send(nil) - dismiss(animated: true) - } - // MARK: - Public func setPickerItems(items: [[String]], selectedItems: [String]) { pickerView.changeSelectedItemPublisher.send(nil) @@ -92,11 +118,30 @@ final class BusSearchDatePickerViewController: KoinModalViewController { } extension BusSearchDatePickerViewController { - // MARK: - Configure - private func configureView() { - updateMessageLabel(alignment: .left) - updateSubMessageLabel(alignment: .left) - updaterightButton(borderWidth: 0, title: "완료") - updateCloseButton(borderWidth: 0, title: "지금 출발") + + private func configureView() { + [mainTitleLabel, subTitleLabel, pickerView].forEach { + customView.addSubview($0) + } + mainTitleLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(29) + } + subTitleLabel.snp.makeConstraints { + $0.top.equalTo(mainTitleLabel.snp.bottom).offset(8) + $0.leading.trailing.equalTo(mainTitleLabel) + $0.height.equalTo(44) + } + pickerView.snp.makeConstraints { + $0.top.equalTo(subTitleLabel.snp.bottom).offset(24) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + + customView.snp.makeConstraints { + $0.width.equalTo(301) + $0.height.equalTo(227) + } } } From 76944cff5bb47c0221de402803e73097049e0039 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 16 Aug 2026 20:42:28 +0900 Subject: [PATCH 034/101] =?UTF-8?q?refactor:=20RevokeModalViewController?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeMyProfileViewController.swift | 6 +- .../RevokeModalViewController.swift | 165 ++++-------------- 2 files changed, 39 insertions(+), 132 deletions(-) diff --git a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift index 87851c11..5a9f0478 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift @@ -377,11 +377,9 @@ final class ChangeMyProfileViewController: UIViewController { extension ChangeMyProfileViewController { @objc private func revokeButtonTapped() { - let revokeModalViewController = RevokeModalViewController(onRevokeButtonTapped: { [weak self] in + let revokeModalViewController = RevokeModalViewController { [weak self] in self?.viewModel.revoke() - }) - revokeModalViewController.modalPresentationStyle = .overFullScreen - revokeModalViewController.modalTransitionStyle = .crossDissolve + } present(revokeModalViewController, animated: true, completion: nil) } @objc private func inquryButtonTapped() { diff --git a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift index ff47fe8c..bc2ceec5 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift @@ -2,140 +2,49 @@ // RevokeModalViewController.swift // koin // -// Created by 김나훈 on 9/6/24. +// Created by 홍기정 on 8/16/26. // import UIKit -final class RevokeModalViewController: UIViewController { - private let onRevokeButtonTapped: () -> Void - - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 18) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 8 - let text = "회원탈퇴를 하시겠습니까?" - let attributedString = NSMutableAttributedString(string: text) +final class RevokeModalViewController: KoinModalViewController { + + init(onRevokeButtonTapped: @escaping ()->Void) { + let mainTitle = { + let text = "회원탈퇴를 하시겠습니까?" + let revokeRange = (text as NSString).range(of: "회원탈퇴") + let fullRange = (text as NSString).range(of: text) + return NSMutableAttributedString(string: text).then { + $0.addAttribute(.foregroundColor, value: UIColor.appColor(.neutral700), range: fullRange) + $0.addAttribute(.foregroundColor, value: UIColor.appColor(.danger600), range: revokeRange) + $0.addAttribute(.font, value: UIFont.appFont(.pretendardMedium, size: 18), range: fullRange) + $0.addAttribute(.font, value: UIFont.appFont(.pretendardBold, size: 18), range: revokeRange) + } + }() + let subTitle = { + let text = "회원탈퇴를 하면 계정 복구가 불가능합니다." + let fullRange = (text as NSString).range(of: text) + return NSMutableAttributedString(string: text).then { + $0.addAttribute(.font, value: UIFont.appFont(.pretendardRegular, size: 14), range: fullRange) + $0.addAttribute(.foregroundColor, value: UIColor.appColor(.neutral500), range: fullRange) + } + }() - let loginRange = (text as NSString).range(of: "회원탈퇴") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger600), range: loginRange) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - $0.attributedText = attributedString - $0.textAlignment = .center - } - - - private let subMessageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - $0.textColor = UIColor.appColor(.neutral500) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - let text = "회원탈퇴를 하면 계정 복구가 불가능합니다." - let attributedString = NSMutableAttributedString(string: text) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - $0.attributedText = attributedString - $0.textAlignment = .center - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let revokeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.danger600) - $0.setTitle("회원탈퇴", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() - - init(onRevokeButtonTapped: @escaping () -> Void) { - self.onRevokeButtonTapped = onRevokeButtonTapped - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { + super.init(configuration: .init( + appearance: .destructive, + content: .attributedTitles( + mainTitle: mainTitle, + subTitle: subTitle + ), + button: .init( + leftButtonTitle: "취소", + rightButtonTitle: "회원탈퇴", + rightButtonAction: onRevokeButtonTapped + ) + )) + } + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - revokeButton.addTarget(self, action: #selector(revokeButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) - } - - @objc private func revokeButtonTapped() { - dismiss(animated: true, completion: nil) - onRevokeButtonTapped() - } -} - -extension RevokeModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, closeButton, revokeButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(301) - make.height.equalTo(179) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.centerX.equalTo(containerView.snp.centerX) - } - subMessageLabel.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(16) - make.centerX.equalTo(containerView.snp.centerX) - } - closeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - revokeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } From 4c0662c82b8d0c7e43c3d658880c5354d95e4288 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 02:03:03 +0900 Subject: [PATCH 035/101] =?UTF-8?q?delete:=20ModifyUserModalViewController?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Login/Login/LoginViewController.swift | 38 +++-- .../Login/ModifyUserModalViewController.swift | 131 ------------------ koin.xcodeproj/project.pbxproj | 4 - 3 files changed, 29 insertions(+), 144 deletions(-) delete mode 100644 Koin/Presentation/Login/Login/ModifyUserModalViewController.swift diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index c3dd2df4..241bc9b6 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -164,15 +164,35 @@ final class LoginViewController: UIViewController { extension LoginViewController { private func presentModifyUserModal() { - let modifyUserModalViewController = ModifyUserModalViewController( - onCancelButtonTapped: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }, - onNavigateButtonTapped: { [weak self] in - self?.navigateToChangeMyProfile() - } - ) - present(modifyUserModalViewController, animated: true) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .primary, + content: .titles( + mainTitleText: "아직 입력되지 않은 정보가 있어요.", + mainTitleStyle: .init( + textColor: .neutral800, + font: .pretendardMedium, + fontSize: 18 + ), + subTitleText: "필수 정보를 입력하시면 더 많은 기능을 이용하실 수 있어요.\n지금 입력하시겠어요?", + subTitleStyle: .init( + textColor: .neutral500, + font: .pretendardRegular, + fontSize: 12 + ) + ), + button: .init( + leftButtonTitle: "나중에 하기", + leftButtonAction: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + rightButtonTitle: "지금 입력하기", + rightButtonAction: { [weak self] in + self?.navigateToChangeMyProfile() + } + ), + layout: .init(width: 342) + )) + present(modalViewController, animated: true) } private func navigateToChangeMyProfile() { diff --git a/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift b/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift deleted file mode 100644 index 231173f1..00000000 --- a/Koin/Presentation/Login/Login/ModifyUserModalViewController.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// ModifyUserModalViewController.swift -// koin -// -// Created by 김나훈 on 7/14/25. -// - -import UIKit - -final class ModifyUserModalViewController: UIViewController { - private let onCancelButtonTapped: () -> Void - private let onNavigateButtonTapped: () -> Void - - private let messageLabel = UILabel().then { - $0.text = "아직 입력되지 않은 정보가 있어요." - $0.font = UIFont.appFont(.pretendardMedium, size: 18) - $0.textColor = .black - } - - private let subMessageLabel = UILabel().then { - $0.text = "필수 정보를 입력하시면 더 많은 기능을 이용하실 수 있어요.\n지금 입력하시겠어요?" - $0.font = UIFont.appFont(.pretendardRegular, size: 12) - $0.numberOfLines = 2 - $0.textAlignment = .center - $0.textColor = .gray - } - - private let cancelButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("나중에 하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let navigateButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("지금 입력하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 16 - view.layer.masksToBounds = true - return view - }() - - init( - onCancelButtonTapped: @escaping () -> Void, - onNavigateButtonTapped: @escaping () -> Void - ) { - self.onCancelButtonTapped = onCancelButtonTapped - self.onNavigateButtonTapped = onNavigateButtonTapped - super.init(nibName: nil, bundle: nil) - self.modalPresentationStyle = .overFullScreen - self.modalTransitionStyle = .crossDissolve - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) - navigateButton.addTarget(self, action: #selector(navigateButtonTapped), for: .touchUpInside) - } - @objc private func cancelButtonTapped() { - dismiss(animated: true, completion: onCancelButtonTapped) - } - @objc private func navigateButtonTapped() { - dismiss(animated: true, completion: onNavigateButtonTapped) - } - -} - -extension ModifyUserModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, cancelButton, navigateButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { - $0.centerX.equalTo(view.snp.centerX) - $0.centerY.equalTo(view.snp.centerY) - $0.horizontalEdges.equalToSuperview().inset(24) - $0.height.equalTo(232) - } - messageLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(42.5) - $0.centerX.equalToSuperview() - } - subMessageLabel.snp.makeConstraints { - $0.top.equalTo(messageLabel.snp.bottom).offset(12) - $0.centerX.equalToSuperview() - } - cancelButton.snp.makeConstraints { - $0.leading.equalToSuperview().offset(32) - $0.bottom.equalToSuperview().offset(-42.5) - $0.trailing.equalTo(view.snp.centerX).offset(-4) - $0.height.equalTo(48) - } - navigateButton.snp.makeConstraints { - $0.leading.equalTo(view.snp.centerX).offset(4) - $0.bottom.equalToSuperview().offset(-42.5) - $0.trailing.equalToSuperview().offset(-32) - $0.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 182d24ee..c06146d2 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -593,7 +593,6 @@ 7CED91DD2EF2D1F900457128 /* ShopDetailTableViewNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FE02EF2D1F900457128 /* ShopDetailTableViewNameCell.swift */; }; 7CED91DF2EF2D1F900457128 /* NotiViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F152EF2D1F900457128 /* NotiViewController.swift */; }; 7CED91E32EF2D1F900457128 /* AddClassCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90962EF2D1F900457128 /* AddClassCollectionViewCell.swift */; }; - 7CED91E52EF2D1F900457128 /* ModifyUserModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F222EF2D1F900457128 /* ModifyUserModalViewController.swift */; }; 7CED91E62EF2D1F900457128 /* ShopSummaryInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FC82EF2D1F900457128 /* ShopSummaryInfoView.swift */; }; 7CED91E92EF2D1F900457128 /* ShopDetailTableViewDeliveryTipsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FE32EF2D1F900457128 /* ShopDetailTableViewDeliveryTipsCell.swift */; }; 7CED91EA2EF2D1F900457128 /* TimetableCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90992EF2D1F900457128 /* TimetableCollectionView.swift */; }; @@ -1468,7 +1467,6 @@ 7CED8F1A2EF2D1F900457128 /* PolicyListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyListCollectionViewCell.swift; sourceTree = ""; }; 7CED8F1B2EF2D1F900457128 /* PolicyListTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyListTableView.swift; sourceTree = ""; }; 7CED8F1E2EF2D1F900457128 /* PolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyViewController.swift; sourceTree = ""; }; - 7CED8F222EF2D1F900457128 /* ModifyUserModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifyUserModalViewController.swift; sourceTree = ""; }; 7CED8F232EF2D1F900457128 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 7CED8F242EF2D1F900457128 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 7CED8F262EF2D1F900457128 /* AgreementFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementFormViewController.swift; sourceTree = ""; }; @@ -3510,7 +3508,6 @@ 7CED8F252EF2D1F900457128 /* Login */ = { isa = PBXGroup; children = ( - 7CED8F222EF2D1F900457128 /* ModifyUserModalViewController.swift */, 7CED8F232EF2D1F900457128 /* LoginViewModel.swift */, 7CED8F242EF2D1F900457128 /* LoginViewController.swift */, ); @@ -6379,7 +6376,6 @@ 7C61D3492F220EAC005FAC3A /* EditLostItemFoundDateView.swift in Sources */, 7CC42B202F56F3C400940CE1 /* CallVanState.swift in Sources */, 7C8ADD382F20C0B200F85BDE /* LostItemListDto.swift in Sources */, - 7CED91E52EF2D1F900457128 /* ModifyUserModalViewController.swift in Sources */, 7C640409300F3F7700A9589F /* FetchDepartmentRequestDto.swift in Sources */, 7CED91E62EF2D1F900457128 /* ShopSummaryInfoView.swift in Sources */, 7CED91E92EF2D1F900457128 /* ShopDetailTableViewDeliveryTipsCell.swift in Sources */, From 10503e8ef310016d822ce8c6a070f2cf1e69ec6f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 02:03:27 +0900 Subject: [PATCH 036/101] =?UTF-8?q?fix:=20KoinModalViewController=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalViewController/Subviews/ModalContentView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift b/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift index d7ef89fa..d22dc07b 100644 --- a/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift +++ b/Koin/Core/View/KoinModalViewController/Subviews/ModalContentView.swift @@ -133,12 +133,14 @@ extension ModalContentView { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) let paragraphStyle = NSMutableParagraphStyle().then { $0.lineHeightMultiple = 1.6 + $0.alignment = .center } let fullRange = { let text = attributedString.string return (text as NSString).range(of: text) }() mutableAttributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange) - label.attributedText = mutableAttributedString + label.attributedText = mutableAttributedString + label.numberOfLines = 0 } } From d8ac7ae34a50925d89b0907dc2039f6445bb2543 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 02:10:05 +0900 Subject: [PATCH 037/101] =?UTF-8?q?delete:=20UpdateModalViewController=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ForceUpdateViewController.swift | 31 ++-- .../UpdateModalViewController.swift | 151 ------------------ koin.xcodeproj/project.pbxproj | 4 - 3 files changed, 19 insertions(+), 167 deletions(-) delete mode 100644 Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift diff --git a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift index b0c16e34..552c37b5 100644 --- a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift +++ b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift @@ -180,18 +180,25 @@ extension ForceUpdateViewController { } @objc private func errorCheckButtonTapped() { - let updateModalViewController = UpdateModalViewController( - onOpenStoreButtonTapped: { [weak self] in - self?.openStore() - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) - }, - onCloseButtonTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) - } - ) - updateModalViewController.modalPresentationStyle = .overFullScreen - updateModalViewController.modalTransitionStyle = .crossDissolve - present(updateModalViewController, animated: true, completion: nil) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .titles( + mainTitleText: "이미 업데이트 하셨나요?", + subTitleText: "업데이트 이후에도 이 화면이 나타나는\n경우에는 스토어에서 코인을\n삭제 후 재설치 해 주세요." + ), + button: .init( + leftButtonTitle: "닫기", + leftButtonAction: { [weak self] in + self?.openStore() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) + }, + rightButtonTitle: "스토어 가기", + rightButtonAction: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) + } + ) + )) + present(modalViewController, animated: true, completion: nil) inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.forceUpdateAlreadyDone, .click, "이미업데이트")) } } diff --git a/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift b/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift deleted file mode 100644 index 4adb8308..00000000 --- a/Koin/Presentation/Core/ForceUpdate/UpdateModalViewController.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// UpdateModalViewController.swift -// koin -// -// Created by 김나훈 on 10/1/24. -// - -import UIKit -import SnapKit - -final class UpdateModalViewController: UIViewController { - // MARK: - Properties - private let onOpenStoreButtonTapped: () -> Void - private let onCloseButtonTapped: () -> Void - - // MARK: - UI Components - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 18) - $0.text = "이미 업데이트 하셨나요?" - } - - private let subMessageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 13) - $0.textColor = UIColor.appColor(.neutral600) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - let text = "업데이트 이후에도 이 화면이 나타나는\n경우에는 스토어에서 코인을\n삭제 후 재설치 해 주세요." - let attributedString = NSMutableAttributedString(string: text) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - $0.attributedText = attributedString - $0.numberOfLines = 3 - $0.textAlignment = .center - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral400).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("닫기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - private let openStoreButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.new500) - $0.setTitle("스토어 가기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - private let containerView = UIView().then { - $0.backgroundColor = .white - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - init( - onOpenStoreButtonTapped: @escaping () -> Void, - onCloseButtonTapped: @escaping () -> Void - ) { - self.onOpenStoreButtonTapped = onOpenStoreButtonTapped - self.onCloseButtonTapped = onCloseButtonTapped - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - configureView() - setAddTarget() - } - - private func setAddTarget() { - openStoreButton.addTarget(self, action: #selector(openStoreButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } -} - -// MARK: - @objc -extension UpdateModalViewController { - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) - onCloseButtonTapped() - } - - @objc private func openStoreButtonTapped() { - dismiss(animated: true, completion: nil) - onOpenStoreButtonTapped() - } -} - -// MARK: - UI Function -extension UpdateModalViewController { - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - - [messageLabel, subMessageLabel, closeButton, openStoreButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { - $0.centerX.equalTo(view.snp.centerX) - $0.centerY.equalTo(view.snp.centerY) - $0.width.equalTo(301) - $0.height.equalTo(210) - } - - messageLabel.snp.makeConstraints { - $0.top.equalTo(containerView.snp.top).offset(24) - $0.centerX.equalTo(containerView.snp.centerX) - } - - subMessageLabel.snp.makeConstraints { - $0.top.equalTo(messageLabel.snp.bottom).offset(16) - $0.centerX.equalTo(containerView.snp.centerX) - } - - closeButton.snp.makeConstraints { - $0.top.equalTo(subMessageLabel.snp.bottom).offset(24) - $0.trailing.equalTo(containerView.snp.centerX).offset(-4) - $0.width.equalTo(114.5) - $0.height.equalTo(48) - } - - openStoreButton.snp.makeConstraints { - $0.top.equalTo(subMessageLabel.snp.bottom).offset(24) - $0.leading.equalTo(containerView.snp.centerX).offset(4) - $0.width.equalTo(114.5) - $0.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index c06146d2..8f783bad 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -557,7 +557,6 @@ 7CED91962EF2D1F900457128 /* ScoreChartCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FFF2EF2D1F900457128 /* ScoreChartCollectionView.swift */; }; 7CED91982EF2D1F900457128 /* PolicyListTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F1B2EF2D1F900457128 /* PolicyListTableView.swift */; }; 7CED91992EF2D1F900457128 /* ShopSummaryPhoneButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FC62EF2D1F900457128 /* ShopSummaryPhoneButton.swift */; }; - 7CED919B2EF2D1F900457128 /* UpdateModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EE12EF2D1F900457128 /* UpdateModalViewController.swift */; }; 7CED919E2EF2D1F900457128 /* ChangePasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F122EF2D1F900457128 /* ChangePasswordViewController.swift */; }; 7CED919F2EF2D1F900457128 /* RecentSearchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F542EF2D1F900457128 /* RecentSearchTableViewCell.swift */; }; 7CED91A02EF2D1F900457128 /* StateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F332EF2D1F900457128 /* StateButton.swift */; }; @@ -1431,7 +1430,6 @@ 7CED8ED42EF2D1F900457128 /* ForceModifyUserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceModifyUserViewController.swift; sourceTree = ""; }; 7CED8EDF2EF2D1F900457128 /* ForceUpdateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceUpdateViewModel.swift; sourceTree = ""; }; 7CED8EE02EF2D1F900457128 /* ForceUpdateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceUpdateViewController.swift; sourceTree = ""; }; - 7CED8EE12EF2D1F900457128 /* UpdateModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateModalViewController.swift; sourceTree = ""; }; 7CED8EE52EF2D1F900457128 /* ReportLostItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportLostItemViewModel.swift; sourceTree = ""; }; 7CED8EE62EF2D1F900457128 /* ReportLostItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportLostItemViewController.swift; sourceTree = ""; }; 7CED8EE82EF2D1F900457128 /* LostArticleImageCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostArticleImageCollectionView.swift; sourceTree = ""; }; @@ -3303,7 +3301,6 @@ children = ( 7CED8EDF2EF2D1F900457128 /* ForceUpdateViewModel.swift */, 7CED8EE02EF2D1F900457128 /* ForceUpdateViewController.swift */, - 7CED8EE12EF2D1F900457128 /* UpdateModalViewController.swift */, ); path = ForceUpdate; sourceTree = ""; @@ -6231,7 +6228,6 @@ 7CED91962EF2D1F900457128 /* ScoreChartCollectionView.swift in Sources */, 7CED91982EF2D1F900457128 /* PolicyListTableView.swift in Sources */, 7CED91992EF2D1F900457128 /* ShopSummaryPhoneButton.swift in Sources */, - 7CED919B2EF2D1F900457128 /* UpdateModalViewController.swift in Sources */, 7C8A94352FD2C40400DEA6F5 /* FetchShopCountUseCase.swift in Sources */, 7CED919E2EF2D1F900457128 /* ChangePasswordViewController.swift in Sources */, 7CED919F2EF2D1F900457128 /* RecentSearchTableViewCell.swift in Sources */, From 89a12131dee940d1e6b27cba50b22ce3ed8fc295 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 02:20:33 +0900 Subject: [PATCH 038/101] =?UTF-8?q?delete:=20ReviewLoginModalViewControlle?= =?UTF-8?q?r=20=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewListViewController.swift | 50 +++-- .../ReviewLoginModalViewController.swift | 186 ------------------ koin.xcodeproj/project.pbxproj | 4 - 3 files changed, 36 insertions(+), 204 deletions(-) delete mode 100644 Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift diff --git a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift index 66110023..c0ea54b3 100644 --- a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift @@ -325,20 +325,42 @@ extension ReviewListViewController { } private func presentReviewLoginModal(message: String) { - let reviewLoginModalViewController = ReviewLoginModalViewController( - message: message, - onLoginButtonTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) - self?.showLoginScreen() - }, - onCancelButtonTapped: { [weak self] in - guard message == "작성", let self else { return } - inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, .click, viewModel.getShopName())) - } - ) - reviewLoginModalViewController.modalPresentationStyle = .overFullScreen - reviewLoginModalViewController.modalTransitionStyle = .crossDissolve - present(reviewLoginModalViewController, animated: true) + let mainText: String + let subText: String + + switch message { + case "작성": + mainText = "리뷰를 작성하기 위해\n로그인이 필요해요." + subText = "리뷰 작성은 회원만 사용 가능합니다." + + case "신고": + mainText = "리뷰를 신고하기 위해\n로그인이 필요해요." + subText = "리뷰 신고는 회원만 사용 가능합니다." + + default: + return + } + + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .titles( + mainTitleText: mainText, + subTitleText: subText + ), + button: .init( + leftButtonTitle: "닫기", + leftButtonAction: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) + self?.showLoginScreen() + }, + rightButtonTitle: "로그인하기", + rightButtonAction: { [weak self] in + guard message == "작성", let self else { return } + inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, .click, viewModel.getShopName())) + } + ) + )) + present(modalViewController, animated: true) } private func deleteReview() { diff --git a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift b/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift deleted file mode 100644 index 5b9be193..00000000 --- a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewLoginModalViewController.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// ReviewLoginModalViewController.swift -// koin -// -// Created by 김나훈 on 8/13/24. -// - - -import UIKit - -final class ReviewLoginModalViewController: UIViewController { - - // MARK: - Properties - - private let message: String - private let onLoginButtonTapped: () -> Void - private let onCancelButtonTapped: () -> Void - - // MARK: - UI Components - - private lazy var messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 18) - $0.textColor = UIColor.appColor(.neutral600) - $0.numberOfLines = 2 - $0.textAlignment = .center - } - - private lazy var subMessageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - $0.textColor = UIColor.appColor(.neutral500) - $0.numberOfLines = 1 - $0.textAlignment = .center - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral400).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("닫기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - private let loginButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.new500) - $0.setTitle("로그인하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - private let containerView = UIView().then { - $0.backgroundColor = .white - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - // MARK: - Initializer - - init( - message: String, - onLoginButtonTapped: @escaping () -> Void, - onCancelButtonTapped: @escaping () -> Void - ) { - self.message = message - self.onLoginButtonTapped = onLoginButtonTapped - self.onCancelButtonTapped = onCancelButtonTapped - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Life Cycles - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - configureMessages() - setAddTarget() - } - - private func setAddTarget() { - loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - private func configureMessages() { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 8 - - let mainText: String - let subText: String - - switch message { - case "작성": - mainText = "리뷰를 작성하기 위해\n로그인이 필요해요." - subText = "리뷰 작성은 회원만 사용 가능합니다." - - case "신고": - mainText = "리뷰를 신고하기 위해\n로그인이 필요해요." - subText = "리뷰 신고는 회원만 사용 가능합니다." - - default: - mainText = "리뷰를 작성하기 위해\n로그인이 필요해요." - subText = "리뷰 작성은 회원만 사용 가능합니다." - } - - let mainAttributedString = NSMutableAttributedString( - string: mainText, - attributes: [.paragraphStyle: paragraphStyle] - ) - messageLabel.attributedText = mainAttributedString - - let subParagraphStyle = NSMutableParagraphStyle() - subParagraphStyle.lineSpacing = 6 - let subAttributedString = NSMutableAttributedString( - string: subText, - attributes: [.paragraphStyle: subParagraphStyle] - ) - subMessageLabel.attributedText = subAttributedString - } - - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) - onCancelButtonTapped() - } - - @objc private func loginButtonTapped() { - dismiss(animated: true, completion: nil) - onLoginButtonTapped() - } -} - -// MARK: - UI Functions - -extension ReviewLoginModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, closeButton, loginButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(301) - make.height.equalTo(208) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.centerX.equalTo(containerView.snp.centerX) - } - subMessageLabel.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(8) - make.centerX.equalTo(containerView.snp.centerX) - } - closeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-4) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - loginButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(4) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 8f783bad..25436341 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -537,7 +537,6 @@ 7CED917A2EF2D1F900457128 /* ShopSummaryTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FCE2EF2D1F900457128 /* ShopSummaryTableView.swift */; }; 7CED917D2EF2D1F900457128 /* ReviewImageUploadCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90172EF2D1F900457128 /* ReviewImageUploadCollectionView.swift */; }; 7CED917F2EF2D1F900457128 /* NoticeListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F4D2EF2D1F900457128 /* NoticeListViewController.swift */; }; - 7CED91802EF2D1F900457128 /* ReviewLoginModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED900C2EF2D1F900457128 /* ReviewLoginModalViewController.swift */; }; 7CED91822EF2D1F900457128 /* DeliveryTipsCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FDC2EF2D1F900457128 /* DeliveryTipsCollectionView.swift */; }; 7CED91832EF2D1F900457128 /* NoticeDataViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F702EF2D1F900457128 /* NoticeDataViewModel.swift */; }; 7CED91852EF2D1F900457128 /* DiningOperatingTimeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90762EF2D1F900457128 /* DiningOperatingTimeCollectionView.swift */; }; @@ -1562,7 +1561,6 @@ 7CED90082EF2D1F900457128 /* ImageDropDownCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDropDownCell.swift; sourceTree = ""; }; 7CED900A2EF2D1F900457128 /* ScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreView.swift; sourceTree = ""; }; 7CED900B2EF2D1F900457128 /* NonReviewListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonReviewListView.swift; sourceTree = ""; }; - 7CED900C2EF2D1F900457128 /* ReviewLoginModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewLoginModalViewController.swift; sourceTree = ""; }; 7CED900D2EF2D1F900457128 /* DeleteReviewModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteReviewModalViewController.swift; sourceTree = ""; }; 7CED900F2EF2D1F900457128 /* ReviewListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewController.swift; sourceTree = ""; }; 7CED90102EF2D1F900457128 /* ReviewListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewModel.swift; sourceTree = ""; }; @@ -3987,7 +3985,6 @@ 7CED90092EF2D1F900457128 /* ReviewListCollectionView */, 7CED900A2EF2D1F900457128 /* ScoreView.swift */, 7CED900B2EF2D1F900457128 /* NonReviewListView.swift */, - 7CED900C2EF2D1F900457128 /* ReviewLoginModalViewController.swift */, 7CED900D2EF2D1F900457128 /* DeleteReviewModalViewController.swift */, ); path = SubViews; @@ -6195,7 +6192,6 @@ 7CC42B942F5DCEA600940CE1 /* CallVanChatTableView.swift in Sources */, 7CED917D2EF2D1F900457128 /* ReviewImageUploadCollectionView.swift in Sources */, 7CED917F2EF2D1F900457128 /* NoticeListViewController.swift in Sources */, - 7CED91802EF2D1F900457128 /* ReviewLoginModalViewController.swift in Sources */, 7CED91822EF2D1F900457128 /* DeliveryTipsCollectionView.swift in Sources */, 7C58FE892F5F9E7600A2737B /* CallVanData.swift in Sources */, 7CED91832EF2D1F900457128 /* NoticeDataViewModel.swift in Sources */, From 111e9a9db61eb2c11e0c639c5ea51284fb1a3997 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 02:24:53 +0900 Subject: [PATCH 039/101] =?UTF-8?q?delete:=20DeleteReviewModalViewControll?= =?UTF-8?q?er=20=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewListViewController.swift | 29 ++-- .../DeleteReviewModalViewController.swift | 145 ------------------ koin.xcodeproj/project.pbxproj | 4 - 3 files changed, 17 insertions(+), 161 deletions(-) delete mode 100644 Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift diff --git a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift index c0ea54b3..e7b560f3 100644 --- a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift @@ -310,18 +310,23 @@ extension ReviewListViewController { .click, viewModel.getShopName() )) - let deleteReviewModalViewController = DeleteReviewModalViewController( - onDeleteButtonTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) - self?.deleteReview() - }, - onCancelButtonTapped: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "X")) - } - ) - deleteReviewModalViewController.modalPresentationStyle = .overFullScreen - deleteReviewModalViewController.modalTransitionStyle = .crossDissolve - present(deleteReviewModalViewController, animated: true) + + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .singleTitle(text: "삭제한 리뷰는 되돌릴 수 없습니다.\n삭제 하시겠습니까?"), + button: .init( + leftButtonTitle: "취소하기", + leftButtonAction: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) + self?.deleteReview() + }, + rightButtonTitle: "삭제하기", + rightButtonAction: { [weak self] in + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "X")) + } + ) + )) + present(modalViewController, animated: true) } private func presentReviewLoginModal(message: String) { diff --git a/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift b/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift deleted file mode 100644 index c057af53..00000000 --- a/Koin/Presentation/Shop/ShopReviewList/SubViews/DeleteReviewModalViewController.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// DeleteReviewModalViewController.swift -// koin -// -// Created by 김나훈 on 8/13/24. -// - -import UIKit -import SnapKit - -final class DeleteReviewModalViewController: UIViewController { - - // MARK: - Properties - private let onDeleteButtonTapped: () -> Void - private let onCancelButtonTapped: () -> Void - - // MARK: - UI Components - private let containerView = UIView().then { - $0.backgroundColor = .white - $0.layer.cornerRadius = 8 - $0.layer.masksToBounds = true - } - - private let reviewDeleteInfoLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 15) - $0.textColor = UIColor.appColor(.neutral600) - $0.numberOfLines = 0 - $0.textAlignment = .center - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - paragraphStyle.alignment = .center - - let text = "삭제한 리뷰는 되돌릴 수 없습니다.\n삭제 하시겠습니까?" - let attributedString = NSMutableAttributedString(string: text) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - - $0.attributedText = attributedString - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral400).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - private let deleteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.new500) - $0.setTitle("삭제하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 6 - $0.layer.masksToBounds = true - } - - init( - onDeleteButtonTapped: @escaping () -> Void, - onCancelButtonTapped: @escaping () -> Void - ) { - self.onDeleteButtonTapped = onDeleteButtonTapped - self.onCancelButtonTapped = onCancelButtonTapped - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - configureView() - setAddTarget() - } - - private func setAddTarget() { - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } -} - -extension DeleteReviewModalViewController { - @objc private func closeButtonTapped() { - onCancelButtonTapped() - dismiss(animated: true, completion: nil) - } - - @objc private func deleteButtonTapped() { - dismiss(animated: true, completion: nil) - onDeleteButtonTapped() - } -} - -// MARK: - UI Functions -extension DeleteReviewModalViewController { - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - - [reviewDeleteInfoLabel, closeButton, deleteButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { - $0.centerX.equalTo(view.snp.centerX) - $0.centerY.equalTo(view.snp.centerY) - $0.width.equalTo(301) - $0.height.equalTo(168) - } - - reviewDeleteInfoLabel.snp.makeConstraints { - $0.top.equalTo(containerView.snp.top).offset(24) - $0.centerX.equalTo(containerView.snp.centerX) - } - - closeButton.snp.makeConstraints { - $0.top.equalTo(reviewDeleteInfoLabel.snp.bottom).offset(24) - $0.trailing.equalTo(containerView.snp.centerX).offset(-4) - $0.width.equalTo(114.5) - $0.height.equalTo(48) - } - - deleteButton.snp.makeConstraints { - $0.top.equalTo(reviewDeleteInfoLabel.snp.bottom).offset(24) - $0.leading.equalTo(containerView.snp.centerX).offset(4) - $0.width.equalTo(114.5) - $0.height.equalTo(48) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 25436341..90dfaae4 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -440,7 +440,6 @@ 7CED90E22EF2D1F900457128 /* PointLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90892EF2D1F900457128 /* PointLabel.swift */; }; 7CED90E32EF2D1F900457128 /* FindPhoneIdViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F2F2EF2D1F900457128 /* FindPhoneIdViewController.swift */; }; 7CED90E42EF2D1F900457128 /* SubstituteTimetableModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED908D2EF2D1F900457128 /* SubstituteTimetableModalViewController.swift */; }; - 7CED90E62EF2D1F900457128 /* DeleteReviewModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED900D2EF2D1F900457128 /* DeleteReviewModalViewController.swift */; }; 7CED90E72EF2D1F900457128 /* ForceUpdateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EE02EF2D1F900457128 /* ForceUpdateViewController.swift */; }; 7CED90E92EF2D1F900457128 /* DiningCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED906C2EF2D1F900457128 /* DiningCollectionView.swift */; }; 7CED90EB2EF2D1F900457128 /* LandViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90AC2EF2D1F900457128 /* LandViewController.swift */; }; @@ -1561,7 +1560,6 @@ 7CED90082EF2D1F900457128 /* ImageDropDownCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDropDownCell.swift; sourceTree = ""; }; 7CED900A2EF2D1F900457128 /* ScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreView.swift; sourceTree = ""; }; 7CED900B2EF2D1F900457128 /* NonReviewListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonReviewListView.swift; sourceTree = ""; }; - 7CED900D2EF2D1F900457128 /* DeleteReviewModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteReviewModalViewController.swift; sourceTree = ""; }; 7CED900F2EF2D1F900457128 /* ReviewListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewController.swift; sourceTree = ""; }; 7CED90102EF2D1F900457128 /* ReviewListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewModel.swift; sourceTree = ""; }; 7CED90122EF2D1F900457128 /* DashedBorderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashedBorderButton.swift; sourceTree = ""; }; @@ -3985,7 +3983,6 @@ 7CED90092EF2D1F900457128 /* ReviewListCollectionView */, 7CED900A2EF2D1F900457128 /* ScoreView.swift */, 7CED900B2EF2D1F900457128 /* NonReviewListView.swift */, - 7CED900D2EF2D1F900457128 /* DeleteReviewModalViewController.swift */, ); path = SubViews; sourceTree = ""; @@ -6023,7 +6020,6 @@ 7CED90E22EF2D1F900457128 /* PointLabel.swift in Sources */, 7CED90E32EF2D1F900457128 /* FindPhoneIdViewController.swift in Sources */, 7CED90E42EF2D1F900457128 /* SubstituteTimetableModalViewController.swift in Sources */, - 7CED90E62EF2D1F900457128 /* DeleteReviewModalViewController.swift in Sources */, 7CED90E72EF2D1F900457128 /* ForceUpdateViewController.swift in Sources */, 7CED90E92EF2D1F900457128 /* DiningCollectionView.swift in Sources */, 7C86750E30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift in Sources */, From 70a850e600e64e48b20218d056d5793a74d39106 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 03:52:29 +0900 Subject: [PATCH 040/101] =?UTF-8?q?refactor:=20ModifyFrameModalViewControl?= =?UTF-8?q?ler=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalViewController.swift | 20 +- .../FrameList/FrameListViewController.swift | 4 +- .../ModifyFrameModalViewController.swift | 255 +++++++++--------- 3 files changed, 148 insertions(+), 131 deletions(-) diff --git a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index c6acd0db..307b1bf2 100644 --- a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -45,17 +45,25 @@ class KoinModalViewController: UIViewController { // MARK: - Bind private func bind() { buttonView.leftButtonTappedPublisher.sink { [weak self] in - self?.dismiss(animated: true) { [weak self] in - self?.configuration.button?.leftButtonAction?() - } + self?.leftButtonTapped() }.store(in: &subscriptions) buttonView.rightButtonTappedPublisher.sink { [weak self] in - self?.dismiss(animated: true) { [weak self] in - self?.configuration.button?.rightButtonAction() - } + self?.rightButtonTapped() }.store(in: &subscriptions) } + + func leftButtonTapped() { + dismiss(animated: true) { [weak self] in + self?.configuration.button?.leftButtonAction?() + } + } + + func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + self?.configuration.button?.rightButtonAction() + } + } } extension KoinModalViewController: UIViewControllerTransitioningDelegate { diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index 45e27f49..8b8ea403 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -126,9 +126,7 @@ extension FrameListViewController: TimetableCellDelegate { }, onSaveButtonTapped: { [weak self] frame in self?.inputSubject.send(.modifyFrame(frame)) - }, - width: 327, - height: 216 + } ) modifyFrameModalViewController.configure(frame: viewModel.frameData[section].frame[row]) present(modifyFrameModalViewController, animated: true) diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift index 90b33fd5..3c06f2c7 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift @@ -7,101 +7,102 @@ import UIKit -final class ModifyFrameModalViewController: UIViewController { +final class ModifyFrameModalViewController: KoinModalViewController { + + // MARK: - Properties private let onDeleteButtonTapped: (FrameDto) -> Void private let onSaveButtonTapped: (FrameDto) -> Void - var containerWidth: CGFloat - var containerHeight: CGFloat - var frame: FrameDto = FrameDto(id: 0, timetableName: "", isMain: false) + private var frame: FrameDto = FrameDto(id: 0, timetableName: "", isMain: false) - private let messageLabel = UILabel().then { - $0.text = "시간표 설정" - $0.font = UIFont.appFont(.pretendardRegular, size: 17) - } + // MARK: - UI Components + let containerView = UIView() - private let deleteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.danger700) - $0.setTitle("삭제", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 14) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } + let messageLabel = UILabel() + let deleteButton = UIButton() + let textField = UITextField() - private let textField = UITextField().then { - $0.backgroundColor = UIColor.appColor(.neutral100) - } - - private let checkButton = UIButton().then { _ in - } - private let buttonTextLabel = UILabel().then { - $0.text = "기본 시간표로 설정하기" - $0.font = UIFont.appFont(.pretendardMedium, size: 14) - } - - private let cancelButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let saveButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("저장", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() + let checkButtonWrapperView = UIView() + let checkButton = UIButton() + let buttonTextLabel = UILabel() + // MARK: - Initializer init( onDeleteButtonTapped: @escaping (FrameDto) -> Void, - onSaveButtonTapped: @escaping (FrameDto) -> Void, - width: CGFloat, - height: CGFloat + onSaveButtonTapped: @escaping (FrameDto) -> Void ) { self.onDeleteButtonTapped = onDeleteButtonTapped self.onSaveButtonTapped = onSaveButtonTapped - self.containerWidth = width - self.containerHeight = height - super.init(nibName: nil, bundle: nil) - textField.delegate = self - } - - required init?(coder: NSCoder) { + + super.init(configuration: .init( + appearance: .primary, + content: .custom(customView: containerView), + button: .init( + leftButtonTitle: "취소", + rightButtonTitle: "저장", + rightButtonAction: {} //rightButtonAction + ), + layout: .init( + width: 327, + contentTopPadding: 12, + contentHorizontalPadding: 24, + paddingBetweenContentAndButton: 10, + buttonHorizontalPadding: 24, + buttonBottomPadding: 16 + ) + )) + } + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() configureView() - textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) - checkButton.addTarget(self, action: #selector(checkButtonTapped), for: .touchUpInside) - saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside) + setUpDelegate() + setUpAddTargets() } + // MARK: - Public func configure(frame: FrameDto) { self.frame = frame - self.textField.text = frame.timetableName + self.textField.attributedPlaceholder = NSAttributedString( + string: frame.timetableName, + attributes: [ + .font: UIFont.appFont(.pretendardRegular, size: 14), + .foregroundColor: UIColor.appColor(.neutral500) + ]) self.frame = frame self.checkButton.setImage(UIImage.appImage(asset: frame.isMain ? .checkFill : .checkEmpty), for: .normal) } + // MARK: - Override + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + onSaveButtonTapped(frame) + } + } +} + +extension ModifyFrameModalViewController { + private func setUpDelegate() { + textField.delegate = self + } + + override func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() // 키보드 내리기 + return true + } +} + +extension ModifyFrameModalViewController { + private func setUpAddTargets() { + textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) + checkButton.addTarget(self, action: #selector(checkButtonTapped), for: .touchUpInside) + } + @objc private func textFieldDidChange(_ textField: UITextField) { guard let text = textField.text else { return } frame.timetableName = text @@ -111,85 +112,95 @@ final class ModifyFrameModalViewController: UIViewController { frame.isMain.toggle() checkButton.setImage(UIImage.appImage(asset: frame.isMain ? .checkFill : .checkEmpty), for: .normal) } + @objc private func deleteButtonTapped() { dismiss(animated: true) { [weak self] in guard let self else { return } onDeleteButtonTapped(frame) } } - @objc private func cancelButtonTapped() { - dismiss(animated: true, completion: nil) - } - - @objc private func saveButtonTapped() { - onSaveButtonTapped(frame) - dismiss(animated: true, completion: nil) - } - override func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() // 키보드 내리기 - return true - } - } extension ModifyFrameModalViewController { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) + private func setUpStyles() { + messageLabel.do { + $0.text = "시간표 설정" + $0.font = UIFont.appFont(.pretendardSemiBold, size: 16) + } + + deleteButton.do { + $0.backgroundColor = UIColor.appColor(.danger700) + $0.setTitle("삭제", for: .normal) + $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) + $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 14) + $0.layer.cornerRadius = 4 + $0.layer.masksToBounds = true + } + + textField.do { + $0.backgroundColor = UIColor.appColor(.neutral100) + $0.layer.borderColor = UIColor.appColor(.neutral300).cgColor + $0.layer.borderWidth = 1 + $0.layer.cornerRadius = 4 + $0.leftView = UIView(frame: .init(x: 0, y: 0, width: 16, height: 22)) + $0.leftViewMode = .always + $0.rightView = UIView(frame: .init(x: 0, y: 0, width: 16, height: 22)) + $0.rightViewMode = .always + $0.font = UIFont.appFont(.pretendardRegular, size: 14) + $0.textColor = UIColor.appColor(.neutral800) + } + + buttonTextLabel.do { + $0.text = "기본 시간표로 설정하기" + $0.font = UIFont.appFont(.pretendardMedium, size: 14) + } + } + private func setUpLayouts() { + [checkButton, buttonTextLabel].forEach { + checkButtonWrapperView.addSubview($0) } - [deleteButton, messageLabel, textField, checkButton, buttonTextLabel, cancelButton, saveButton].forEach { + [deleteButton, messageLabel, textField, checkButtonWrapperView].forEach { containerView.addSubview($0) } } - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(containerWidth) - make.height.equalTo(containerHeight) - } deleteButton.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(13) - make.leading.equalTo(containerView.snp.leading).offset(24) + make.top.leading.equalToSuperview() make.width.equalTo(60) make.height.equalTo(24) } messageLabel.snp.makeConstraints { make in - make.top.equalTo(deleteButton.snp.bottom) - make.centerX.equalTo(containerView.snp.centerX) + make.top.equalToSuperview().offset(14) + make.centerX.equalToSuperview() + make.height.equalTo(26) } textField.snp.makeConstraints { make in make.top.equalTo(messageLabel.snp.bottom).offset(14) - make.leading.equalTo(containerView.snp.leading).offset(24) - make.trailing.equalTo(containerView.snp.trailing).offset(-24) + make.leading.equalTo(containerView.snp.leading) + make.trailing.equalTo(containerView.snp.trailing) make.height.equalTo(46) } - checkButton.snp.makeConstraints { make in - make.top.equalTo(textField.snp.bottom).offset(8) - make.leading.equalTo(textField.snp.leading).offset(61) - make.width.height.equalTo(24) + checkButtonWrapperView.snp.makeConstraints { + $0.height.equalTo(24) + $0.centerX.equalToSuperview() + $0.top.equalTo(textField.snp.bottom).offset(10) + $0.bottom.equalToSuperview() } - buttonTextLabel.snp.makeConstraints { make in - make.centerY.equalTo(checkButton.snp.centerY) - make.leading.equalTo(checkButton.snp.trailing).offset(5) + + checkButton.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.size.equalTo(24) } - cancelButton.snp.makeConstraints { make in - make.width.equalTo(135.5) - make.height.equalTo(48) - make.trailing.equalTo(containerView.snp.centerX).offset(-4) - make.bottom.equalTo(containerView.snp.bottom).offset(-16) + buttonTextLabel.snp.makeConstraints { + $0.centerY.equalTo(checkButton) + $0.leading.equalTo(checkButton.snp.trailing).offset(5) + $0.trailing.equalToSuperview() } - saveButton.snp.makeConstraints { make in - make.width.height.bottom.equalTo(cancelButton) - make.leading.equalTo(containerView.snp.centerX).offset(4) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) } } From 66adb93548ea1e42c66a62194b3cec46de83bdaa Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 03:52:55 +0900 Subject: [PATCH 041/101] =?UTF-8?q?feat:=20KoinModalViewController?= =?UTF-8?q?=EA=B0=80=20=ED=82=A4=EB=B3=B4=EB=93=9C=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalViewController.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index 307b1bf2..780b319d 100644 --- a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -17,6 +17,7 @@ class KoinModalViewController: UIViewController { private var subscriptions = Set() // MARK: - UI Components + private let containerLayoutGuide = UILayoutGuide() private let containerView = UIView() private var contentView: ModalContentView private let buttonView: ModalButtonView @@ -100,14 +101,16 @@ extension KoinModalViewController: UIViewControllerTransitioningDelegate { extension KoinModalViewController { private func setUpGestureRecognizer() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOutsideOfContainerView)) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAround)) view.addGestureRecognizer(tapGesture) } - @objc private func tapOutsideOfContainerView(_ sender: UITapGestureRecognizer) { + @objc private func didTapAround(_ sender: UITapGestureRecognizer) { let location = sender.location(in: view) if !containerView.frame.contains(location) { dismiss(animated: true) + } else { + view.endEditing(true) } } } @@ -136,6 +139,8 @@ extension KoinModalViewController { view.addSubview($0) } + view.addLayoutGuide(containerLayoutGuide) + if configuration.button == nil { buttonView.removeFromSuperview() } @@ -163,8 +168,14 @@ extension KoinModalViewController { } containerView.snp.makeConstraints { - $0.center.equalToSuperview() + $0.center.equalTo(containerLayoutGuide) $0.width.equalTo(configuration.layout.width) } + + containerLayoutGuide.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide.snp.top) + $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top) + $0.leading.trailing.equalToSuperview() + } } } From 7623835b12609b990bc048b4c65ada9ecad80a30 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 04:23:50 +0900 Subject: [PATCH 042/101] =?UTF-8?q?refactor:=20KoinModalViewController=20B?= =?UTF-8?q?utton=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalConfiguration.swift | 36 ++++---- .../Configuration/KoinModalStyle.swift | 4 + .../KoinModalViewController.swift | 44 ++++++++-- .../Subviews/ModalButtonView.swift | 82 +++++++++++++------ .../BusSearchDatePickerViewController.swift | 2 +- .../Chat/Chat/ChatViewController.swift | 2 +- .../ForceUpdateViewController.swift | 2 +- .../Login/Login/LoginViewController.swift | 2 +- .../LostItemDataViewController.swift | 8 +- .../LostItemKeywordViewController.swift | 2 +- .../LostItemListViewController.swift | 2 +- .../ManageNoticeKeywordViewController.swift | 2 +- .../RevokeModalViewController.swift | 2 +- .../ReviewListViewController.swift | 4 +- .../ModifyFrameModalViewController.swift | 2 +- 15 files changed, 128 insertions(+), 68 deletions(-) diff --git a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift index 63293038..cd3bf6f9 100644 --- a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift +++ b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift @@ -10,13 +10,13 @@ import UIKit struct KoinModalConfiguration { let appearance: Appearance let content: Content - let button: Button? + let button: Button let layout: Layout init( appearance: Appearance, content: Content, - button: Button?, + button: Button, layout: Layout = .init() ) { self.appearance = appearance @@ -53,30 +53,22 @@ struct KoinModalConfiguration { customView: UIView ) } - - struct Button { - let leftButtonTitle: String - let leftButtonAction: (()->Void)? - let leftButtonStyle: KoinModalStyle.ButtonStyle? - let rightButtonTitle: String - let rightButtonAction: ()->Void - let rightButtonStyle: KoinModalStyle.ButtonStyle? - - init( + + enum Button { + case buttons( leftButtonTitle: String, - leftButtonAction: (() -> Void)? = nil, + leftButtonAction: (()->Void)? = nil, leftButtonStyle: KoinModalStyle.ButtonStyle? = nil, rightButtonTitle: String, - rightButtonAction: @escaping () -> Void, + rightButtonAction: ()->Void, rightButtonStyle: KoinModalStyle.ButtonStyle? = nil - ) { - self.leftButtonTitle = leftButtonTitle - self.leftButtonAction = leftButtonAction - self.leftButtonStyle = leftButtonStyle - self.rightButtonTitle = rightButtonTitle - self.rightButtonAction = rightButtonAction - self.rightButtonStyle = rightButtonStyle - } + ) + case singleButton( + title: String, + action: (()->Void)?, + style: KoinModalStyle.ButtonStyle? + ) + case none } struct Layout { diff --git a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift index 4417f2d0..2624f782 100644 --- a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift +++ b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalStyle.swift @@ -14,6 +14,10 @@ struct KoinModalStyle { let leftButton: ButtonStyle let rightButton: ButtonStyle + var singleButton: ButtonStyle { + rightButton + } + struct TitleStyle { let textColor: ColorAsset let font: FontAsset diff --git a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index 780b319d..878c5c0c 100644 --- a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -16,6 +16,10 @@ class KoinModalViewController: UIViewController { private let configuration: KoinModalConfiguration private var subscriptions = Set() + private let leftButtonAction: (()->Void)? + private let rightButtonAction: (()->Void)? + private let singleButtonAction: (()->Void)? + // MARK: - UI Components private let containerLayoutGuide = UILayoutGuide() private let containerView = UIView() @@ -27,6 +31,21 @@ class KoinModalViewController: UIViewController { self.configuration = configuration self.contentView = ModalContentView(configuration: configuration) self.buttonView = ModalButtonView(configuration: configuration) + + switch configuration.button { + case .buttons(_, let leftButtonAction, _, _, let rightButtonAction, _): + self.leftButtonAction = leftButtonAction + self.rightButtonAction = rightButtonAction + self.singleButtonAction = nil + case .singleButton(_, let action, _): + self.leftButtonAction = nil + self.rightButtonAction = nil + self.singleButtonAction = action + default: + self.leftButtonAction = nil + self.rightButtonAction = nil + self.singleButtonAction = nil + } super.init(nibName: nil, bundle: nil) configureTransition() } @@ -52,17 +71,27 @@ class KoinModalViewController: UIViewController { buttonView.rightButtonTappedPublisher.sink { [weak self] in self?.rightButtonTapped() }.store(in: &subscriptions) + + buttonView.singleButtonTappedPublisher.sink { [weak self] in + self?.singleButtonTapped() + }.store(in: &subscriptions) } func leftButtonTapped() { dismiss(animated: true) { [weak self] in - self?.configuration.button?.leftButtonAction?() + self?.leftButtonAction?() } } func rightButtonTapped() { dismiss(animated: true) { [weak self] in - self?.configuration.button?.rightButtonAction() + self?.rightButtonAction?() + } + } + + func singleButtonTapped() { + dismiss(animated: true) { [weak self] in + self?.singleButtonAction?() } } } @@ -141,12 +170,17 @@ extension KoinModalViewController { view.addLayoutGuide(containerLayoutGuide) - if configuration.button == nil { + + switch configuration.button { + case .none: buttonView.removeFromSuperview() + case .buttons, .singleButton: + break } } private func setUpConstraints() { - if let _ = configuration.button { + switch configuration.button { + case .buttons, .singleButton: contentView.snp.makeConstraints { $0.top.equalToSuperview().offset(configuration.layout.contentTopPadding) $0.leading.equalToSuperview().offset(configuration.layout.contentHorizontalPadding) @@ -158,7 +192,7 @@ extension KoinModalViewController { $0.trailing.equalToSuperview().offset(-configuration.layout.buttonHorizontalPadding) $0.bottom.equalToSuperview().offset(-configuration.layout.buttonBottomPadding) } - } else { + case .none: contentView.snp.makeConstraints { $0.top.equalToSuperview().offset(configuration.layout.contentTopPadding) $0.leading.equalToSuperview().offset(configuration.layout.contentHorizontalPadding) diff --git a/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift b/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift index 2bccd40a..cebb2af8 100644 --- a/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift +++ b/Koin/Core/View/KoinModalViewController/Subviews/ModalButtonView.swift @@ -21,10 +21,12 @@ final class ModalButtonView: UIView { private let configuration: KoinModalConfiguration let leftButtonTappedPublisher = PassthroughSubject() let rightButtonTappedPublisher = PassthroughSubject() + let singleButtonTappedPublisher = PassthroughSubject() // MARK: - UI Components private let leftButton = UIButton() private let rightButton = UIButton() + private let singleButton = UIButton() // MARK: - Initializer init(configuration: KoinModalConfiguration) { @@ -40,6 +42,11 @@ final class ModalButtonView: UIView { } extension ModalButtonView { + private func setUpAddTargets() { + leftButton.addTarget(self, action: #selector(onLeftButtonTapped), for: .touchUpInside) + rightButton.addTarget(self, action: #selector(onRightButtonTapped), for: .touchUpInside) + singleButton.addTarget(self, action: #selector(onSingleButtonTapped), for: .touchUpInside) + } // MARK: - Objc @objc private func onLeftButtonTapped() { @@ -48,12 +55,8 @@ extension ModalButtonView { @objc private func onRightButtonTapped() { rightButtonTappedPublisher.send() } -} - -extension ModalButtonView { - private func setUpAddTargets() { - leftButton.addTarget(self, action: #selector(onLeftButtonTapped), for: .touchUpInside) - rightButton.addTarget(self, action: #selector(onRightButtonTapped), for: .touchUpInside) + @objc private func onSingleButtonTapped() { + singleButtonTappedPublisher.send() } } @@ -65,33 +68,60 @@ extension ModalButtonView { } private func setUpStyles() { - guard let button = configuration.button else { return } - - let leftButtonTitle = button.leftButtonTitle - let leftButtonStyle = button.leftButtonStyle ?? configuration.style.leftButton - let rightButtonTitle = button.rightButtonTitle - let rightButtonStyle = button.rightButtonStyle ?? configuration.style.rightButton - - apply(title: leftButtonTitle, style: leftButtonStyle, to: leftButton) - apply(title: rightButtonTitle, style: rightButtonStyle, to: rightButton) + switch configuration.button { + case .buttons(let leftButtonTitle, _, let leftButtonStyle, let rightButtonTitle, _, let rightButtonStyle): + let leftButtonTitle = leftButtonTitle + let leftButtonStyle = leftButtonStyle ?? configuration.style.leftButton + let rightButtonTitle = rightButtonTitle + let rightButtonStyle = rightButtonStyle ?? configuration.style.rightButton + + apply(title: leftButtonTitle, style: leftButtonStyle, to: leftButton) + apply(title: rightButtonTitle, style: rightButtonStyle, to: rightButton) + case .singleButton(let title, _, let style): + let title = title + let style = style ?? configuration.style.singleButton + + apply(title: title, style: style, to: singleButton) + case .none: + return + } } private func setUpLayouts() { - [leftButton, rightButton].forEach { - addSubview($0) + switch configuration.button { + case .buttons: + [leftButton, rightButton].forEach { + addSubview($0) + } + case .singleButton: + [singleButton].forEach { + addSubview($0) + } + case .none: + return } } private func setUpConstraints() { - leftButton.snp.makeConstraints { - $0.height.equalTo(Layout.ButtonHeight) - $0.top.leading.bottom.equalToSuperview() - } - rightButton.snp.makeConstraints { - $0.height.equalTo(Layout.ButtonHeight) - $0.width.equalTo(leftButton.snp.width) - $0.leading.equalTo(leftButton.snp.trailing).offset(Layout.PaddingBetweenButtons) - $0.top.trailing.bottom.equalToSuperview() + switch configuration.button { + case .buttons: + leftButton.snp.makeConstraints { + $0.height.equalTo(Layout.ButtonHeight) + $0.top.leading.bottom.equalToSuperview() + } + rightButton.snp.makeConstraints { + $0.height.equalTo(Layout.ButtonHeight) + $0.width.equalTo(leftButton.snp.width) + $0.leading.equalTo(leftButton.snp.trailing).offset(Layout.PaddingBetweenButtons) + $0.top.trailing.bottom.equalToSuperview() + } + case .singleButton: + singleButton.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalTo(Layout.ButtonHeight) + } + case .none: + return } } } diff --git a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift index 59d09303..a200a133 100644 --- a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift @@ -83,7 +83,7 @@ final class BusSearchDatePickerViewController: KoinModalViewController { super.init(configuration: .init( appearance: .primary, content: .custom(customView: customView), - button: .init( + button: .buttons( leftButtonTitle: "지금 출발", leftButtonAction: onLeftButtonTapped, leftButtonStyle: .init( diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/Chat/Chat/ChatViewController.swift index 68561f97..b30a46f5 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/Chat/Chat/ChatViewController.swift @@ -179,7 +179,7 @@ extension ChatViewController{ mainTitleText: "이 사용자를 차단하시겠습니까?", subTitleText: "쪽지 수신 및 발신이 모두 차단됩니다." ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", rightButtonTitle: "차단하기", rightButtonAction: { [weak self] in diff --git a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift index 552c37b5..c7ff12f5 100644 --- a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift +++ b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift @@ -186,7 +186,7 @@ extension ForceUpdateViewController { mainTitleText: "이미 업데이트 하셨나요?", subTitleText: "업데이트 이후에도 이 화면이 나타나는\n경우에는 스토어에서 코인을\n삭제 후 재설치 해 주세요." ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", leftButtonAction: { [weak self] in self?.openStore() diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index 241bc9b6..be6094c9 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -180,7 +180,7 @@ extension LoginViewController { fontSize: 12 ) ), - button: .init( + button: .buttons( leftButtonTitle: "나중에 하기", leftButtonAction: { [weak self] in self?.navigationController?.popViewController(animated: true) diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift index 97d3514d..b31edbc7 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift @@ -249,7 +249,7 @@ extension LostItemDataViewController { let modalViewController = KoinModalViewController(configuration: .init( appearance: .primary, content: .singleTitle(text: "삭제 시 되돌릴 수 없습니다.\n게시글을 삭제하시겠습니까?"), - button: .init( + button: .buttons( leftButtonTitle: "취소", rightButtonTitle: "확인", rightButtonAction: onRightButtonTapped @@ -280,7 +280,7 @@ extension LostItemDataViewController { let modalViewController = KoinModalViewController(configuration: .init( appearance: .primary, content: .singleTitle(text: "상태 변경 시 되돌릴 수 없습니다.\n찾음으로 변경하시겠습니까?"), - button: .init( + button: .buttons( leftButtonTitle: "취소", rightButtonTitle: "확인", rightButtonAction: onRightButtonTapped @@ -331,7 +331,7 @@ extension LostItemDataViewController { mainTitleText: "쪽지를 보내려면\n로그인이 필요해요.", subTitleText: "로그인 후 대화를 시작하세요!" ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", leftButtonAction: onLeftButtonTapped, rightButtonTitle: "로그인하기", @@ -368,7 +368,7 @@ extension LostItemDataViewController { mainTitleText: "게시글을 신고하려면\n로그인이 필요해요.", subTitleText: "로그인 후 이용해주세요." ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", rightButtonTitle: "로그인하기", rightButtonAction: onRightButtonTapped diff --git a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift index 47129ffe..471bb1c5 100644 --- a/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift +++ b/Koin/Presentation/LostItem/LostItemKeyword/LostItemKeywordViewController.swift @@ -201,7 +201,7 @@ extension LostItemKeywordViewController { mainTitleText: "키워드 알림을 받으려면\n로그인이 필요해요.", subTitleText: "로그인 후 간편하게 분실물 키워드\n알림을 받아보세요!" ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", rightButtonTitle: "로그인하기", rightButtonAction: onRightButtonTapped diff --git a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift index 8ef699f6..72979769 100644 --- a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift +++ b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift @@ -233,7 +233,7 @@ extension LostItemListViewController { mainTitleText: "게시글을 작성하려면\n로그인이 필요해요.", subTitleText: "로그인 후 글을 작성해주세요!" ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", leftButtonAction: onLeftButtonTapped, rightButtonTitle: "로그인하기", diff --git a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift index 61b7219b..7a54ec76 100644 --- a/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift +++ b/Koin/Presentation/Notice/ManageNoticeKeyWord/ManageNoticeKeywordViewController.swift @@ -226,7 +226,7 @@ extension ManageNoticeKeywordViewController { mainTitleText: "키워드 알림을 받으려면\n로그인이 필요해요.", subTitleText: "로그인 후 간편하게 공지사항 키워드\n알림을 받아보세요!" ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", leftButtonAction: onLeftButtonTapped, rightButtonTitle: "로그인하기", diff --git a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift index bc2ceec5..5fb98f76 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/RevokeModalViewController.swift @@ -37,7 +37,7 @@ final class RevokeModalViewController: KoinModalViewController { mainTitle: mainTitle, subTitle: subTitle ), - button: .init( + button: .buttons( leftButtonTitle: "취소", rightButtonTitle: "회원탈퇴", rightButtonAction: onRevokeButtonTapped diff --git a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift index e7b560f3..4a8e31bd 100644 --- a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift @@ -314,7 +314,7 @@ extension ReviewListViewController { let modalViewController = KoinModalViewController(configuration: .init( appearance: .new, content: .singleTitle(text: "삭제한 리뷰는 되돌릴 수 없습니다.\n삭제 하시겠습니까?"), - button: .init( + button: .buttons( leftButtonTitle: "취소하기", leftButtonAction: { [weak self] in self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) @@ -352,7 +352,7 @@ extension ReviewListViewController { mainTitleText: mainText, subTitleText: subText ), - button: .init( + button: .buttons( leftButtonTitle: "닫기", leftButtonAction: { [weak self] in self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift index 3c06f2c7..320032ad 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift @@ -36,7 +36,7 @@ final class ModifyFrameModalViewController: KoinModalViewController { super.init(configuration: .init( appearance: .primary, content: .custom(customView: containerView), - button: .init( + button: .buttons( leftButtonTitle: "취소", rightButtonTitle: "저장", rightButtonAction: {} //rightButtonAction From dda8ebc88c90dc1dc961fa14fd393bcd47404fab Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 04:30:46 +0900 Subject: [PATCH 043/101] =?UTF-8?q?delete:=20CallVanModalViewController=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalConfiguration.swift | 4 +- .../CallVanListViewController.swift | 28 ++-- .../Subviews/CallVanModalViewController.swift | 137 ------------------ .../CallVanPostViewController.swift | 33 +++-- koin.xcodeproj/project.pbxproj | 4 - 5 files changed, 38 insertions(+), 168 deletions(-) delete mode 100644 Koin/Presentation/CallVan/CallVanList/Subviews/CallVanModalViewController.swift diff --git a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift index cd3bf6f9..2ed1fcd3 100644 --- a/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift +++ b/Koin/Core/View/KoinModalViewController/Configuration/KoinModalConfiguration.swift @@ -65,8 +65,8 @@ struct KoinModalConfiguration { ) case singleButton( title: String, - action: (()->Void)?, - style: KoinModalStyle.ButtonStyle? + action: (()->Void)? = nil, + style: KoinModalStyle.ButtonStyle? = nil ) case none } diff --git a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift index 8fd2cf73..a7a82d30 100644 --- a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift +++ b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift @@ -390,23 +390,27 @@ extension CallVanListViewController { } private func showRestrictedModal(type: RestrictionType?, until: String?) { - let modalViewController: CallVanModalViewController + let mainTitle: String + let subTitle: String + switch type { case .temporaryRestriction14Days: - guard let until else { - return - } - modalViewController = CallVanModalViewController( - title: RestrictionType.temporaryRestriction14Days.rawValue, - description: RestrictionType.temporaryRestriction14Days.getDescription(until: until)) + mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + subTitle = RestrictionType.temporaryRestriction14Days.getDescription(until: until) case .permanentRestriction: - modalViewController = CallVanModalViewController( - title: RestrictionType.temporaryRestriction14Days.rawValue, - description: RestrictionType.permanentRestriction.getDescription()) - default: + mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + subTitle = RestrictionType.permanentRestriction.getDescription() + case nil: return } - modalViewController.modalPresentationStyle = .overFullScreen + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .titles( + mainTitleText: mainTitle, + subTitleText: subTitle + ), + button: .singleButton(title: "닫기") + )) present(modalViewController, animated: false) } diff --git a/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanModalViewController.swift b/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanModalViewController.swift deleted file mode 100644 index 5625e6cb..00000000 --- a/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanModalViewController.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// CallVanModalViewController.swift -// koin -// -// Created by 홍기정 on 4/6/26. -// - -import UIKit -import SnapKit -import Then - -final class CallVanModalViewController: UIViewController { - - // MARK: - UI Components - private let dimView = UIView() - private let modalView = UIView() - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let closeButton = UIButton() - - // MARK: - Initializer - init(title: String, description: String) { - super.init(nibName: nil, bundle: nil) - titleLabel.attributedText = NSAttributedString( - string: title, - attributes: [ - .font : UIFont.appFont(.pretendardMedium, size: 18), - .foregroundColor : UIColor.appColor(.neutral600) - ] - ) - let paragraphStyle = NSMutableParagraphStyle() - let font = UIFont.appFont(.pretendardRegular, size: 14) - paragraphStyle.lineSpacing = font.lineHeight * 0.6 - descriptionLabel.attributedText = NSAttributedString( - string: description, - attributes: [ - .font : font, - .foregroundColor : UIColor.appColor(.gray), - .paragraphStyle : paragraphStyle - ] - ) - } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - configureView() - setAddTargets() - } -} - -extension CallVanModalViewController { - - private func setAddTargets() { - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismiss(animated: false) - } -} - -extension CallVanModalViewController { - - private func configureView() { - setUpStyles() - setUpLayouts() - setUpConstraints() - } - - private func setUpStyles() { - dimView.do { - $0.alpha = 0.7 - $0.backgroundColor = UIColor.black - } - modalView.do { - $0.backgroundColor = UIColor.white - $0.layer.cornerRadius = 8 - } - descriptionLabel.do { - $0.numberOfLines = 2 - $0.textAlignment = .center - } - closeButton.do { - $0.setAttributedTitle( - NSAttributedString( - string: "닫기", - attributes: [ - .font : UIFont.appFont(.pretendardMedium, size: 15), - .foregroundColor : UIColor.white - ]), - for: .normal - ) - $0.backgroundColor = UIColor.appColor(.new500) - $0.layer.cornerRadius = 8 - $0.clipsToBounds = true - } - } - - private func setUpLayouts() { - [titleLabel, descriptionLabel, closeButton].forEach { - modalView.addSubview($0) - } - [dimView, modalView].forEach { - view.addSubview($0) - } - } - - private func setUpConstraints() { - dimView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - modalView.snp.makeConstraints { - $0.center.equalToSuperview() - $0.width.equalTo(300) - } - titleLabel.snp.makeConstraints { - $0.height.equalTo(29) - $0.centerX.equalToSuperview() - $0.top.equalToSuperview().offset(24) - } - descriptionLabel.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(titleLabel.snp.bottom).offset(8) - } - closeButton.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(descriptionLabel.snp.bottom).offset(24) - $0.height.equalTo(48) - $0.bottom.equalToSuperview().offset(-24) - $0.leading.trailing.equalToSuperview().inset(32) - } - } -} diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index ec4cd39f..f139bcb5 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -146,25 +146,32 @@ extension CallVanPostViewController { extension CallVanPostViewController { private func showRestrictedModal(_ type: RestrictionType?, _ until: String?) { - let modalViewController: CallVanModalViewController + let mainTitle: String + let subTitle: String + switch type { case .temporaryRestriction14Days: - guard let until else { - return - } - modalViewController = CallVanModalViewController( - title: RestrictionType.temporaryRestriction14Days.rawValue, - description: RestrictionType.temporaryRestriction14Days.getDescription(until: until)) + mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + subTitle = RestrictionType.temporaryRestriction14Days.getDescription(until: until) case .permanentRestriction: - modalViewController = CallVanModalViewController( - title: RestrictionType.temporaryRestriction14Days.rawValue, - description: RestrictionType.permanentRestriction.getDescription()) - default: + mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + subTitle = RestrictionType.permanentRestriction.getDescription() + case nil: return } - modalViewController.modalPresentationStyle = .overFullScreen + + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .titles( + mainTitleText: mainTitle, + subTitleText: subTitle + ), + button: .singleButton( + title: "닫기" + ) + )) present(modalViewController, animated: false) - } + } private func presentDeparturePlaceBottomSheet() { let onApplyButtonTapped: (CallVanPlace, String?)->Void = { [weak self] (place, customPlace) in diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 90dfaae4..0ebbc383 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -226,7 +226,6 @@ 7C7BD723302B3D8F003C5A15 /* NoticeAISummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */; }; 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */; }; 7C7BD727302D8002003C5A15 /* NoticeAISummaryDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BD726302D8002003C5A15 /* NoticeAISummaryDto.swift */; }; - 7C7CCE4A2F834D4B00E3A54B /* CallVanModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */; }; 7C7CCE702F866E9400E3A54B /* AppPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */; }; 7C7CCE722F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */; }; 7C7CCE742F86866B00E3A54B /* CallVanRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE732F86866B00E3A54B /* CallVanRestriction.swift */; }; @@ -1253,7 +1252,6 @@ 7C7BD722302B3D8F003C5A15 /* NoticeAISummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummary.swift; sourceTree = ""; }; 7C7BD724302B3E25003C5A15 /* NoticeAISummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummaryView.swift; sourceTree = ""; }; 7C7BD726302D8002003C5A15 /* NoticeAISummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAISummaryDto.swift; sourceTree = ""; }; - 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanModalViewController.swift; sourceTree = ""; }; 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPath.swift; sourceTree = ""; }; 7C7CCE712F86863200E3A54B /* FetchCallVanRestrictionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCallVanRestrictionUseCase.swift; sourceTree = ""; }; 7C7CCE732F86866B00E3A54B /* CallVanRestriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanRestriction.swift; sourceTree = ""; }; @@ -3108,7 +3106,6 @@ 7CC42B292F575F4D00940CE1 /* CallVanFilterButton.swift */, 7CC42B272F575E8100940CE1 /* CallVanListFilterViewController.swift */, 7CC42B2E2F58105B00940CE1 /* CallVanBottomSheetViewController.swift */, - 7C7CCE492F834D4B00E3A54B /* CallVanModalViewController.swift */, ); path = Subviews; sourceTree = ""; @@ -6100,7 +6097,6 @@ 7CED91372EF2D1F900457128 /* ReviewListHeaderView.swift in Sources */, 7CED91382EF2D1F900457128 /* ShopReviewViewModel.swift in Sources */, 7C61D3432F220E8C005FAC3A /* EditLostItemHeaderView.swift in Sources */, - 7C7CCE4A2F834D4B00E3A54B /* CallVanModalViewController.swift in Sources */, 7CED91392EF2D1F900457128 /* DiningNotiContentViewController.swift in Sources */, 7CED913A2EF2D1F900457128 /* CalendarCollectionViewCell.swift in Sources */, 7CED913B2EF2D1F900457128 /* AddLostItemFooterView.swift in Sources */, From c739ae6e10561d7ace6493f2550133f89fb179f7 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 04:44:07 +0900 Subject: [PATCH 044/101] =?UTF-8?q?refactor:=20DeleteLectureModalViewContr?= =?UTF-8?q?oller=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeleteLectureModalViewController.swift | 157 +++++------------- .../Timetable/TimetableViewController.swift | 7 +- 2 files changed, 41 insertions(+), 123 deletions(-) diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift index 62517a39..9c1274ce 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift @@ -7,130 +7,49 @@ import UIKit -final class DeleteLectureModalViewController: UIViewController { +final class DeleteLectureModalViewController: KoinModalViewController { + // MARK: - Properties private let onDeleteButtonTapped: (LectureData) -> Void - private var lectureData: LectureData? = nil + private let lectureData: LectureData - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 17) - $0.textColor = UIColor.appColor(.neutral800) - $0.numberOfLines = 0 - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let deleteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.danger700) - $0.setTitle("삭제하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() - - init(onDeleteButtonTapped: @escaping (LectureData) -> Void) { + // MARK: - Initializer + init( + lectureData: LectureData, + onDeleteButtonTapped: @escaping (LectureData) -> Void + ) { + self.lectureData = lectureData self.onDeleteButtonTapped = onDeleteButtonTapped - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - modalPresentationStyle = .overFullScreen - modalTransitionStyle = .crossDissolve - configureView() - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) - } - - @objc private func deleteButtonTapped() { - dismiss(animated: true, completion: nil) - if let lectureData = lectureData { - onDeleteButtonTapped(lectureData) - } - } -} - -extension DeleteLectureModalViewController { - - func setMessageLabelText(lectureData: LectureData) { - self.lectureData = lectureData - let lectureName = lectureData.name - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - paragraphStyle.alignment = .center - let text = "\(lectureName)\(lectureName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?\n삭제한 강의는 수업추가에서\n다시 추가할 수 있어요." - let attributedString = NSMutableAttributedString(string: text) - let range1 = (text as NSString).range(of: "삭제") - let range2 = (text as NSString).range(of: "수업추가") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range1) - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.primary500), range: range2) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - messageLabel.attributedText = attributedString - } - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, closeButton, deleteButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(301) - make.height.equalTo(194) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.centerX.equalTo(containerView.snp.centerX) - } - closeButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - deleteButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) + let attributedString = { + let lectureName = lectureData.name + let text = "\(lectureName)\(lectureName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?\n삭제한 강의는 수업추가에서\n다시 추가할 수 있어요." + let attributedString = NSMutableAttributedString(string: text) + let range1 = (text as NSString).range(of: "삭제") + let range2 = (text as NSString).range(of: "수업추가") + attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range1) + attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.primary500), range: range2) + return attributedString + }() + super.init(configuration: .init( + appearance: .destructive, + content: .attributedSingleTitle(title: attributedString), + button: .buttons( + leftButtonTitle: "취소", + rightButtonTitle: "삭제하기", + rightButtonAction: {} + ) + )) + } + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Override + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + onDeleteButtonTapped(lectureData) } } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 287ecdb4..0a6d3049 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -281,11 +281,10 @@ extension TimetableViewController { } private func presentDeleteLectureModal(lecture: LectureData) { - let viewController = DeleteLectureModalViewController(onDeleteButtonTapped: { [weak self] lecture in + let modalViewController = DeleteLectureModalViewController(lectureData: lecture) { [weak self] lecture in self?.inputSubject.send(._deleteLecture(lecture)) - }) - viewController.setMessageLabelText(lectureData: lecture) - present(viewController, animated: false) + } + present(modalViewController, animated: false) } } From a2c0265964603f182cd41ea7be148ad4803e0ff6 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 14:51:54 +0900 Subject: [PATCH 045/101] =?UTF-8?q?refactor:=20SelectDeptModalViewControll?= =?UTF-8?q?er=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SelectDeptModalViewController.swift | 233 ++++++++++-------- .../Timetable/TimetableViewController.swift | 6 +- 2 files changed, 131 insertions(+), 108 deletions(-) diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift index 9c4054f8..d7bf6713 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift @@ -7,18 +7,17 @@ import UIKit -final class SelectDeptModalViewController: UIViewController { - - private let onDepartmentSelected: (String?) -> Void +final class SelectDeptModalViewController: KoinModalViewController { - init(onDepartmentSelected: @escaping (String?) -> Void) { - self.onDepartmentSelected = onDepartmentSelected - super.init(nibName: nil, bundle: nil) + private enum Layout { + static let buttonsPerRow: Int = 2 + static let gridSpacing: CGFloat = 4.8 + static let departmentButtonHeight: CGFloat = 32 + static let departmentButtonCornerRadius: CGFloat = 4 } - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private var departmentButtons: [UIButton] = [] + // MARK: - Properties + private let onDepartmentSelected: (String?) -> Void private let departments = [ "디자인ㆍ건축공학부", "고용서비스정책학과", @@ -35,103 +34,116 @@ final class SelectDeptModalViewController: UIViewController { ] private var selectedDepartment: String? = nil private var selectedButton: UIButton? = nil - - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 18) - $0.textColor = UIColor.appColor(.primary500) - $0.text = "전공선택" - } - - private let completeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("완료", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true + + // MARK: - UI Components + private let containerView = UIView() + private let messageLabel = UILabel() + private let gridStackView = UIStackView() + private var departmentButtons: [UIButton] = [] + private let completeButton = UIButton() + + // MARK: - Initializer + init( + onDepartmentSelected: @escaping (String?) -> Void + ) { + self.onDepartmentSelected = onDepartmentSelected + + super.init(configuration: .init( + appearance: .primary, + content: .custom(customView: containerView), + button: .none, + layout: .init( + width: 327, + contentTopPadding: 18, + contentHorizontalPadding: 12, + contentBottomPadding: 18 + ) + )) } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() - - private let gridStackView = UIStackView().then { - $0.axis = .vertical - $0.spacing = 4.8 - $0.distribution = .fillEqually + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() configureView() - modalPresentationStyle = .overFullScreen - modalTransitionStyle = .crossDissolve + setUpDepartments() + setAddTarget() + } +} + +extension SelectDeptModalViewController { + private func setAddTarget() { completeButton.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside) - setupDepartments() } @objc private func completeButtonTapped() { onDepartmentSelected(selectedDepartment) - dismiss(animated: true, completion: nil) + dismiss(animated: true) } +} - private func setupDepartments() { - departmentButtons.forEach { $0.removeFromSuperview() } - departmentButtons.removeAll() - - let buttonsPerRow = 2 +extension SelectDeptModalViewController { + private func setUpDepartments() { var currentRow: UIStackView? = nil - gridStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - + for (index, department) in departments.enumerated() { let button = createDepartmentButton(title: department) departmentButtons.append(button) - if index % buttonsPerRow == 0 { - currentRow = UIStackView() - currentRow?.axis = .horizontal - currentRow?.spacing = 4.8 - currentRow?.distribution = .fillEqually + + if index % Layout.buttonsPerRow == 0 { + currentRow = makeRowStackView() gridStackView.addArrangedSubview(currentRow!) } - + currentRow?.addArrangedSubview(button) } - - view.layoutIfNeeded() } - + + private func makeRowStackView() -> UIStackView { + let stackView = UIStackView() + stackView.do { + $0.axis = .horizontal + $0.spacing = Layout.gridSpacing + $0.distribution = .fillEqually + } + return stackView + } + private func createDepartmentButton(title: String) -> UIButton { let button = UIButton(type: .system) - button.setTitle(title, for: .normal) - button.setTitleColor(UIColor.appColor(.neutral800), for: .normal) - button.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 14) - button.layer.cornerRadius = 4 - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.appColor(.neutral300).cgColor - button.backgroundColor = .white - button.addTarget(self, action: #selector(departmentButtonTapped(_:)), for: .touchUpInside) + button.do { + $0.setTitle(title, for: .normal) + $0.titleLabel?.font = .appFont(.pretendardMedium, size: 14) + $0.layer.cornerRadius = Layout.departmentButtonCornerRadius + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.appColor(.neutral300).cgColor + $0.addTarget(self, action: #selector(departmentButtonTapped(_:)), for: .touchUpInside) + } + button.snp.makeConstraints { + $0.height.equalTo(Layout.departmentButtonHeight) + } + apply(isSelected: false, to: button) return button } - + + private func apply(isSelected: Bool, to button: UIButton) { + button.backgroundColor = .appColor(isSelected ? .primary500 : .neutral0) + button.setTitleColor(.appColor(isSelected ? .neutral0 : .neutral800), for: .normal) + } + + // MARK: - Objc @objc private func departmentButtonTapped(_ sender: UIButton) { guard let department = sender.title(for: .normal) else { return } - + if selectedButton == sender { - sender.backgroundColor = .white - sender.setTitleColor(UIColor.appColor(.neutral800), for: .normal) + apply(isSelected: false, to: sender) selectedButton = nil selectedDepartment = nil } else { - departmentButtons.forEach { button in - button.backgroundColor = .white - button.setTitleColor(UIColor.appColor(.neutral800), for: .normal) - } - sender.backgroundColor = UIColor.appColor(.primary500) - sender.setTitleColor(.white, for: .normal) + departmentButtons.forEach { apply(isSelected: false, to: $0) } + apply(isSelected: true, to: sender) selectedButton = sender selectedDepartment = department } @@ -139,44 +151,55 @@ final class SelectDeptModalViewController: UIViewController { } extension SelectDeptModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + messageLabel.do { + $0.text = "전공선택" + $0.font = .appFont(.pretendardMedium, size: 18) + $0.textColor = .appColor(.primary500) + } + + gridStackView.do { + $0.axis = .vertical + $0.spacing = Layout.gridSpacing + $0.distribution = .fill } + + completeButton.do { + $0.backgroundColor = UIColor.appColor(.primary500) + $0.setTitle("완료", for: .normal) + $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) + $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) + $0.layer.cornerRadius = 4 + $0.layer.masksToBounds = true + } + } + + private func setUpLayouts() { [messageLabel, gridStackView, completeButton].forEach { containerView.addSubview($0) } } - + private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(327) - make.height.equalTo(323) + messageLabel.snp.makeConstraints { + $0.top.leading.equalToSuperview() + $0.height.equalTo(29) } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(18) - make.leading.equalTo(containerView.snp.leading).offset(12) + gridStackView.snp.makeConstraints { + $0.top.equalTo(messageLabel.snp.bottom).offset(6) + $0.leading.trailing.equalToSuperview() } - gridStackView.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(10) - make.leading.equalTo(containerView.snp.leading).offset(12) - make.trailing.equalTo(containerView.snp.trailing).offset(-12) - make.height.equalTo(216) + completeButton.snp.makeConstraints { + $0.width.equalTo(60) + $0.height.equalTo(30) + $0.top.equalTo(gridStackView.snp.bottom).offset(6) + $0.trailing.bottom.equalToSuperview() } - completeButton.snp.makeConstraints { make in - make.trailing.equalTo(containerView.snp.trailing).offset(-12) - make.bottom.equalTo(containerView.snp.bottom).offset(-18) - make.width.equalTo(60) - make.height.equalTo(30) - } - } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) } } diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 0a6d3049..4fcdc31d 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -274,10 +274,10 @@ extension TimetableViewController { } private func presentSelectDeptModal() { - let viewController = SelectDeptModalViewController(onDepartmentSelected: { [weak self] department in + let viewController = SelectDeptModalViewController { [weak self] department in self?.addClassCollectionView.setUpSelectedDept(dept: department) - }) - present(viewController, animated: false) + } + present(viewController, animated: true) } private func presentDeleteLectureModal(lecture: LectureData) { From 9bb9e08373d6de479761570b11977a85a7184317 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 15:03:44 +0900 Subject: [PATCH 046/101] =?UTF-8?q?refactor:=20SubstituteTimetableModalVie?= =?UTF-8?q?wController=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...bstituteTimetableModalViewController.swift | 184 ++++++------------ .../Timetable/TimetableViewController.swift | 5 +- 2 files changed, 63 insertions(+), 126 deletions(-) diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift index 1cf03747..2595ccd6 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/SubstituteTimetableModalViewController.swift @@ -7,146 +7,86 @@ import UIKit -final class SubstituteTimetableModalViewController: UIViewController { +final class SubstituteTimetableModalViewController: KoinModalViewController { + // MARK: - Properties private let onSubstituteButtonTapped: (Any) -> Void + + // MARK: - State private var lectureData: LectureData? private var customLecture: (String, [Int])? - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardBold, size: 16) - $0.textColor = UIColor.appColor(.neutral800) - $0.text = "시간표가 중복돼요." - } - - private let subMessageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - $0.textColor = UIColor.appColor(.neutral600) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - let text = "추가하시려는 시간에 이미 다른 강의가\n있어요. 새로운 강의로 대체하시겠어요?" - let attributedString = NSMutableAttributedString(string: text) - let loginRange = (text as NSString).range(of: "새로운 강의로 대체") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.warning500), range: loginRange) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - $0.attributedText = attributedString - $0.textAlignment = .center - $0.numberOfLines = 2 - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let substituteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("대체하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() - + // MARK: - Initializer init(onSubstituteButtonTapped: @escaping (Any) -> Void) { self.onSubstituteButtonTapped = onSubstituteButtonTapped - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - substituteButton.addTarget(self, action: #selector(substituteButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) + + let mainTitle = NSAttributedString( + string: "시간표가 중복돼요.", + attributes: [ + .font: UIFont.appFont(.pretendardBold, size: 16), + .foregroundColor: UIColor.appColor(.neutral800) + ] + ) + + let subTitle = { + let fullText = "추가하시려는 시간에 이미 다른 강의가\n있어요. 새로운 강의로 대체하시겠어요?" + let highlightedText = "새로운 강의로 대체" + + let highlightRange = (fullText as NSString).range(of: highlightedText) + + let attributedString = NSMutableAttributedString( + string: fullText, + attributes: [ + .font: UIFont.appFont(.pretendardRegular, size: 14), + .foregroundColor: UIColor.appColor(.neutral600) + ] + ) + + attributedString.addAttribute( + .foregroundColor, + value: UIColor.appColor(.warning500), + range: highlightRange + ) + return attributedString + }() + + super.init(configuration: .init( + appearance: .primary, + content: .attributedTitles( + mainTitle: mainTitle, + subTitle: subTitle + ), + button: .buttons( + leftButtonTitle: "취소", + rightButtonTitle: "대체하기", + rightButtonAction: {} + ) + )) } - - @objc private func substituteButtonTapped() { - dismiss(animated: true, completion: nil) - if let lectureData = lectureData { - onSubstituteButtonTapped(lectureData) - } else if let customLecture = customLecture { - onSubstituteButtonTapped(customLecture) - } + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } + // MARK: - Public func configure(lectureData: LectureData) { - self.lectureData = nil - self.customLecture = nil - self.lectureData = lectureData + self.customLecture = nil } + func configure(customLecture: (String, [Int])) { self.lectureData = nil - self.customLecture = nil - self.customLecture = customLecture } -} - -extension SubstituteTimetableModalViewController { - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, subMessageLabel, closeButton, substituteButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(301) - make.height.equalTo(198) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(24) - make.centerX.equalTo(containerView.snp.centerX) - } - subMessageLabel.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(16) - make.centerX.equalTo(containerView.snp.centerX) - } - closeButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - substituteButton.snp.makeConstraints { make in - make.top.equalTo(subMessageLabel.snp.bottom).offset(24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) + // MARK: - Override + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + if let lectureData { + onSubstituteButtonTapped(lectureData) + } else if let customLecture { + onSubstituteButtonTapped(customLecture) + } } } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 4fcdc31d..ad0556be 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -250,11 +250,8 @@ extension TimetableViewController { } private func makeSubstituteTimetableModal() -> SubstituteTimetableModalViewController { - SubstituteTimetableModalViewController(onSubstituteButtonTapped: { [weak self] response in + SubstituteTimetableModalViewController { [weak self] response in self?.handleSubstituteResponse(response) - }).then { - $0.modalPresentationStyle = .overFullScreen - $0.modalTransitionStyle = .crossDissolve } } From f9cf0c3460ce26beb89955a7b9ec76994cd68690 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 15:35:57 +0900 Subject: [PATCH 047/101] =?UTF-8?q?refactor:=20ModifySemesterModalViewCont?= =?UTF-8?q?roller=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameList/FrameListViewController.swift | 6 +- .../ModifySemesterModalViewController.swift | 413 ++++++++---------- 2 files changed, 196 insertions(+), 223 deletions(-) diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index 8b8ea403..36272ee5 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -96,11 +96,9 @@ extension FrameListViewController: TimetableCellDelegate { let modifySemesterModalViewController = ModifySemesterModalViewController( onApplyButtonTapped: { [weak self] addedSemesters, removedSemesters in self?.applySemesterChanges(addedSemesters: addedSemesters, removedSemesters: removedSemesters) - }, - width: 327, - height: 232 + } ) - modifySemesterModalViewController.configre(frameList: viewModel.frameData) + modifySemesterModalViewController.configure(frameList: viewModel.frameData) present(modifySemesterModalViewController, animated: true) } diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift index e66c3705..7ff6e2c5 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifySemesterModalViewController.swift @@ -7,192 +7,170 @@ import UIKit -final class ModifySemesterModalViewController: UIViewController { +final class ModifySemesterModalViewController: KoinModalViewController { + + private enum Layout { + static let yearButtonWidth: CGFloat = 90 + static let yearButtonHeight: CGFloat = 24 + static let messageLabelTopOffset: CGFloat = 12 + static let messageLabelHeight: CGFloat = 26 + static let semesterButtonWidth: CGFloat = 125 + static let semesterButtonHeight: CGFloat = 40 + static let semesterButtonCornerRadius: CGFloat = 4 + static let paddingBetweenMessageAndSemester: CGFloat = 10 + static let paddingBetweenSemesterRows: CGFloat = 10 + } + + private enum SemesterState: Int { + case unselected = 0 + case added = 1 + case existing = 2 + case removed = 3 + } + + // MARK: - Properties private let onApplyButtonTapped: ([String], [String]) -> Void - var frameList: [FrameData] = [] - var containerWidth: CGFloat - var containerHeight: CGFloat - + + // MARK: - State + private var frameList: [FrameData] = [] private var selectedYear: String = { let currentYear = Calendar.current.component(.year, from: Date()) return "\(currentYear)" }() - private var selectedFrames: Set = [] - - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() - - private let selectYearButton = UIButton().then { - let currentYear = Calendar.current.component(.year, from: Date()) - $0.backgroundColor = UIColor.appColor(.neutral300) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 12) - $0.setTitle("\(currentYear)", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral800), for: .normal) + private var semesterMapping: [(button: UIButton, semester: String)] { + [ + (firstSemesterButton, "\(selectedYear)1"), + (summerSemesterButton, "\(selectedYear)-여름"), + (secondSemesterButton, "\(selectedYear)2"), + (winterSemesterButton, "\(selectedYear)-겨울") + ] } - private let messageLabel = UILabel().then { - $0.text = "학기 편집" - $0.font = UIFont.appFont(.pretendardBold, size: 18) - } - private let firstSemesterButton = UIButton().then { - $0.setTitle("1학기", for: .normal) - } - private let summerSelectButton = UIButton().then { - $0.setTitle("여름학기", for: .normal) - } - private let secondSemesterButton = UIButton().then { - $0.setTitle("2학기", for: .normal) - } - private let winterSemesterButton = UIButton().then { - $0.setTitle("겨울학기", for: .normal) - } - private let cancelButton = UIButton().then { - $0.setTitleColor(UIColor.appColor(.neutral800), for: .normal) - $0.setTitle("취소", for: .normal) - } - private let applyButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("적용하기", for: .normal) - } - - init( - onApplyButtonTapped: @escaping ([String], [String]) -> Void, - width: CGFloat, - height: CGFloat - ) { + // MARK: - UI Components + private let containerView = UIView() + private let selectYearButton = UIButton() + private let messageLabel = UILabel() + private let firstSemesterButton = UIButton() + private let summerSemesterButton = UIButton() + private let secondSemesterButton = UIButton() + private let winterSemesterButton = UIButton() + + // MARK: - Initializer + init(onApplyButtonTapped: @escaping ([String], [String]) -> Void) { self.onApplyButtonTapped = onApplyButtonTapped - self.containerWidth = width - self.containerHeight = height - super.init(nibName: nil, bundle: nil) + + super.init(configuration: .init( + appearance: .primary, + content: .custom(customView: containerView), + button: .buttons( + leftButtonTitle: "취소", + rightButtonTitle: "적용하기", + rightButtonAction: {} + ), + layout: .init( + width: 327, + contentTopPadding: 12, + contentHorizontalPadding: 24, + paddingBetweenContentAndButton: 10, + buttonHorizontalPadding: 24 + ) + )) } - - required init?(coder: NSCoder) { + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() configureView() - selectYearButton.addTarget(self, action: #selector(selectYearButtonTapped), for: .touchUpInside) - firstSemesterButton.addTarget(self, action: #selector(semesterButtonTapped(_:)), for: .touchUpInside) - summerSelectButton.addTarget(self, action: #selector(semesterButtonTapped(_:)), for: .touchUpInside) - secondSemesterButton.addTarget(self, action: #selector(semesterButtonTapped(_:)), for: .touchUpInside) - winterSemesterButton.addTarget(self, action: #selector(semesterButtonTapped(_:)), for: .touchUpInside) - - cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) - applyButton.addTarget(self, action: #selector(applyButtonTapped), for: .touchUpInside) - - // 초기 상태 업데이트 + setUpAddTargets() updateSemesterButtons() } - - func configre(frameList: [FrameData]) { + + // MARK: - Public + func configure(frameList: [FrameData]) { self.frameList = frameList updateSemesterButtons() } -} -extension ModifySemesterModalViewController { - - private func updateSemesterButtons() { - let semesterMapping: [(UIButton, String)] = [ - (firstSemesterButton, "\(selectedYear)1"), - (summerSelectButton, "\(selectedYear)-여름"), - (secondSemesterButton, "\(selectedYear)2"), - (winterSemesterButton, "\(selectedYear)-겨울") - ] - + + // MARK: - Override + override func rightButtonTapped() { + var addedSemesters: [String] = [] + var removedSemesters: [String] = [] + for (button, semester) in semesterMapping { - if frameList.contains(where: { $0.semester == semester }) { - button.tag = 2 - button.backgroundColor = UIColor.appColor(.success700) - button.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - } else { - button.tag = 0 - button.backgroundColor = UIColor.appColor(.neutral0) - button.setTitleColor(UIColor.appColor(.neutral800), for: .normal) + switch SemesterState(rawValue: button.tag) { + case .added: + addedSemesters.append(semester) + case .removed: + removedSemesters.append(semester) + default: + break } } - } - - @objc private func semesterButtonTapped(_ sender: UIButton) { - switch sender.tag { - case 0: // 초기 상태(흰색) - sender.tag = 1 - sender.backgroundColor = UIColor.appColor(.success700) - sender.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - case 1: // 새로 선택된 학기(초록색) - sender.tag = 0 - sender.backgroundColor = UIColor.appColor(.neutral0) - sender.setTitleColor(UIColor.appColor(.neutral800), for: .normal) - case 2: // 이미 존재하는 학기(초록색) - sender.tag = 3 - sender.backgroundColor = UIColor.appColor(.danger700) - sender.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - case 3: // 삭제 예정(빨간색) - sender.tag = 2 - sender.backgroundColor = UIColor.appColor(.success700) - sender.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - default: - break + + dismiss(animated: true) { [weak self] in + guard let self else { return } + onApplyButtonTapped(addedSemesters, removedSemesters) } } +} - - private func getSemester(for button: UIButton) -> String? { - switch button { - case firstSemesterButton: return "\(selectedYear)1" - case summerSelectButton: return "\(selectedYear)-여름" - case secondSemesterButton: return "\(selectedYear)2" - case winterSemesterButton: return "\(selectedYear)-겨울" - default: return nil +extension ModifySemesterModalViewController { + private func setUpAddTargets() { + selectYearButton.addTarget(self, action: #selector(selectYearButtonTapped), for: .touchUpInside) + [firstSemesterButton, summerSemesterButton, secondSemesterButton, winterSemesterButton].forEach { + $0.addTarget(self, action: #selector(semesterButtonTapped(_:)), for: .touchUpInside) } } - - @objc private func cancelButtonTapped() { - updateSemesterButtons() - dismiss(animated: true, completion: nil) + + private func updateSemesterButtons() { + for (button, semester) in semesterMapping { + let isExisting = frameList.contains { $0.semester == semester } + apply(state: isExisting ? .existing : .unselected, to: button) + } } - - @objc private func applyButtonTapped() { - dismiss(animated: true, completion: nil) - let semesterMapping: [(UIButton, String)] = [ - (firstSemesterButton, "\(selectedYear)1"), - (summerSelectButton, "\(selectedYear)-여름"), - (secondSemesterButton, "\(selectedYear)2"), - (winterSemesterButton, "\(selectedYear)-겨울") - ] - var addedSemesters: [String] = [] - var removedSemesters: [String] = [] + private func apply(state: SemesterState, to button: UIButton) { + button.tag = state.rawValue - for (button, semester) in semesterMapping { - if button.tag == 1 { // 새로 추가될 학기 - addedSemesters.append(semester) - } else if button.tag == 3 { // 삭제될 학기 - removedSemesters.append(semester) - } + switch state { + case .unselected: + button.backgroundColor = .appColor(.neutral0) + button.setTitleColor(.appColor(.neutral800), for: .normal) + case .added, .existing: + button.backgroundColor = .appColor(.success700) + button.setTitleColor(.appColor(.neutral0), for: .normal) + case .removed: + button.backgroundColor = .appColor(.danger700) + button.setTitleColor(.appColor(.neutral0), for: .normal) } - print(addedSemesters) - print(removedSemesters) - onApplyButtonTapped(addedSemesters, removedSemesters) + } - updateSemesterButtons() + // MARK: - Objc + @objc private func semesterButtonTapped(_ sender: UIButton) { + switch SemesterState(rawValue: sender.tag) { + case .unselected: // 초기 상태(흰색) + apply(state: .added, to: sender) + case .added: // 새로 선택된 학기(초록색) + apply(state: .unselected, to: sender) + case .existing: // 이미 존재하는 학기(초록색) + apply(state: .removed, to: sender) + case .removed: // 삭제 예정(빨간색) + apply(state: .existing, to: sender) + case .none: + break + } } - @objc private func selectYearButtonTapped() { - // 현재 연도 계산 let currentYear = Calendar.current.component(.year, from: Date()) let years = (2019...currentYear).reversed().map { "\($0)" } // 2019년부터 현재 연도까지 - + let alert = UIAlertController(title: "연도 선택", message: nil, preferredStyle: .actionSheet) - + for year in years { let action = UIAlertAction(title: year, style: .default) { [weak self] _ in guard let self = self else { return } @@ -202,93 +180,90 @@ extension ModifySemesterModalViewController { } alert.addAction(action) } - + let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) alert.addAction(cancelAction) - + present(alert, animated: true, completion: nil) } +} - - - - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [selectYearButton, messageLabel, firstSemesterButton, summerSelectButton, secondSemesterButton, winterSemesterButton, cancelButton, applyButton].forEach { - containerView.addSubview($0) - } +extension ModifySemesterModalViewController { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(containerWidth) - make.height.equalTo(containerHeight) - } - selectYearButton.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(12) - make.leading.equalTo(containerView.snp.leading).offset(24) - make.width.equalTo(90) - make.height.equalTo(24) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(selectYearButton.snp.bottom) - make.centerX.equalTo(containerView.snp.centerX) + + private func setUpStyles() { + selectYearButton.do { + $0.backgroundColor = .appColor(.neutral300) + $0.titleLabel?.font = .appFont(.pretendardMedium, size: 12) + $0.setTitle(selectedYear, for: .normal) + $0.setTitleColor(.appColor(.neutral800), for: .normal) } - firstSemesterButton.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(14) - make.leading.equalTo(containerView.snp.leading).offset(24) - make.width.equalTo(125) - make.height.equalTo(40) + + messageLabel.do { + $0.text = "학기 편집" + $0.font = .appFont(.pretendardBold, size: 18) } - summerSelectButton.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(14) - make.trailing.equalTo(containerView.snp.trailing).offset(-24) - make.width.height.equalTo(firstSemesterButton) + + firstSemesterButton.setTitle("1학기", for: .normal) + summerSemesterButton.setTitle("여름학기", for: .normal) + secondSemesterButton.setTitle("2학기", for: .normal) + winterSemesterButton.setTitle("겨울학기", for: .normal) + + [firstSemesterButton, summerSemesterButton, secondSemesterButton, winterSemesterButton].forEach { + $0.titleLabel?.font = .appFont(.pretendardMedium, size: 16) + $0.setTitleColor(.appColor(.neutral800), for: .normal) + $0.layer.borderWidth = 1.0 + $0.layer.borderColor = UIColor.appColor(.neutral300).cgColor + $0.layer.cornerRadius = Layout.semesterButtonCornerRadius + $0.layer.masksToBounds = true } - secondSemesterButton.snp.makeConstraints { make in - make.top.equalTo(firstSemesterButton.snp.bottom).offset(10) - make.width.height.leading.equalTo(firstSemesterButton) + } + + private func setUpLayouts() { + [selectYearButton, messageLabel, firstSemesterButton, summerSemesterButton, secondSemesterButton, winterSemesterButton].forEach { + containerView.addSubview($0) } - winterSemesterButton.snp.makeConstraints { make in - make.top.equalTo(secondSemesterButton) - make.width.height.trailing.equalTo(summerSelectButton) + } + + private func setUpConstraints() { + selectYearButton.snp.makeConstraints { + $0.top.leading.equalToSuperview() + $0.width.equalTo(Layout.yearButtonWidth) + $0.height.equalTo(Layout.yearButtonHeight) } - cancelButton.snp.makeConstraints { make in - make.width.equalTo(135.5) - make.height.equalTo(48) - make.trailing.equalTo(containerView.snp.centerX).offset(-4) - make.top.equalTo(winterSemesterButton.snp.bottom).offset(10) + messageLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(Layout.messageLabelTopOffset) + $0.centerX.equalToSuperview() + $0.height.equalTo(Layout.messageLabelHeight) } - applyButton.snp.makeConstraints { make in - make.width.height.bottom.equalTo(cancelButton) - make.leading.equalTo(containerView.snp.centerX).offset(4) + firstSemesterButton.snp.makeConstraints { + $0.top.equalTo(messageLabel.snp.bottom).offset(Layout.paddingBetweenMessageAndSemester) + $0.leading.equalToSuperview() + $0.width.equalTo(Layout.semesterButtonWidth) + $0.height.equalTo(Layout.semesterButtonHeight) } - } - - private func setUpComponents() { - [firstSemesterButton, secondSemesterButton, summerSelectButton, winterSemesterButton, cancelButton, applyButton].forEach { - $0.layer.borderWidth = 1.0 - $0.layer.borderColor = UIColor.appColor(.neutral300).cgColor - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true + summerSemesterButton.snp.makeConstraints { + $0.top.equalTo(firstSemesterButton) + $0.trailing.equalToSuperview() + $0.width.equalTo(Layout.semesterButtonWidth) + $0.height.equalTo(Layout.semesterButtonHeight) } - [firstSemesterButton, secondSemesterButton, summerSelectButton, winterSemesterButton].forEach { - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) - $0.setTitleColor(UIColor.appColor(.neutral800), for: .normal) + secondSemesterButton.snp.makeConstraints { + $0.top.equalTo(firstSemesterButton.snp.bottom).offset(Layout.paddingBetweenSemesterRows) + $0.leading.equalTo(firstSemesterButton) + $0.width.equalTo(Layout.semesterButtonWidth) + $0.height.equalTo(Layout.semesterButtonHeight) + $0.bottom.equalToSuperview() } - [cancelButton, applyButton].forEach { - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) + winterSemesterButton.snp.makeConstraints { + $0.top.equalTo(secondSemesterButton) + $0.trailing.equalTo(summerSemesterButton) + $0.width.equalTo(Layout.semesterButtonWidth) + $0.height.equalTo(Layout.semesterButtonHeight) } } - private func configureView() { - setUpLayOuts() - setUpConstraints() - setUpComponents() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } From cd7244e2c85b8b0a2f642993b2934e9f92e9a613 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 15:46:32 +0900 Subject: [PATCH 048/101] =?UTF-8?q?refactor:=20DeleteFrameModalViewControl?= =?UTF-8?q?ler=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameList/FrameListViewController.swift | 5 +- .../DeleteFrameModalViewController.swift | 151 +++++------------- 2 files changed, 42 insertions(+), 114 deletions(-) diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index 36272ee5..9f664fdc 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -142,10 +142,9 @@ extension FrameListViewController: TimetableCellDelegate { } private func presentDeleteFrameModal(frame: FrameDto) { - let deleteFrameModalViewController = DeleteFrameModalViewController(onDeleteButtonTapped: { [weak self] frame in + let deleteFrameModalViewController = DeleteFrameModalViewController(frame: frame) { [weak self] frame in self?.inputSubject.send(.deleteFrame(frame)) - }) - deleteFrameModalViewController.configure(frame: frame) + } present(deleteFrameModalViewController, animated: false) } } diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift index 57b3029c..d4213bfd 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift @@ -7,124 +7,53 @@ import UIKit -final class DeleteFrameModalViewController: UIViewController { - private let onDeleteButtonTapped: (FrameDto) -> Void - private var frame: FrameDto? = nil - - private let messageLabel = UILabel().then { - $0.textColor = UIColor.appColor(.neutral800) - $0.font = UIFont.appFont(.pretendardMedium, size: 16) - } - - private let deleteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.danger700) - $0.setTitle("삭제하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 14) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } +final class DeleteFrameModalViewController: KoinModalViewController { - private let cancelButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView = UIButton().then { - $0.backgroundColor = .white - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } + // MARK: - Properties + private let onDeleteButtonTapped: (FrameDto) -> Void + private let frame: FrameDto - init(onDeleteButtonTapped: @escaping (FrameDto) -> Void) { + // MARK: - Initializer + init( + frame: FrameDto, + onDeleteButtonTapped: @escaping (FrameDto) -> Void, + ) { self.onDeleteButtonTapped = onDeleteButtonTapped - super.init(nibName: nil, bundle: nil) - } + self.frame = frame - @available(*, unavailable) - required init?(coder: NSCoder) { + let attributedString = { + let frameName = frame.timetableName + let text = "\(frameName)\(frameName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?" + let attributedString = NSMutableAttributedString( + string: text, + attributes: [ + .font: UIFont.appFont(.pretendardMedium, size: 16), + .foregroundColor: UIColor.appColor(.neutral800) + ] + ) + let range = (text as NSString).range(of: "삭제") + attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range) + return attributedString + }() + super.init(configuration: .init( + appearance: .destructive, + content: .attributedSingleTitle(title: attributedString), + button: .buttons( + leftButtonTitle: "취소", + rightButtonTitle: "삭제하기", + rightButtonAction: {} + ) + )) + } + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) - } - - @objc private func deleteButtonTapped() { - if let frame = frame { - onDeleteButtonTapped(frame) - } - dismiss(animated: true, completion: nil) - } - @objc private func cancelButtonTapped() { - dismiss(animated: true, completion: nil) - } - - func configure(frame: FrameDto) { - self.frame = frame - - let frameName = frame.timetableName - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - paragraphStyle.alignment = .center - let text = "\(frameName)\(frameName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?" - let attributedString = NSMutableAttributedString(string: text) - let range = (text as NSString).range(of: "삭제") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - messageLabel.attributedText = attributedString - } - -} -extension DeleteFrameModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [deleteButton, messageLabel, cancelButton, deleteButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(327) - make.height.equalTo(216) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(47) - make.centerX.equalToSuperview() - } - cancelButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-47) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(127.5) - make.height.equalTo(48) - } - deleteButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-47) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(127.5) - make.height.equalTo(48) + // MARK: - Override + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + onDeleteButtonTapped(frame) } } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } From 241c6462e347839786754d2c265f1ed108b9f98f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 16:01:46 +0900 Subject: [PATCH 049/101] =?UTF-8?q?refactor:=20DeleteSemesterModalViewCont?= =?UTF-8?q?roller=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameList/FrameListViewController.swift | 5 +- .../DeleteSemesterModalViewController.swift | 152 +++++------------- 2 files changed, 41 insertions(+), 116 deletions(-) diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index 9f664fdc..1c629f18 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -134,10 +134,9 @@ extension FrameListViewController: TimetableCellDelegate { inputSubject.send(.modifySemester(addedSemesters, [])) guard !removedSemesters.isEmpty else { return } - let deleteSemesterModalViewController = DeleteSemesterModalViewController(onDeleteButtonTapped: { [weak self] semesters in + let deleteSemesterModalViewController = DeleteSemesterModalViewController(semesters: removedSemesters) { [weak self] semesters in self?.inputSubject.send(.modifySemester([], semesters)) - }) - deleteSemesterModalViewController.setSemesters(semesters: removedSemesters) + } present(deleteSemesterModalViewController, animated: false) } diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift index 1aa44f39..c9389bae 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteSemesterModalViewController.swift @@ -7,126 +7,52 @@ import UIKit -final class DeleteSemesterModalViewController: UIViewController { +final class DeleteSemesterModalViewController: KoinModalViewController { + + // MARK: - Properties private let onDeleteButtonTapped: ([String]) -> Void - private var semesters: [String]? = nil - - private let messageLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 16) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 6 - $0.textColor = UIColor.appColor(.neutral800) - $0.numberOfLines = 3 - let text = "시간표가 작성되어 있는 학기가\n있어요. 해당 학기를 제외할 경우\n학기 내 시간표도 함께 삭제돼요." - let attributedString = NSMutableAttributedString(string: text) - let range = (text as NSString).range(of: "학기 내 시간표도 함께 삭제") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range) - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - $0.attributedText = attributedString - } - - private let closeButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.borderColor = UIColor.appColor(.neutral500).cgColor - $0.layer.borderWidth = 1.0 - $0.setTitle("취소", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral600), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let deleteButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.danger700) - $0.setTitle("삭제하기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - } - - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 4 - view.layer.masksToBounds = true - return view - }() + private let semesters: [String] - init(onDeleteButtonTapped: @escaping ([String]) -> Void) { + // MARK: - Initializer + init( + semesters: [String], + onDeleteButtonTapped: @escaping ([String]) -> Void + ) { + self.semesters = semesters self.onDeleteButtonTapped = onDeleteButtonTapped - super.init(nibName: nil, bundle: nil) - } - @available(*, unavailable) - required init?(coder: NSCoder) { + let attributedString = { + let text = "시간표가 작성되어 있는 학기가\n있어요. 해당 학기를 제외할 경우\n학기 내 시간표도 함께 삭제돼요." + let attributedString = NSMutableAttributedString( + string: text, + attributes: [ + .font: UIFont.appFont(.pretendardMedium, size: 16), + .foregroundColor: UIColor.appColor(.neutral800) + ] + ) + let range = (text as NSString).range(of: "학기 내 시간표도 함께 삭제") + attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range) + return attributedString + }() + super.init(configuration: .init( + appearance: .destructive, + content: .attributedSingleTitle(title: attributedString), + button: .buttons( + leftButtonTitle: "취소", + rightButtonTitle: "삭제하기", + rightButtonAction: {} + ) + )) + } + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func viewDidLoad() { - super.viewDidLoad() - modalPresentationStyle = .overFullScreen - modalTransitionStyle = .crossDissolve - configureView() - deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismiss(animated: true, completion: nil) - } - - @objc private func deleteButtonTapped() { - if let semesters = semesters { - onDeleteButtonTapped(semesters) - } - dismiss(animated: true, completion: nil) - } - - func setSemesters(semesters: [String]) { - self.semesters = semesters - } -} -extension DeleteSemesterModalViewController { - - private func setUpLayOuts() { - [containerView].forEach { - view.addSubview($0) - } - [messageLabel, closeButton, deleteButton].forEach { - containerView.addSubview($0) - } - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { make in - make.centerX.equalTo(view.snp.centerX) - make.centerY.equalTo(view.snp.centerY) - make.width.equalTo(301) - make.height.equalTo(198) - } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(containerView.snp.top).offset(30) - make.centerX.equalTo(containerView.snp.centerX) - } - closeButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-24) - make.trailing.equalTo(containerView.snp.centerX).offset(-2) - make.width.equalTo(114.5) - make.height.equalTo(48) - } - deleteButton.snp.makeConstraints { make in - make.bottom.equalTo(containerView.snp.bottom).offset(-24) - make.leading.equalTo(containerView.snp.centerX).offset(2) - make.width.equalTo(114.5) - make.height.equalTo(48) + // MARK: - Override + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + onDeleteButtonTapped(semesters) } } - - private func configureView() { - setUpLayOuts() - setUpConstraints() - view.backgroundColor = UIColor.appColor(.neutral800).withAlphaComponent(0.7) - } } From 2df351f2801502d93719f2e16b3e07acc1e46ab1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 16:12:37 +0900 Subject: [PATCH 050/101] =?UTF-8?q?delete:=20BackButtonPopUpViewController?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C,=20KoinModalViewController=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BackButtonPopUpViewController.swift | 114 ------------------ .../ShopReview/ShopReviewViewController.swift | 22 ++-- koin.xcodeproj/project.pbxproj | 4 - 3 files changed, 13 insertions(+), 127 deletions(-) delete mode 100644 Koin/Presentation/Shop/ShopReview/BackButtonPopUpViewController.swift diff --git a/Koin/Presentation/Shop/ShopReview/BackButtonPopUpViewController.swift b/Koin/Presentation/Shop/ShopReview/BackButtonPopUpViewController.swift deleted file mode 100644 index d58263a5..00000000 --- a/Koin/Presentation/Shop/ShopReview/BackButtonPopUpViewController.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// CustomAlertViewController.swift -// koin -// -// Created by 김성민 on 11/4/25. -// - -import UIKit -import SnapKit - -final class BackButtonPopUpViewController: UIViewController { - var onStop: (() -> Void)? - - - private let dimView = UIView().then { - $0.backgroundColor = UIColor.black.withAlphaComponent(0.7) - } - - private let card = UIView().then { - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.cornerRadius = 8 - $0.layer.masksToBounds = true - } - - private let titleLabel = UILabel().then { - $0.text = "리뷰 수정을 그만하시겠어요?" - $0.textAlignment = .center - $0.textColor = UIColor.appColor(.neutral600) - $0.font = UIFont.setFont(.body2) - } - - private let stopButton = UIButton(type: .system).then { - var config = UIButton.Configuration.plain() - config.baseForegroundColor = UIColor.appColor(.neutral600) - config.background.backgroundColor = UIColor.appColor(.neutral0) - config.background.strokeColor = UIColor.appColor(.neutral400) - config.background.strokeWidth = 1 - config.attributedTitle = AttributedString( - "그만하기", - attributes: .init([.font: UIFont.setFont(.body2Strong)]) - ) - config.contentInsets = .init(top: 12, leading: 31.25, bottom: 12, trailing: 31.25) - $0.configuration = config - } - - private let keepButton = UIButton(type: .system).then { - var config = UIButton.Configuration.plain() - config.baseForegroundColor = UIColor.appColor(.neutral0) - config.background.backgroundColor = UIColor.appColor(.new500) - config.attributedTitle = AttributedString( - "계속쓰기", - attributes: .init([.font: UIFont.setFont(.body2Strong)]) - ) - config.contentInsets = .init(top: 12, leading: 31.25, bottom: 12, trailing: 31.25) - $0.configuration = config - } - - override func viewDidLoad() { - super.viewDidLoad() - configureView() - stopButton.addTarget(self, action: #selector(stopButtonTapped), for: .touchUpInside) - keepButton.addTarget(self, action: #selector(keepButtonTapped), for: .touchUpInside) - } - - - private func configureView() { - setLayouts() - setUpConstraints() - } - - private func setLayouts() { - view.backgroundColor = .clear - view.addSubview(dimView) - view.addSubview(card) - [titleLabel, stopButton, keepButton].forEach{ - card.addSubview($0) - } - } - - private func setUpConstraints() { - dimView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - card.snp.makeConstraints { - $0.center.equalToSuperview() - $0.height.equalTo(144) - $0.width.equalTo(301) - } - titleLabel.snp.makeConstraints{ - $0.centerX.equalToSuperview() - $0.top.equalToSuperview().offset(24) - } - stopButton.snp.makeConstraints{ - $0.height.equalTo(48) - $0.leading.equalToSuperview().offset(32) - $0.top.equalTo(titleLabel.snp.bottom).offset(24) - } - keepButton.snp.makeConstraints{ - $0.size.equalTo(stopButton) - $0.leading.equalTo(stopButton.snp.trailing).offset(8) - $0.centerY.equalTo(stopButton) - } - } - - - - @objc private func stopButtonTapped() { - dismiss(animated: false) { [weak self] in self?.onStop?() } - } - - @objc private func keepButtonTapped() { - dismiss(animated: false) - } -} diff --git a/Koin/Presentation/Shop/ShopReview/ShopReviewViewController.swift b/Koin/Presentation/Shop/ShopReview/ShopReviewViewController.swift index a3f7ae22..b4a7c847 100644 --- a/Koin/Presentation/Shop/ShopReview/ShopReviewViewController.swift +++ b/Koin/Presentation/Shop/ShopReview/ShopReviewViewController.swift @@ -417,14 +417,19 @@ extension ShopReviewViewController { guard presentedViewController == nil, navigationController?.transitionCoordinator == nil else { return } - let viewController = BackButtonPopUpViewController() - viewController.modalPresentationStyle = .overFullScreen - viewController.modalTransitionStyle = .crossDissolve - viewController.onStop = { [weak self, weak viewController] in - viewController?.dismiss(animated: false) - self?.navigationController?.popViewController(animated: true) - } - present(viewController, animated: false) + let modalViewController = KoinModalViewController(configuration: .init( + appearance: .new, + content: .singleTitle(text: "리뷰 수정을 그만하시겠어요?"), + button: .buttons( + leftButtonTitle: "그만하기", + leftButtonAction: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + rightButtonTitle: "계속쓰기", + rightButtonAction: {} + ) + )) + present(modalViewController, animated: true) } private func setNavigationItem() { @@ -667,4 +672,3 @@ extension UIResponder { Static.responder = self } } - diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 0ebbc383..06345705 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -545,7 +545,6 @@ 7CED918A2EF2D1F900457128 /* NoticeDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F6F2EF2D1F900457128 /* NoticeDataViewController.swift */; }; 7CED918B2EF2D1F900457128 /* LostArticleImageCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EE82EF2D1F900457128 /* LostArticleImageCollectionView.swift */; }; 7CED918D2EF2D1F900457128 /* ScoreChartCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90002EF2D1F900457128 /* ScoreChartCollectionViewCell.swift */; }; - 7CED918F2EF2D1F900457128 /* BackButtonPopUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED901D2EF2D1F900457128 /* BackButtonPopUpViewController.swift */; }; 7CED91902EF2D1F900457128 /* StateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F342EF2D1F900457128 /* StateView.swift */; }; 7CED91912EF2D1F900457128 /* TabBarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F3D2EF2D1F900457128 /* TabBarCollectionView.swift */; }; 7CED91922EF2D1F900457128 /* ShopSummaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FDA2EF2D1F900457128 /* ShopSummaryViewController.swift */; }; @@ -1568,7 +1567,6 @@ 7CED90182EF2D1F900457128 /* ReviewImageUploadCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewImageUploadCollectionViewCell.swift; sourceTree = ""; }; 7CED901B2EF2D1F900457128 /* ShopReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopReviewViewModel.swift; sourceTree = ""; }; 7CED901C2EF2D1F900457128 /* ShopReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopReviewViewController.swift; sourceTree = ""; }; - 7CED901D2EF2D1F900457128 /* BackButtonPopUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackButtonPopUpViewController.swift; sourceTree = ""; }; 7CED901F2EF2D1F900457128 /* ReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportDetailView.swift; sourceTree = ""; }; 7CED90212EF2D1F900457128 /* ShopReviewReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopReviewReportViewModel.swift; sourceTree = ""; }; 7CED90222EF2D1F900457128 /* ShopReviewReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopReviewReportViewController.swift; sourceTree = ""; }; @@ -4029,7 +4027,6 @@ 7CED901A2EF2D1F900457128 /* SubViews */, 7CED901B2EF2D1F900457128 /* ShopReviewViewModel.swift */, 7CED901C2EF2D1F900457128 /* ShopReviewViewController.swift */, - 7CED901D2EF2D1F900457128 /* BackButtonPopUpViewController.swift */, ); path = ShopReview; sourceTree = ""; @@ -6201,7 +6198,6 @@ 7CA60D792F664952007C08C1 /* PostAllNotificationsReadUseCase.swift in Sources */, 7CED918D2EF2D1F900457128 /* ScoreChartCollectionViewCell.swift in Sources */, 7CB11C7C2FB32817004E0C80 /* LostItemKeyword.swift in Sources */, - 7CED918F2EF2D1F900457128 /* BackButtonPopUpViewController.swift in Sources */, 7C58FE862F5F9CDD00A2737B /* CallVanDataViewController.swift in Sources */, 7C372F202F1CD90B00149729 /* LostItemDataRecentHeaderView.swift in Sources */, 7CED91902EF2D1F900457128 /* StateView.swift in Sources */, From 93343139b58df78c5ce47baebce3bad81f697c73 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 17:27:21 +0900 Subject: [PATCH 051/101] =?UTF-8?q?chore:=20=EC=B1=84=ED=8C=85=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A7=84=EC=9E=85=EC=A0=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Apps/SceneDelegate.swift | 6 +++- Koin/Core/Extensions/Asset/ImageAsset.swift | 1 + Koin/Domain/Model/Home/CategoryModels.swift | 5 ++++ .../Core/ForceModifyUserViewController.swift | 6 +++- .../Category/CategoryHostingController.swift | 14 +++++++++ .../Home/Category/CategoryView.swift | 29 ++++++++++++++++--- .../Home/Category/CategoryViewModel.swift | 16 +++++++++- .../FoundIdViewController.swift | 6 +++- .../ChangePasswordSuccessViewController.swift | 6 +++- .../Login/Login/LoginViewController.swift | 6 +++- .../Home/categoryChat.imageset/Contents.json | 12 ++++++++ .../categoryChat.imageset/categoryChat.svg | 4 +++ 12 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/Contents.json create mode 100644 Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/categoryChat.svg diff --git a/Koin/Apps/SceneDelegate.swift b/Koin/Apps/SceneDelegate.swift index cdd85794..a92eeeeb 100644 --- a/Koin/Apps/SceneDelegate.swift +++ b/Koin/Apps/SceneDelegate.swift @@ -236,8 +236,12 @@ extension SceneDelegate { } private func makeCategoryHostingController() -> UIViewController { + let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let categoryRootView = CategoryView(viewModel: CategoryViewModel(logAnalyticsEventUseCase: logAnalyticsEventUseCase)) + let categoryRootView = CategoryView( + viewModel: CategoryViewModel( + checkLoginUseCase: checkLoginUseCase, + logAnalyticsEventUseCase: logAnalyticsEventUseCase)) return CategoryHostingController(rootView: categoryRootView) } diff --git a/Koin/Core/Extensions/Asset/ImageAsset.swift b/Koin/Core/Extensions/Asset/ImageAsset.swift index 8a253e2c..faaeda9c 100644 --- a/Koin/Core/Extensions/Asset/ImageAsset.swift +++ b/Koin/Core/Extensions/Asset/ImageAsset.swift @@ -259,6 +259,7 @@ public enum ImageAsset: String { case categoryBusSearch case categoryBusTimetable case categoryCallVan + case categoryChat case categoryDepartment case categoryDining case categoryFacility diff --git a/Koin/Domain/Model/Home/CategoryModels.swift b/Koin/Domain/Model/Home/CategoryModels.swift index 2db31fa0..1d281044 100644 --- a/Koin/Domain/Model/Home/CategoryModels.swift +++ b/Koin/Domain/Model/Home/CategoryModels.swift @@ -16,6 +16,7 @@ enum HomeCategoryItem: Identifiable { case busTimetable case busRoute case callVan + case chat case land case business case department @@ -42,6 +43,8 @@ enum HomeCategoryItem: Identifiable { return "교통편 조회하기" case .callVan: return "콜밴팟 모집" + case .chat: + return "채팅" case .land: return "복덕방" case .business: @@ -80,6 +83,8 @@ enum HomeCategoryItem: Identifiable { return .categoryBusSearch case .callVan: return .categoryCallVan + case .chat: + return .categoryChat case .land: return .categoryLand case .business: diff --git a/Koin/Presentation/Core/ForceModifyUserViewController.swift b/Koin/Presentation/Core/ForceModifyUserViewController.swift index 208e027d..0a795165 100644 --- a/Koin/Presentation/Core/ForceModifyUserViewController.swift +++ b/Koin/Presentation/Core/ForceModifyUserViewController.swift @@ -146,8 +146,12 @@ extension ForceModifyUserViewController { } private func makeCategoryHostingController() -> UIViewController { + let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let categoryRootView = CategoryView(viewModel: CategoryViewModel(logAnalyticsEventUseCase: logAnalyticsEventUseCase)) + let categoryRootView = CategoryView( + viewModel: CategoryViewModel( + checkLoginUseCase: checkLoginUseCase, + logAnalyticsEventUseCase: logAnalyticsEventUseCase)) return CategoryHostingController(rootView: categoryRootView) } diff --git a/Koin/Presentation/Home/Category/CategoryHostingController.swift b/Koin/Presentation/Home/Category/CategoryHostingController.swift index 0230097f..5f13f4cd 100644 --- a/Koin/Presentation/Home/Category/CategoryHostingController.swift +++ b/Koin/Presentation/Home/Category/CategoryHostingController.swift @@ -44,10 +44,20 @@ final class CategoryHostingController: UIHostingController, Hostin navigationController?.pushViewController(makeBusSearchViewController(), animated: true) case .showCallVan: navigationController?.pushViewController(makeCallVanListViewController(), animated: true) + case .showChatList: + navigationController?.pushViewController(makeChatListViewController(), animated: true) case .showLand: navigationController?.pushViewController(makeLandViewController(), animated: true) case .showBusiness: presentBusiness() + + case .showLoginToast: + showToastMessageWithButton( + message: "로그인이 필요한 기능입니다.", + buttonTitle: "로그인" + ) { [weak self] in + self?.navigateToLogin() + } } } } @@ -174,6 +184,10 @@ extension CategoryHostingController { ) return CallVanListViewController(viewModel: viewModel) } + + private func makeChatListViewController() -> UIViewController { + return ChatListTableViewController(viewModel: ChatListTableViewModel()) + } private func makeLandViewController() -> UIViewController { let landService = DefaultLandService() diff --git a/Koin/Presentation/Home/Category/CategoryView.swift b/Koin/Presentation/Home/Category/CategoryView.swift index 93de61f0..e2bfe625 100644 --- a/Koin/Presentation/Home/Category/CategoryView.swift +++ b/Koin/Presentation/Home/Category/CategoryView.swift @@ -15,14 +15,17 @@ struct CategoryView: ActionBindableView { case showTimetable case showLostItem case showFacility + case showDepartment case showDining case showShop case showBusTimetable case showBusRoute case showCallVan + case showChatList case showLand case showBusiness - case showDepartment + + case showLoginToast } // MARK: - Properties @@ -81,6 +84,7 @@ struct CategoryView: ActionBindableView { CategorySection( title: "기타", items: [ + .chat, .land, .business, ], @@ -96,17 +100,28 @@ struct CategoryView: ActionBindableView { } .scrollIndicators(.hidden) .background(Color.appColor(.newBackground)) + .onAppear { + viewModel.execute(.checkAuth) + } } } private extension CategoryView { private func didTapItem(_ item: HomeCategoryItem) { + if case item = .chat { + guard viewModel.isLoggedIn else { + sendAction(.showLoginToast) + return + } + } + let action = action(for: item) sendAction(action) - let loggingInfo = loggingInfo(for: action) - viewModel.execute(.logEvent(loggingInfo.label, .click, loggingInfo.value)) + if let loggingInfo = loggingInfo(for: action) { + viewModel.execute(.logEvent(loggingInfo.label, .click, loggingInfo.value)) + } } private func action(for item: HomeCategoryItem) -> Action { @@ -129,6 +144,8 @@ private extension CategoryView { return .showBusRoute case .callVan: return .showCallVan + case .chat: + return .showChatList case .land: return .showLand case .business: @@ -136,7 +153,7 @@ private extension CategoryView { } } - private func loggingInfo(for action: Action) -> (label: EventParameter.EventLabel.Campus, value: String) { + private func loggingInfo(for action: Action) -> (label: EventParameter.EventLabel.Campus, value: String)? { switch action { case .showTimetable: return (.categoryTimetable, "시간표") @@ -156,10 +173,14 @@ private extension CategoryView { return (.categoryTransportation, "교통편 조회하기") case .showCallVan: return (.categoryTransportation, "콜밴팟 모집") + case .showChatList: + return (.categoryEtc, "채팅") case .showLand: return (.categoryEtc, "복덕방") case .showBusiness: return (.categoryEtc, "코인 for Business") + case .showLoginToast: + return nil } } } diff --git a/Koin/Presentation/Home/Category/CategoryViewModel.swift b/Koin/Presentation/Home/Category/CategoryViewModel.swift index 1f7e8c72..1343db14 100644 --- a/Koin/Presentation/Home/Category/CategoryViewModel.swift +++ b/Koin/Presentation/Home/Category/CategoryViewModel.swift @@ -7,22 +7,36 @@ import Foundation import Observation +import Combine @Observable @MainActor final class CategoryViewModel: SwiftUIViewModelProtocol { enum Input { + case checkAuth case logEvent(EventLabelType, EventParameter.EventCategory, Any) } + private let checkLoginUseCase: CheckLoginUseCase private let logAnalyticsEventUseCase: LogAnalyticsEventUseCase + private(set) var isLoggedIn: Bool = false + private var subscriptions = Set() + - init(logAnalyticsEventUseCase: LogAnalyticsEventUseCase) { + init( + checkLoginUseCase: CheckLoginUseCase, + logAnalyticsEventUseCase: LogAnalyticsEventUseCase + ) { + self.checkLoginUseCase = checkLoginUseCase self.logAnalyticsEventUseCase = logAnalyticsEventUseCase } func execute(_ input: Input) { switch input { + case .checkAuth: + checkLoginUseCase.execute().sink { [weak self] isLoggedIn in + self?.isLoggedIn = isLoggedIn + }.store(in: &subscriptions) case let .logEvent(label, category, value): logAnalyticsEventUseCase.execute(label: label, category: category, value: value) } diff --git a/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift b/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift index 3fbd76be..641fc2e6 100644 --- a/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift +++ b/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift @@ -181,8 +181,12 @@ extension FoundIdViewController { } private func makeCategoryHostingController() -> UIViewController { + let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let categoryRootView = CategoryView(viewModel: CategoryViewModel(logAnalyticsEventUseCase: logAnalyticsEventUseCase)) + let categoryRootView = CategoryView( + viewModel: CategoryViewModel( + checkLoginUseCase: checkLoginUseCase, + logAnalyticsEventUseCase: logAnalyticsEventUseCase)) return CategoryHostingController(rootView: categoryRootView) } diff --git a/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift b/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift index 70c49f54..32599802 100644 --- a/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift +++ b/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift @@ -154,8 +154,12 @@ extension ChangePasswordSuccessViewController { } private func makeCategoryHostingController() -> UIViewController { + let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let categoryRootView = CategoryView(viewModel: CategoryViewModel(logAnalyticsEventUseCase: logAnalyticsEventUseCase)) + let categoryRootView = CategoryView( + viewModel: CategoryViewModel( + checkLoginUseCase: checkLoginUseCase, + logAnalyticsEventUseCase: logAnalyticsEventUseCase)) return CategoryHostingController(rootView: categoryRootView) } diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index c3dd2df4..826fc7d2 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -413,8 +413,12 @@ extension LoginViewController { } private func makeCategoryHostingController() -> UIViewController { + let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) - let categoryRootView = CategoryView(viewModel: CategoryViewModel(logAnalyticsEventUseCase: logAnalyticsEventUseCase)) + let categoryRootView = CategoryView( + viewModel: CategoryViewModel( + checkLoginUseCase: checkLoginUseCase, + logAnalyticsEventUseCase: logAnalyticsEventUseCase)) return CategoryHostingController(rootView: categoryRootView) } diff --git a/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/Contents.json b/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/Contents.json new file mode 100644 index 00000000..67066b1e --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "categoryChat.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/categoryChat.svg b/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/categoryChat.svg new file mode 100644 index 00000000..45e5654b --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Home/categoryChat.imageset/categoryChat.svg @@ -0,0 +1,4 @@ + + + + From a066e30db7ae803c1495b1b5167aaf092bb4b509 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 18:27:38 +0900 Subject: [PATCH 052/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalPresentationController.swift | 1 - .../CallVanList/CallVanListViewController.swift | 2 +- .../CallVanPost/CallVanPostViewController.swift | 4 ++-- .../ForceUpdate/ForceUpdateViewController.swift | 6 +++--- .../ShopReviewList/ReviewListViewController.swift | 14 +++++++------- .../FrameList/FrameListViewController.swift | 2 +- .../SubViews/DeleteFrameModalViewController.swift | 2 +- .../SubViews/ModifyFrameModalViewController.swift | 5 ----- .../Timetable/TimetableViewController.swift | 2 +- 9 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift b/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift index 86979962..99a6e1d0 100644 --- a/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift +++ b/Koin/Core/View/KoinModalViewController/Transition/KoinModalPresentationController.swift @@ -34,7 +34,6 @@ final class KoinModalPresentationController: UIPresentationController { guard let containerView else { return } dimmingView.frame = containerView.bounds -// containerView.insertSubview(dimmingView, at: 0) containerView.addSubview(dimmingView) guard let transitionCoordinator = presentedViewController.transitionCoordinator else { diff --git a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift index a7a82d30..8ec4d0cb 100644 --- a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift +++ b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift @@ -398,7 +398,7 @@ extension CallVanListViewController { mainTitle = RestrictionType.temporaryRestriction14Days.rawValue subTitle = RestrictionType.temporaryRestriction14Days.getDescription(until: until) case .permanentRestriction: - mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + mainTitle = RestrictionType.permanentRestriction.rawValue subTitle = RestrictionType.permanentRestriction.getDescription() case nil: return diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index f139bcb5..6d11807e 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -154,7 +154,7 @@ extension CallVanPostViewController { mainTitle = RestrictionType.temporaryRestriction14Days.rawValue subTitle = RestrictionType.temporaryRestriction14Days.getDescription(until: until) case .permanentRestriction: - mainTitle = RestrictionType.temporaryRestriction14Days.rawValue + mainTitle = RestrictionType.permanentRestriction.rawValue subTitle = RestrictionType.permanentRestriction.getDescription() case nil: return @@ -170,7 +170,7 @@ extension CallVanPostViewController { title: "닫기" ) )) - present(modalViewController, animated: false) + present(modalViewController, animated: true) } private func presentDeparturePlaceBottomSheet() { diff --git a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift index c7ff12f5..1d369bf4 100644 --- a/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift +++ b/Koin/Presentation/Core/ForceUpdate/ForceUpdateViewController.swift @@ -189,12 +189,12 @@ extension ForceUpdateViewController { button: .buttons( leftButtonTitle: "닫기", leftButtonAction: { [weak self] in - self?.openStore() - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) }, rightButtonTitle: "스토어 가기", rightButtonAction: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "확인")) + self?.openStore() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.ForceUpdate.alreadyUpdatePopup, .click, "스토어로 가기")) } ) )) diff --git a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift index 4a8e31bd..dd7bb09b 100644 --- a/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift +++ b/Koin/Presentation/Shop/ShopReviewList/ReviewListViewController.swift @@ -317,12 +317,12 @@ extension ReviewListViewController { button: .buttons( leftButtonTitle: "취소하기", leftButtonAction: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) - self?.deleteReview() + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "X")) }, rightButtonTitle: "삭제하기", rightButtonAction: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "X")) + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewDeleteDone, .click, "O")) + self?.deleteReview() } ) )) @@ -355,13 +355,13 @@ extension ReviewListViewController { button: .buttons( leftButtonTitle: "닫기", leftButtonAction: { [weak self] in - self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) - self?.showLoginScreen() + guard message == "작성", let self else { return } + inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, .click, viewModel.getShopName())) }, rightButtonTitle: "로그인하기", rightButtonAction: { [weak self] in - guard message == "작성", let self else { return } - inputSubject.send(.logEvent(EventParameter.EventLabel.Business.shopDetailViewReviewWriteCancel, .click, viewModel.getShopName())) + self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Business.loginPrompt, .click, "리뷰 \(message) 팝업")) + self?.showLoginScreen() } ) )) diff --git a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift index 1c629f18..196d8195 100644 --- a/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/FrameListViewController.swift @@ -144,7 +144,7 @@ extension FrameListViewController: TimetableCellDelegate { let deleteFrameModalViewController = DeleteFrameModalViewController(frame: frame) { [weak self] frame in self?.inputSubject.send(.deleteFrame(frame)) } - present(deleteFrameModalViewController, animated: false) + present(deleteFrameModalViewController, animated: true) } } extension FrameListViewController: UITableViewDelegate, UITableViewDataSource { diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift index d4213bfd..ed8c6da1 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/DeleteFrameModalViewController.swift @@ -16,7 +16,7 @@ final class DeleteFrameModalViewController: KoinModalViewController { // MARK: - Initializer init( frame: FrameDto, - onDeleteButtonTapped: @escaping (FrameDto) -> Void, + onDeleteButtonTapped: @escaping (FrameDto) -> Void ) { self.onDeleteButtonTapped = onDeleteButtonTapped self.frame = frame diff --git a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift index 320032ad..34d88853 100644 --- a/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift +++ b/Koin/Presentation/TimeTable/FrameList/SubViews/ModifyFrameModalViewController.swift @@ -89,11 +89,6 @@ extension ModifyFrameModalViewController { private func setUpDelegate() { textField.delegate = self } - - override func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() // 키보드 내리기 - return true - } } extension ModifyFrameModalViewController { diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index ad0556be..57e448fc 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -281,7 +281,7 @@ extension TimetableViewController { let modalViewController = DeleteLectureModalViewController(lectureData: lecture) { [weak self] lecture in self?.inputSubject.send(._deleteLecture(lecture)) } - present(modalViewController, animated: false) + present(modalViewController, animated: true) } } From afbfdff6d8549b4d88a55ce32134404d1a2e1e31 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 18:27:47 +0900 Subject: [PATCH 053/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81,=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BusSearchDatePickerViewController.swift | 135 +++++++++++------- .../DeleteLectureModalViewController.swift | 51 +++++-- 2 files changed, 121 insertions(+), 65 deletions(-) diff --git a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift index a200a133..28db0c9d 100644 --- a/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift +++ b/Koin/Presentation/Bus/BusSearchResult/SubViews/BusSearchDatePickerViewController.swift @@ -16,17 +16,9 @@ final class BusSearchDatePickerViewController: KoinModalViewController { // MARK: - UI Components private let customView = UIView() - private let mainTitleLabel = UILabel().then { - $0.text = "출발 시각 설정" - $0.textColor = .appColor(.neutral700) - $0.font = .appFont(.pretendardMedium, size: 18) - $0.numberOfLines = 1 - $0.textAlignment = .left - } - private let subTitleLabel = UILabel().then { - $0.numberOfLines = 2 - } - let pickerView: KoinPickerView + private let mainTitleLabel = UILabel() + private let subTitleLabel = UILabel() + private let pickerView = KoinPickerView() // MARK: - Initializer init( @@ -35,70 +27,30 @@ final class BusSearchDatePickerViewController: KoinModalViewController { onPickerItemsSelected: @escaping ([String]) -> Void, onDepartureNowTapped: @escaping ()->Void ) { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.do { - $0.alignment = .left - $0.lineSpacing = 14 * 0.6 - $0.lineBreakStrategy = .hangulWordPriority - } - subTitleLabel.attributedText = NSAttributedString( - string: subTitle, - attributes: [ - .foregroundColor: UIColor.appColor(.neutral500), - .font: UIFont.appFont(.pretendardRegular, size: 14), - .paragraphStyle: paragraphStyle, - ] - ) - self.onPickerDateChanged = onPickerDateChanged self.onPickerItemsSelected = onPickerItemsSelected self.onDepartureNowTapped = onDepartureNowTapped - let pickerView = KoinPickerView().then { - $0.backgroundColor = .appColor(.neutral50) - } - self.pickerView = pickerView - - let onLeftButtonTapped: ()->Void = { - let currentDate = Date() - let calendar = Calendar.current - let hour = calendar.component(.hour, from: currentDate) - let minute = calendar.component(.minute, from: currentDate) - let amPm = hour < 12 ? "오전" : "오후" - let adjustedHour = hour % 12 - let displayHour = adjustedHour == 0 ? 12 : adjustedHour - - let selectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] - pickerView.setSelectedData(selectedItem: selectedItems) - onPickerItemsSelected(selectedItems) - pickerView.changeSelectedItemPublisher.send(nil) - } - - let onRightButtonTapped: ()->Void = { - onPickerItemsSelected(pickerView.selectedItemPublisher.value) - onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) - pickerView.changeSelectedItemPublisher.send(nil) - } - super.init(configuration: .init( appearance: .primary, content: .custom(customView: customView), button: .buttons( leftButtonTitle: "지금 출발", - leftButtonAction: onLeftButtonTapped, leftButtonStyle: .init( textColor: .neutral600, font: .pretendardMedium, fontSize: 15 ), rightButtonTitle: "완료", - rightButtonAction: onRightButtonTapped + rightButtonAction: {} ), layout: .init( contentTopPadding: 24, contentHorizontalPadding: 0 ) )) + + configureSubTitleLabel(subTitle) } @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -115,14 +67,87 @@ final class BusSearchDatePickerViewController: KoinModalViewController { pickerView.changeSelectedItemPublisher.send(nil) pickerView.setPickerData(items: items, selectedItem: selectedItems) } + + // MARK: - Override + override func leftButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + + let currentDate = Date() + let calendar = Calendar.current + let hour = calendar.component(.hour, from: currentDate) + let minute = calendar.component(.minute, from: currentDate) + let amPm = hour < 12 ? "오전" : "오후" + let adjustedHour = hour % 12 + let displayHour = adjustedHour == 0 ? 12 : adjustedHour + + let selectedItems = ["오늘", amPm, String(displayHour), String(format: "%02d", minute)] + pickerView.setSelectedData(selectedItem: selectedItems) + onPickerItemsSelected(selectedItems) + pickerView.changeSelectedItemPublisher.send(nil) + + onDepartureNowTapped() + } + } + + override func rightButtonTapped() { + dismiss(animated: true) { [weak self] in + guard let self else { return } + onPickerItemsSelected(pickerView.selectedItemPublisher.value) + onPickerDateChanged(pickerView.changeSelectedItemPublisher.value) + pickerView.changeSelectedItemPublisher.send(nil) + } + } } extension BusSearchDatePickerViewController { - private func configureView() { + private func configureSubTitleLabel(_ text: String) { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.do { + $0.alignment = .left + $0.lineSpacing = 14 * 0.6 + $0.lineBreakStrategy = .hangulWordPriority + } + subTitleLabel.attributedText = NSAttributedString( + string: text, + attributes: [ + .foregroundColor: UIColor.appColor(.neutral500), + .font: UIFont.appFont(.pretendardRegular, size: 14), + .paragraphStyle: paragraphStyle, + ] + ) + } + + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + mainTitleLabel.do { + $0.text = "출발 시각 설정" + $0.textColor = .appColor(.neutral700) + $0.font = .appFont(.pretendardMedium, size: 18) + $0.numberOfLines = 1 + $0.textAlignment = .left + } + subTitleLabel.do { + $0.numberOfLines = 2 + } + pickerView.do { + $0.backgroundColor = .appColor(.neutral50) + } + } + + private func setUpLayouts() { [mainTitleLabel, subTitleLabel, pickerView].forEach { customView.addSubview($0) } + } + + private func setUpConstraints() { mainTitleLabel.snp.makeConstraints { $0.top.equalToSuperview() $0.leading.trailing.equalToSuperview().inset(24) diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift index 9c1274ce..6f8f41f9 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/DeleteLectureModalViewController.swift @@ -21,16 +21,8 @@ final class DeleteLectureModalViewController: KoinModalViewController { self.lectureData = lectureData self.onDeleteButtonTapped = onDeleteButtonTapped - let attributedString = { - let lectureName = lectureData.name - let text = "\(lectureName)\(lectureName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?\n삭제한 강의는 수업추가에서\n다시 추가할 수 있어요." - let attributedString = NSMutableAttributedString(string: text) - let range1 = (text as NSString).range(of: "삭제") - let range2 = (text as NSString).range(of: "수업추가") - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.danger700), range: range1) - attributedString.addAttribute(.foregroundColor, value: UIColor.appColor(.primary500), range: range2) - return attributedString - }() + let attributedString = Self.makeAttributedString(lectureName: lectureData.name) + super.init(configuration: .init( appearance: .destructive, content: .attributedSingleTitle(title: attributedString), @@ -38,6 +30,9 @@ final class DeleteLectureModalViewController: KoinModalViewController { leftButtonTitle: "취소", rightButtonTitle: "삭제하기", rightButtonAction: {} + ), + layout: .init( + paddingBetweenContentAndButton: 24 - 9.6 ) )) } @@ -53,3 +48,39 @@ final class DeleteLectureModalViewController: KoinModalViewController { } } } + +extension DeleteLectureModalViewController { + private static func makeAttributedString(lectureName: String) -> NSAttributedString { + let textMedium16 = "\(lectureName)\(lectureName.hasFinalConsonant() ? "을":"를") 삭제하시겠어요?" + let textMedium15 = "삭제한 강의는 수업추가에서\n다시 추가할 수 있어요." + let text = textMedium16 + "\n" + textMedium15 + + let rangeMedium16 = (text as NSString).range(of: textMedium16) + let rangeMedium15 = (text as NSString).range(of: textMedium15) + let range1 = (text as NSString).range(of: "삭제") + let range2 = (text as NSString).range(of: "수업추가") + + let attributedString = NSMutableAttributedString(string: text) + + attributedString.addAttributes([ + .font: UIFont.appFont(.pretendardMedium, size: 16), + .foregroundColor: UIColor.appColor(.neutral800) + ], range: rangeMedium16) + + attributedString.addAttributes([ + .font: UIFont.appFont(.pretendardMedium, size: 15), + .foregroundColor: UIColor.appColor(.neutral800) + ], range: rangeMedium15) + + attributedString.addAttributes([ + .font: UIFont.appFont(.pretendardBold, size: 16), + .foregroundColor: UIColor.appColor(.danger700) + ], range: range1) + + attributedString.addAttributes([ + .foregroundColor: UIColor.appColor(.primary500) + ], range: range2) + + return attributedString + } +} From a4e7cd3f24332c4b29246e1b8f24ad66ea0e3dfa Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 18:42:16 +0900 Subject: [PATCH 054/101] =?UTF-8?q?fix:=20=EC=9E=90=EB=8F=99=EC=99=84?= =?UTF-8?q?=EC=84=B1=EC=97=90=20=EC=9D=98=ED=95=B4=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=EA=B0=80=20=EB=B3=80=EA=B2=BD=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubViews/CertificationView.swift | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift b/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift index 883ff0df..93e01996 100644 --- a/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift +++ b/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift @@ -18,8 +18,17 @@ final class CertificationView: UIView { $0.font = UIFont.appFont(.pretendardRegular, size: 14) } - private let idTextField = UITextField().then { textField in - textField.isUserInteractionEnabled = false + private let idLabelBackgroundView = UIView().then { + $0.backgroundColor = UIColor.appColor(.neutral100) + $0.layer.cornerRadius = 4 + $0.layer.masksToBounds = true + } + + private let idLabel = UILabel().then { + $0.font = .appFont(.pretendardRegular, size: 14) + $0.textColor = UIColor.appColor(.neutral800) + $0.backgroundColor = .clear + $0.textAlignment = .left } private let passwordTitleLabel = UILabel().then { @@ -28,9 +37,19 @@ final class CertificationView: UIView { $0.font = UIFont.appFont(.pretendardRegular, size: 14) } - let passwordTextField = UITextField().then { textField in - textField.placeholder = "현재 비밀번호를 입력해주세요." - textField.isSecureTextEntry = true + let passwordTextField = UITextField().then { + $0.placeholder = "현재 비밀번호를 입력해주세요." + $0.isSecureTextEntry = true + + $0.font = UIFont.appFont(.pretendardRegular, size: 14) + $0.textColor = UIColor.appColor(.neutral800) + $0.backgroundColor = UIColor.appColor(.neutral100) + $0.layer.cornerRadius = 4 + $0.layer.masksToBounds = true + + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: $0.frame.height)) + $0.leftView = paddingView + $0.leftViewMode = .always } private let changeSecureButton = UIButton().then { button in @@ -56,7 +75,7 @@ final class CertificationView: UIView { } func fillEmailText(text: String) { - idTextField.text = text + idLabel.text = text } func getPasswordText() -> String { @@ -84,7 +103,7 @@ extension CertificationView { extension CertificationView { private func setUpLayOuts() { - [idTitleLabel, idTextField, passwordTitleLabel, passwordTextField, errorResponseLabel, changeSecureButton].forEach { + [idTitleLabel, idLabelBackgroundView, idLabel, passwordTitleLabel, passwordTextField, errorResponseLabel, changeSecureButton].forEach { self.addSubview($0) } } @@ -94,13 +113,17 @@ extension CertificationView { make.top.equalTo(self.snp.top) make.leading.equalTo(self.snp.leading).offset(8) } - idTextField.snp.makeConstraints { make in + idLabelBackgroundView.snp.makeConstraints { make in make.top.equalTo(idTitleLabel.snp.bottom).offset(5) make.leading.trailing.equalToSuperview() make.height.equalTo(46) } + idLabel.snp.makeConstraints { make in + make.centerY.equalTo(idLabelBackgroundView) + make.leading.trailing.equalTo(idLabelBackgroundView).inset(16) + } passwordTitleLabel.snp.makeConstraints { make in - make.top.equalTo(idTextField.snp.bottom).offset(30) + make.top.equalTo(idLabel.snp.bottom).offset(30) make.leading.equalTo(self.snp.leading).offset(8) } passwordTextField.snp.makeConstraints { make in @@ -119,25 +142,10 @@ extension CertificationView { make.height.equalTo(20) } } - - private func setUpTextFields() { - [idTextField, passwordTextField].forEach { - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - $0.textColor = UIColor.appColor(.neutral800) - $0.backgroundColor = UIColor.appColor(.neutral100) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true - - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: $0.frame.height)) - $0.leftView = paddingView - $0.leftViewMode = .always - } - } - + private func configureView() { setUpLayOuts() setUpConstraints() - setUpTextFields() self.backgroundColor = .systemBackground } From d2811d60820e37a4f87bd95b34cd5bcef8f0ad9a Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 17 Aug 2026 19:02:19 +0900 Subject: [PATCH 055/101] =?UTF-8?q?chore:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20return=20=ED=82=A4=EB=A5=BC=20=EB=88=8C=EB=A0=80?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20'=EB=8B=A4=EC=9D=8C'=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=B4=20=EB=8F=99=EC=9E=91=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 --- .../Setting/ChangePassword/ChangePasswordViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift b/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift index 1303e4c3..e086c7d4 100644 --- a/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift +++ b/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift @@ -117,6 +117,10 @@ final class ChangePasswordViewController: UIViewController { }.store(in: &subscriptions) } + override func textFieldShouldReturn(_ textField: UITextField) -> Bool { + completeButtonTapped() + return false + } } extension ChangePasswordViewController { From 9882f8392421ff3a35e3762adfee568a15a96047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:10:28 +0900 Subject: [PATCH 056/101] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Din?= =?UTF-8?q?ingViewController=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C=EC=96=B4=20i?= =?UTF-8?q?nternal=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dining/Dining/DiningViewController.swift | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/Koin/Presentation/Dining/Dining/DiningViewController.swift b/Koin/Presentation/Dining/Dining/DiningViewController.swift index d288e123..b664afc9 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewController.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewController.swift @@ -14,8 +14,8 @@ final class DiningViewController: UIViewController { // MARK: - Properties private let viewModel: DiningViewModel - private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] + let inputSubject: PassthroughSubject = .init() private let refreshControl = UIRefreshControl() private var viewDidAppeared = false @@ -33,7 +33,7 @@ final class DiningViewController: UIViewController { } }() - private let diningTypeSegmentControl = UISegmentedControl().then { + let diningTypeSegmentControl = UISegmentedControl().then { $0.setBackgroundImage(UIImage(), for: .normal, barMetrics: .default) $0.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) $0.insertSegment(withTitle: "아침", at: 0, animated: true) @@ -62,7 +62,7 @@ final class DiningViewController: UIViewController { $0.layer.applySketchShadow(color: .appColor(.neutral800), alpha: 0.02, x: 0, y: 1, blur: 1, spread: 0) } - private let diningListCollectionView: DiningCollectionView = { + let diningListCollectionView: DiningCollectionView = { let flowLayout = UICollectionViewFlowLayout().then { $0.scrollDirection = .vertical } @@ -131,11 +131,7 @@ final class DiningViewController: UIViewController { super.viewDidAppear(true) checkAndShowBottomSheet() if viewDidAppeared { - switch diningTypeSegmentControl.selectedSegmentIndex { - case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast)) - case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch)) - default: inputSubject.send(.updateDisplayDateTime(nil, .dinner)) - } + inputSubject.send(.updateDisplayDateTime(nil, currentDiningType)) } viewDidAppeared = true @@ -151,7 +147,7 @@ final class DiningViewController: UIViewController { private func bind() { let outputSubject = viewModel.transform(with: inputSubject.eraseToAnyPublisher()) outputSubject.receive(on: DispatchQueue.main).sink { [weak self] output in - guard let strongSelf = self else { return } + guard self != nil else { return } switch output { case let .updateDiningList(list, diningType): self?.setDiningList(list) @@ -177,7 +173,7 @@ final class DiningViewController: UIViewController { zoomedImageViewController.setImage(tappedDiningImage) self.present(zoomedImageViewController, animated: true, completion: nil) - inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(self.getCurrentDiningType())_\(tappedPlaceText)")) + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(self.currentDiningType.name)_\(tappedPlaceText)")) }.store(in: &subscriptions) @@ -188,7 +184,7 @@ final class DiningViewController: UIViewController { diningListCollectionView.logScrollPublisher.sink { [weak self] _ in guard let self = self else { return } - self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.getCurrentDiningType())) + self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.currentDiningType.name)) }.store(in: &subscriptions) NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification).sink { [weak self] _ in @@ -215,7 +211,7 @@ extension DiningViewController { diningListCollectionView.addGestureRecognizer(swipeRightGesture) } - @objc private func handleSwipe(_ gesture: UISwipeGestureRecognizer) { + @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { let currentSegmentIndex = diningTypeSegmentControl.selectedSegmentIndex if gesture.direction == .left { if currentSegmentIndex < diningTypeSegmentControl.numberOfSegments - 1 { @@ -230,14 +226,9 @@ extension DiningViewController { } @objc private func refresh() { - switch diningTypeSegmentControl.selectedSegmentIndex { - case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast)) - case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch)) - default: inputSubject.send(.updateDisplayDateTime(nil, .dinner)) - } - - inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, getCurrentDiningType())) - + inputSubject.send(.updateDisplayDateTime(nil, currentDiningType)) + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, currentDiningType.name)) + refreshControl.endRefreshing() } @@ -284,13 +275,10 @@ extension DiningViewController { } @objc private func segmentDidChange(_ sender: UISegmentedControl) { - switch sender.selectedSegmentIndex { - case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast)) - case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch)) - default: inputSubject.send(.updateDisplayDateTime(nil, .dinner)) - } - - inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, getCurrentDiningType())) + let diningType = DiningType(segmentIndex: sender.selectedSegmentIndex) ?? .dinner + + inputSubject.send(.updateDisplayDateTime(nil, diningType)) + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, diningType.name)) } private func moveUnderLineView(diningType: DiningType) { @@ -332,12 +320,8 @@ extension DiningViewController { diningListCollectionView.setDiningList(list) } - private func getCurrentDiningType() -> String { - switch diningTypeSegmentControl.selectedSegmentIndex { - case 0: return "아침" - case 1: return "점심" - default: return "저녁" - } + private var currentDiningType: DiningType { + DiningType(segmentIndex: diningTypeSegmentControl.selectedSegmentIndex) ?? .dinner } } From 5902e88565f368b51c77d6176d47f6581bb42225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:14:32 +0900 Subject: [PATCH 057/101] =?UTF-8?q?add:=20=EC=84=B8=EA=B7=B8=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=EB=A1=9C=20DiningType?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20init=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 --- .../DTOs/Decodable/Dining/DiningDto.swift | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Koin/Data/DTOs/Decodable/Dining/DiningDto.swift b/Koin/Data/DTOs/Decodable/Dining/DiningDto.swift index 56505954..40d8c0dc 100644 --- a/Koin/Data/DTOs/Decodable/Dining/DiningDto.swift +++ b/Koin/Data/DTOs/Decodable/Dining/DiningDto.swift @@ -35,7 +35,20 @@ struct DiningDto: Decodable { } func toDomain() -> DiningItem { - return .init(id: id, type: type, place: place, priceCard: priceCard, priceCash: priceCash, kcal: kcal ?? 0, menu: menu ?? [], soldoutAt: soldoutAt, changedAt: changedAt, imageUrl: imageURL, likes: likes, isLiked: isLiked, date: date + return .init( + id: id, + type: type, + place: place, + priceCard: priceCard, + priceCash: priceCash, + kcal: kcal ?? 0, + menu: menu ?? [], + soldoutAt: soldoutAt, + changedAt: changedAt, + imageUrl: imageURL, + likes: likes, + isLiked: isLiked, + date: date ) } } @@ -50,7 +63,16 @@ enum DiningType: String, Decodable { let rawValue = try container.decode(String.self) self = DiningType(rawValue: rawValue) ?? .breakfast } - + + init?(segmentIndex: Int) { + switch segmentIndex { + case 0: self = .breakfast + case 1: self = .lunch + case 2: self = .dinner + default: return nil + } + } + var name: String { switch self { case .breakfast : "아침" From cc8d618341cb87b3299b6475caf050fd31b7be26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:15:25 +0900 Subject: [PATCH 058/101] =?UTF-8?q?add:=20DiningType=20=EC=84=B8=EA=B7=B8?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Dining/DiningTypeTests.swift | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 koinUnitTests/Dining/DiningTypeTests.swift diff --git a/koinUnitTests/Dining/DiningTypeTests.swift b/koinUnitTests/Dining/DiningTypeTests.swift new file mode 100644 index 00000000..a0f79a06 --- /dev/null +++ b/koinUnitTests/Dining/DiningTypeTests.swift @@ -0,0 +1,38 @@ +// +// DiningTypeTests.swift +// koinUnitTests +// +// Created by 이은지 on 8/17/26. +// + +import Foundation +import Testing +@testable import koin + +@Suite("DiningType - 세그먼트 인덱스 변환") +struct DiningTypeTests { + + @Test( + "세그먼트 인덱스를 시간대로 옮긴다", + arguments: [(0, DiningType.breakfast), (1, .lunch), (2, .dinner)] + ) + func 세그먼트_인덱스를_시간대로_옮긴다(index: Int, expected: DiningType) { + #expect(DiningType(segmentIndex: index) == expected) + } + + @Test( + "미선택이거나 범위 밖의 인덱스는 시간대를 특정하지 않는다", + arguments: [-1, 3, 100] + ) + func 미선택이거나_범위_밖의_인덱스는_시간대를_특정하지_않는다(index: Int) { + #expect(DiningType(segmentIndex: index) == nil) + } + + @Test( + "옮긴 시간대는 로깅에 쓰이는 한글 이름을 가진다", + arguments: [(0, "아침"), (1, "점심"), (2, "저녁")] + ) + func 옮긴_시간대는_로깅에_쓰이는_한글_이름을_가진다(index: Int, expectedName: String) { + #expect(DiningType(segmentIndex: index)?.name == expectedName) + } +} From 890d75cb83dac087819a750a5490e5c7dee0948e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:16:16 +0900 Subject: [PATCH 059/101] =?UTF-8?q?test:=20=EC=8B=9D=EB=8B=A8=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20Recorder?= =?UTF-8?q?=EC=99=80=20Stub=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Doubles/DiningViewModelStubs.swift | 55 ++++++++ koinUnitTests/Support/DiningFixtures.swift | 2 - .../Support/DiningLoggingTestSupport.swift | 122 ++++++++++++++++++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 koinUnitTests/Doubles/DiningViewModelStubs.swift create mode 100644 koinUnitTests/Support/DiningLoggingTestSupport.swift diff --git a/koinUnitTests/Doubles/DiningViewModelStubs.swift b/koinUnitTests/Doubles/DiningViewModelStubs.swift new file mode 100644 index 00000000..584cb57e --- /dev/null +++ b/koinUnitTests/Doubles/DiningViewModelStubs.swift @@ -0,0 +1,55 @@ +// +// DiningViewModelStubs.swift +// koinUnitTests +// +// Created by 이은지 on 8/17/26. +// + +import Alamofire +import Combine +import Foundation +@testable import koin + +final class StubFetchDiningListUseCase: FetchDiningListUseCase { + func execute(diningInfo: CurrentDiningTime) -> AnyPublisher<[DiningItem], ErrorResponse> { + Empty<[DiningItem], ErrorResponse>().eraseToAnyPublisher() + } +} + +final class StubDateProvider: DateProvider { + func execute(date: Date) -> CurrentDiningTime { + CurrentDiningTime(date: date, diningType: .breakfast) + } +} + +final class StubShareMenuListUseCase: ShareMenuListUseCase { + func execute(shareModel: ShareDiningMenu) {} +} + +final class StubChangeNotiUseCase: ChangeNotiUseCase { + func execute(method: Alamofire.HTTPMethod, type: SubscribeType) -> AnyPublisher { + Empty().eraseToAnyPublisher() + } +} + +final class StubChangeNotiDetailUseCase: ChangeNotiDetailUseCase { + func execute(method: Alamofire.HTTPMethod, detailType: DetailSubscribeType) -> AnyPublisher { + Empty().eraseToAnyPublisher() + } +} + +final class StubFetchNotiListUseCase: FetchNotiListUseCase { + func execute() -> AnyPublisher { + Empty().eraseToAnyPublisher() + } +} + +final class StubLogAnalyticsEventUseCase: LogAnalyticsEventUseCase { + func execute(label: EventLabelType, category: EventParameter.EventCategory, value: Any) {} + + func executeWithDuration(label: EventLabelType, category: EventParameter.EventCategory, value: Any, previousPage: String?, currentPage: String?, durationTime: String?) {} + + func logEvent(name: String, label: String, value: String, category: String) {} + + func executeWithSessionId(label: EventLabelType, category: EventParameter.EventCategory, value: Any, sessionId: String) {} +} diff --git a/koinUnitTests/Support/DiningFixtures.swift b/koinUnitTests/Support/DiningFixtures.swift index 0ceb71d9..b4bca54e 100644 --- a/koinUnitTests/Support/DiningFixtures.swift +++ b/koinUnitTests/Support/DiningFixtures.swift @@ -62,8 +62,6 @@ enum DiningFixture { ) } - /// 프로덕션 코드(`DefaultDateProvider`, `Date.formatDateToYYMMDD()`)가 `Calendar.current`와 - /// 기본 `DateFormatter`를 쓰기 때문에, 테스트도 같은 기준으로 Date를 만든다. static func date( year: Int = 2026, month: Int = 8, diff --git a/koinUnitTests/Support/DiningLoggingTestSupport.swift b/koinUnitTests/Support/DiningLoggingTestSupport.swift new file mode 100644 index 00000000..e1fa37c2 --- /dev/null +++ b/koinUnitTests/Support/DiningLoggingTestSupport.swift @@ -0,0 +1,122 @@ +// +// DiningLoggingTestSupport.swift +// koinUnitTests +// +// Created by 이은지 on 8/17/26. +// + +import Combine +import UIKit +@testable import koin + +struct DiningLoggedEvent: Equatable { + let label: String + let category: String + let value: String + + init( + _ label: String, + _ category: String, + _ value: String + ) { + self.label = label + self.category = category + self.value = value + } +} + +final class DiningInputRecorder { + + private(set) var inputs: [DiningViewModel.Input] = [] + private var cancellable: AnyCancellable? + + init(_ viewController: DiningViewController) { + cancellable = viewController.inputSubject.sink { [weak self] input in + self?.inputs.append(input) + } + } + + var loggedEvents: [DiningLoggedEvent] { + inputs.compactMap { input in + guard case let .logEvent(label, category, value) = input else { return nil } + return DiningLoggedEvent(label.rawValue, category.rawValue, "\(value)") + } + } + + var inputKinds: [String] { + inputs.map { input in + switch input { + case .updateDisplayDateTime: "updateDisplayDateTime" + case .shareMenuList: "shareMenuList" + case .determineInitDate: "determineInitDate" + case .changeNoti: "changeNoti" + case .fetchNotiList: "fetchNotiList" + case .logEvent: "logEvent" + case .logEventWithSessionId: "logEventWithSessionId" + } + } + } +} + +@MainActor +final class DiningLoggingTestBed { + + let sut: DiningViewController + let recorder: DiningInputRecorder + + init() { + sut = DiningViewController( + viewModel: DiningViewModel( + fetchDiningListUseCase: StubFetchDiningListUseCase(), + logAnalyticsEventUseCase: StubLogAnalyticsEventUseCase(), + dateProvder: StubDateProvider(), + shareMenuListUseCase: StubShareMenuListUseCase(), + changeNotiUseCase: StubChangeNotiUseCase(), + fetchNotiListUsecase: StubFetchNotiListUseCase(), + changeNotiDetailUseCase: StubChangeNotiDetailUseCase() + ) + ) + + sut.loadViewIfNeeded() + recorder = DiningInputRecorder(sut) + } + + func selectSegment(_ index: Int) { + sut.diningTypeSegmentControl.selectedSegmentIndex = index + } + + func tapSegment(_ index: Int) { + selectSegment(index) + sut.diningTypeSegmentControl.sendActions(for: .valueChanged) + } + + func swipe(_ direction: UISwipeGestureRecognizer.Direction) { + let gesture = UISwipeGestureRecognizer() + gesture.direction = direction + sut.handleSwipe(gesture) + } + + func pullToRefresh() { + sut.diningListCollectionView.refreshControl?.sendActions(for: .valueChanged) + } + + func tapMenuImage(place: String) { + sut.diningListCollectionView.imageTapPublisher.send((UIImage(), place)) + } + + func tapShareButton() { + sut.diningListCollectionView.shareButtonPublisher.send(DiningFixture.item().toShareDiningItem()) + } + + func scrollDiningList() { + sut.diningListCollectionView.logScrollPublisher.send(()) + } + + func tapCafeteriaInfoButton() { + guard let item = sut.navigationItem.rightBarButtonItem, + let action = item.action, + let target = item.target as? NSObject + else { return } + target.perform(action) + } +} From 752b1b6396433d2a2ff708d0f582a881707e98fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:16:30 +0900 Subject: [PATCH 060/101] =?UTF-8?q?test:=20=EC=8B=9D=EB=8B=A8=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Dining/DiningLoggingTests.swift | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 koinUnitTests/Dining/DiningLoggingTests.swift diff --git a/koinUnitTests/Dining/DiningLoggingTests.swift b/koinUnitTests/Dining/DiningLoggingTests.swift new file mode 100644 index 00000000..f7a4d7d5 --- /dev/null +++ b/koinUnitTests/Dining/DiningLoggingTests.swift @@ -0,0 +1,102 @@ +// +// DiningLoggingTests.swift +// koinUnitTests +// +// Created by 이은지 on 8/17/26. +// + +import Foundation +import Testing +import UIKit +@testable import koin + +@MainActor +@Suite("DiningViewController - 로깅") +struct DiningLoggingTests { + + @Test( + "메뉴 이미지를 탭하면 시간대와 장소를 조합해 로깅한다", + arguments: [(1, "A코너", "점심_A코너"), (0, "B코너", "아침_B코너")] + ) + func 메뉴_이미지를_탭하면_시간대와_장소를_조합해_로깅한다(segmentIndex: Int, place: String, expectedValue: String) { + let bed = DiningLoggingTestBed() + bed.selectSegment(segmentIndex) + + bed.tapMenuImage(place: place) + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_image", "click", expectedValue)]) + } + + @Test("공유 버튼을 탭하면 menuShare 이벤트를 로깅한다") + func 공유_버튼을_탭하면_menuShare_이벤트를_로깅한다() { + let bed = DiningLoggingTestBed() + + bed.tapShareButton() + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_share", "click", "공유하기")]) + } + + @Test("공유 버튼을 한 번 탭하면 공유와 로그가 각각 한 번씩 발행된다") + func 공유_버튼을_한_번_탭하면_공유와_로그가_각각_한_번씩_발행된다() { + let bed = DiningLoggingTestBed() + + bed.tapShareButton() + + #expect(bed.recorder.inputKinds == ["shareMenuList", "logEvent"]) + } + + @Test( + "리스트를 스크롤하면 현재 시간대로 menuTime을 로깅한다", + arguments: [(0, "아침"), (1, "점심"), (2, "저녁")] + ) + func 리스트를_스크롤하면_현재_시간대로_menuTime을_로깅한다(segmentIndex: Int, expectedValue: String) { + let bed = DiningLoggingTestBed() + bed.selectSegment(segmentIndex) + + bed.scrollDiningList() + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "scroll", expectedValue)]) + } + + @Test( + "당겨서 새로고침하면 현재 시간대로 menuTime을 로깅한다", + arguments: [(0, "아침"), (1, "점심"), (2, "저녁")] + ) + func 당겨서_새로고침하면_현재_시간대로_menuTime을_로깅한다(segmentIndex: Int, expectedValue: String) { + let bed = DiningLoggingTestBed() + bed.selectSegment(segmentIndex) + + bed.pullToRefresh() + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", expectedValue)]) + } + + @Test("학생식당 정보 버튼을 탭하면 cafeteriaInfo 이벤트를 로깅한다") + func 학생식당_정보_버튼을_탭하면_cafeteriaInfo_이벤트를_로깅한다() { + let bed = DiningLoggingTestBed() + + bed.tapCafeteriaInfoButton() + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("cafeteria_info", "click", "학생식당정보")]) + } + + @Test("세그먼트를 전환하면 변경된 시간대로 menuTime을 로깅한다") + func 세그먼트를_전환하면_변경된_시간대로_menuTime을_로깅한다() { + let bed = DiningLoggingTestBed() + bed.selectSegment(0) + + bed.tapSegment(1) + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", "점심")]) + } + + @Test("스와이프로 전환해도 동일하게 로깅한다") + func 스와이프로_전환해도_동일하게_로깅한다() { + let bed = DiningLoggingTestBed() + bed.selectSegment(1) + + bed.swipe(.left) + + #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", "저녁")]) + } +} From 15c88453baa675d1a51a3729beb0063b2bd7cf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:16:33 +0900 Subject: [PATCH 061/101] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B9=85=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20koinUnitTest?= =?UTF-8?q?s=20=ED=83=80=EA=B2=9F=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koin.xcodeproj/project.pbxproj | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a0a0f5bc..7aec8453 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -736,6 +736,10 @@ AA20000000000000000004 /* DateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000004 /* DateProviderTests.swift */; }; AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */; }; AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */; }; + AA20000000000000000007 /* DiningLoggingTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000007 /* DiningLoggingTestSupport.swift */; }; + AA20000000000000000008 /* DiningViewModelStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000008 /* DiningViewModelStubs.swift */; }; + AA20000000000000000009 /* DiningLoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000009 /* DiningLoggingTests.swift */; }; + AA20000000000000000010 /* DiningTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000010 /* DiningTypeTests.swift */; }; B438AEE42E756AB600E889C4 /* OrderImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B438AEE32E756AB600E889C4 /* OrderImage.swift */; }; B46B8CC22E76CB7300A8E797 /* ShopSummaryDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */; }; B46B8CC42E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */; }; @@ -1744,6 +1748,10 @@ AA10000000000000000004 /* DateProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderTests.swift; sourceTree = ""; }; AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchDiningListUseCaseTests.swift; sourceTree = ""; }; AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCaseTests.swift; sourceTree = ""; }; + AA10000000000000000007 /* DiningLoggingTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningLoggingTestSupport.swift; sourceTree = ""; }; + AA10000000000000000008 /* DiningViewModelStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningViewModelStubs.swift; sourceTree = ""; }; + AA10000000000000000009 /* DiningLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningLoggingTests.swift; sourceTree = ""; }; + AA10000000000000000010 /* DiningTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningTypeTests.swift; sourceTree = ""; }; B438AEE32E756AB600E889C4 /* OrderImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderImage.swift; sourceTree = ""; }; B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSummaryDto.swift; sourceTree = ""; }; B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopSummaryFromShopUseCase.swift; sourceTree = ""; }; @@ -4532,6 +4540,7 @@ children = ( AA10000000000000000001 /* PublisherTestSupport.swift */, AA10000000000000000002 /* DiningFixtures.swift */, + AA10000000000000000007 /* DiningLoggingTestSupport.swift */, ); path = Support; sourceTree = ""; @@ -4540,6 +4549,7 @@ isa = PBXGroup; children = ( AA10000000000000000003 /* SpyDiningRepository.swift */, + AA10000000000000000008 /* DiningViewModelStubs.swift */, ); path = Doubles; sourceTree = ""; @@ -4550,6 +4560,8 @@ AA10000000000000000004 /* DateProviderTests.swift */, AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */, AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */, + AA10000000000000000009 /* DiningLoggingTests.swift */, + AA10000000000000000010 /* DiningTypeTests.swift */, ); path = Dining; sourceTree = ""; @@ -6628,6 +6640,10 @@ AA20000000000000000004 /* DateProviderTests.swift in Sources */, AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */, AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */, + AA20000000000000000007 /* DiningLoggingTestSupport.swift in Sources */, + AA20000000000000000008 /* DiningViewModelStubs.swift in Sources */, + AA20000000000000000009 /* DiningLoggingTests.swift in Sources */, + AA20000000000000000010 /* DiningTypeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 8a6daf6c4e347899c12a7121f8be53f634d48172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Mon, 17 Aug 2026 20:33:48 +0900 Subject: [PATCH 062/101] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=ED=83=AD=20arguments=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Dining/DiningLoggingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koinUnitTests/Dining/DiningLoggingTests.swift b/koinUnitTests/Dining/DiningLoggingTests.swift index f7a4d7d5..4229a5c7 100644 --- a/koinUnitTests/Dining/DiningLoggingTests.swift +++ b/koinUnitTests/Dining/DiningLoggingTests.swift @@ -16,7 +16,7 @@ struct DiningLoggingTests { @Test( "메뉴 이미지를 탭하면 시간대와 장소를 조합해 로깅한다", - arguments: [(1, "A코너", "점심_A코너"), (0, "B코너", "아침_B코너")] + arguments: [(0, "A코너", "아침_A코너"), (1, "B코너", "점심_B코너"), (2, "C코너", "저녁_C코너")] ) func 메뉴_이미지를_탭하면_시간대와_장소를_조합해_로깅한다(segmentIndex: Int, place: String, expectedValue: String) { let bed = DiningLoggingTestBed() From 7d436ea5146d6daf83fd6575aa4f7cdb24d72294 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 18 Aug 2026 17:08:24 +0900 Subject: [PATCH 063/101] 5.1.2(444) --- koin.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 06345705..45f89572 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -6871,7 +6871,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 440; + CURRENT_PROJECT_VERSION = 444; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -6890,7 +6890,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.0; + MARKETING_VERSION = 5.1.2; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin.stage; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -6906,7 +6906,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 440; + CURRENT_PROJECT_VERSION = 444; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -6925,7 +6925,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.0; + MARKETING_VERSION = 5.1.2; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; From 22569d2312c7d7af7dd1a263d000808536505a0b Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Wed, 19 Aug 2026 17:27:25 +0900 Subject: [PATCH 064/101] =?UTF-8?q?refactor:=20Chat=EC=9D=84=20LostItemCha?= =?UTF-8?q?t=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Apps/SceneDelegate.swift | 8 +- Koin/Core/Extensions/Common/String+.swift | 6 +- .../LostItemChat/LostItemChatDetailDto.swift} | 13 +- .../LostItemChat/LostItemChatRoomDto.swift} | 12 +- .../LostItemCreateChatRoomResponse.swift} | 4 +- .../LostItemPostChatDetailRequest.swift} | 4 +- .../Repository/DefaultChatRepository.swift | 37 --- .../DefaultLostItemRepository.swift | 20 ++ Koin/Data/Service/ChatService.swift | 42 --- Koin/Data/Service/LostItemService.swift | 26 ++ Koin/Data/Service/Network/API/ChatAPI.swift | 70 ----- .../Service/Network/API/LostItemAPI.swift | 31 ++- .../LostItemChatDateInfo.swift} | 4 +- .../LostItemChatHistoryData.swift} | 6 +- .../LostItemChatRoomItem.swift} | 6 +- Koin/Domain/Repository/ChatRepository.swift | 16 -- .../Repository/LostItemRepository.swift | 6 + .../UseCase/Chat/CreateChatRoomUseCase.swift | 25 -- .../UseCase/Chat/FetchChatRoomUseCase.swift | 26 -- .../UseCase/Chat/PostChatDetailUseCase.swift | 27 -- .../LostItemBlockUserUseCase.swift} | 10 +- .../LostItemCreateChatRoomUseCase.swift | 24 ++ .../LostItemFetchChatDetailUseCase.swift} | 14 +- .../LostItemFetchChatRoomUseCase.swift | 26 ++ .../LostItemPostChatDetailUseCase.swift | 27 ++ .../Category/CategoryHostingController.swift | 2 +- .../Home/Home/HomeHostingController.swift | 2 +- .../NotificationViewController.swift | 8 +- ...stItemBlockCheckModalViewController.swift} | 6 +- .../LostItemChatDateHeaderView.swift} | 10 +- .../LostItemChatHistoryTableView.swift} | 30 +-- .../LostItemChatImageTableViewCell.swift} | 10 +- .../LostItemChatTextTableViewCell.swift} | 10 +- .../LostItemChatViewController.swift} | 18 +- .../LostItemChat/LostItemChatViewModel.swift} | 22 +- ...LostItemChatListTableViewController.swift} | 22 +- .../LostItemChatListTableViewModel.swift} | 19 +- .../LostItemDataViewController.swift | 10 +- .../LostItemData/LostItemDataViewModel.swift | 6 +- .../LostItemListViewController.swift | 4 +- .../PostLostItemViewController.swift | 4 +- koin.xcodeproj/project.pbxproj | 240 ++++++++---------- 42 files changed, 393 insertions(+), 520 deletions(-) rename Koin/Data/DTOs/Decodable/{Chat/ChatDetailDto.swift => LostItem/LostItemChat/LostItemChatDetailDto.swift} (71%) rename Koin/Data/DTOs/Decodable/{Chat/ChatRoomDto.swift => LostItem/LostItemChat/LostItemChatRoomDto.swift} (81%) rename Koin/Data/DTOs/Decodable/{Chat/CreateChatRoomResponse.swift => LostItem/LostItemChat/LostItemCreateChatRoomResponse.swift} (84%) rename Koin/Data/DTOs/Encodable/{Chat/PostChatDetailRequest.swift => LostItem/LostItemChat/LostItemPostChatDetailRequest.swift} (77%) delete mode 100644 Koin/Data/Repository/DefaultChatRepository.swift delete mode 100644 Koin/Data/Service/ChatService.swift delete mode 100644 Koin/Data/Service/Network/API/ChatAPI.swift rename Koin/Domain/Model/{Chat/ChatDateInfo.swift => LostItem/LostItemChatDateInfo.swift} (81%) rename Koin/Domain/Model/{Chat/ChatHistoryData.swift => LostItem/LostItemChatHistoryData.swift} (65%) rename Koin/Domain/Model/{Chat/ChatRoomItem.swift => LostItem/LostItemChatRoomItem.swift} (73%) delete mode 100644 Koin/Domain/Repository/ChatRepository.swift delete mode 100644 Koin/Domain/UseCase/Chat/CreateChatRoomUseCase.swift delete mode 100644 Koin/Domain/UseCase/Chat/FetchChatRoomUseCase.swift delete mode 100644 Koin/Domain/UseCase/Chat/PostChatDetailUseCase.swift rename Koin/Domain/UseCase/{Chat/BlockUserUseCase.swift => LostItem/LostItemBlockUserUseCase.swift} (62%) create mode 100644 Koin/Domain/UseCase/LostItem/LostItemCreateChatRoomUseCase.swift rename Koin/Domain/UseCase/{Chat/FetchChatDetailUseCase.swift => LostItem/LostItemFetchChatDetailUseCase.swift} (57%) create mode 100644 Koin/Domain/UseCase/LostItem/LostItemFetchChatRoomUseCase.swift create mode 100644 Koin/Domain/UseCase/LostItem/LostItemPostChatDetailUseCase.swift rename Koin/Presentation/{Chat/Chat/BlockCheckModalViewController.swift => LostItem/LostItemChat/LostItemBlockCheckModalViewController.swift} (93%) rename Koin/Presentation/{Chat/Chat/ChatHistoryTableView/ChatDateHeaderView.swift => LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatDateHeaderView.swift} (84%) rename Koin/Presentation/{Chat/Chat/ChatHistoryTableView/ChatHistoryTableView.swift => LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatHistoryTableView.swift} (77%) rename Koin/Presentation/{Chat/Chat/ChatHistoryTableView/ChatImageTableViewCell.swift => LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatImageTableViewCell.swift} (91%) rename Koin/Presentation/{Chat/Chat/ChatHistoryTableView/ChatTextTableViewCell.swift => LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatTextTableViewCell.swift} (90%) rename Koin/Presentation/{Chat/Chat/ChatViewController.swift => LostItem/LostItemChat/LostItemChatViewController.swift} (94%) rename Koin/Presentation/{Chat/Chat/ChatViewModel.swift => LostItem/LostItemChat/LostItemChatViewModel.swift} (85%) rename Koin/Presentation/{Chat/ChatList/ChatListTableViewController.swift => LostItem/LostItemChatList/LostItemChatListTableViewController.swift} (89%) rename Koin/Presentation/{Chat/ChatList/ChatListTableViewModel.swift => LostItem/LostItemChatList/LostItemChatListTableViewModel.swift} (79%) diff --git a/Koin/Apps/SceneDelegate.swift b/Koin/Apps/SceneDelegate.swift index a92eeeeb..6d1c01f9 100644 --- a/Koin/Apps/SceneDelegate.swift +++ b/Koin/Apps/SceneDelegate.swift @@ -137,8 +137,8 @@ extension SceneDelegate { case .chat: if let articleId = Int(parsedQuery["articleId"]), let chatRoomId = Int(parsedQuery["chatRoomId"]) { - let viewModel = ChatViewModel(articleId: articleId, chatRoomId: chatRoomId, articleTitle: nil) - let chatViewController = ChatViewController(viewModel: viewModel) + let viewModel = LostItemChatViewModel(articleId: articleId, chatRoomId: chatRoomId, articleTitle: nil) + let chatViewController = LostItemChatViewController(viewModel: viewModel) navigationController?.pushViewController(chatViewController, animated: true) } case .callvan: @@ -373,13 +373,13 @@ extension SceneDelegate { private func makeLostItemData(lostItemId: Int) -> UIViewController { let userRepository = DefaultUserRepository(service: DefaultUserService()) let lostItemRepository = DefaultLostItemRepository(service: DefaultLostItemService()) - let chatRepository = DefaultChatRepository(service: DefaultChatService()) + let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: userRepository) let fetchLostItemDataUseCase = DefaultFetchLostItemDataUseCase(repository: lostItemRepository) let fetchLostItemListUseCase = DefaultFetchLostItemListUseCase(repository: lostItemRepository) let changeLostItemStateUseCase = DefaultChangeLostItemStateUseCase(repository: lostItemRepository) let deleteLostItemUseCase = DefaultDeleteLostItemUseCase(repository: lostItemRepository) - let createChatRoomUseCase = DefaultCreateChatRoomUseCase(chatRepository: chatRepository) + let createChatRoomUseCase = DefaultLostItemCreateChatRoomUseCase(chatRepository: chatRepository) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) let viewModel = LostItemDataViewModel( checkLoginUseCase: checkLoginUseCase, diff --git a/Koin/Core/Extensions/Common/String+.swift b/Koin/Core/Extensions/Common/String+.swift index 3418e1ce..e226f405 100644 --- a/Koin/Core/Extensions/Common/String+.swift +++ b/Koin/Core/Extensions/Common/String+.swift @@ -10,7 +10,7 @@ import Kingfisher import SwiftSoup extension String { - func toChatDateInfo() -> ChatDateInfo { + func toLostItemChatDateInfo() -> LostItemChatDateInfo { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.timeZone = TimeZone(secondsFromGMT: 0) // ✅ UTC 그대로 변환 @@ -31,7 +31,7 @@ extension String { // ✅ 문자열을 Date 타입으로 변환 (UTC 기준) guard let date = formatter.date(from: formattedDateString) else { print("❌ 변환 실패: \(formattedDateString)") - return ChatDateInfo( + return LostItemChatDateInfo( year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0, isToday: false, isYesterday: false, showingText: "날짜 오류" ) @@ -81,7 +81,7 @@ extension String { } - return ChatDateInfo( + return LostItemChatDateInfo( year: components.year ?? 0, month: components.month ?? 0, day: components.day ?? 0, diff --git a/Koin/Data/DTOs/Decodable/Chat/ChatDetailDto.swift b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatDetailDto.swift similarity index 71% rename from Koin/Data/DTOs/Decodable/Chat/ChatDetailDto.swift rename to Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatDetailDto.swift index e53f8941..05f7fbc2 100644 --- a/Koin/Data/DTOs/Decodable/Chat/ChatDetailDto.swift +++ b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatDetailDto.swift @@ -1,5 +1,5 @@ // -// ChatDetailDto.swift +// LostItemChatDetailDto.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,7 +7,7 @@ import Foundation -struct ChatDetailDto: Codable { +struct LostItemChatDetailDto: Codable { let userId: Int let userNickname, content, timestamp: String let isImage: Bool @@ -20,15 +20,14 @@ struct ChatDetailDto: Codable { } } -extension ChatDetailDto { - func toDomain(currentUserId: Int) -> ChatMessage { - return ChatMessage( +extension LostItemChatDetailDto { + func toDomain(currentUserId: Int) -> LostItemChatMessage { + return LostItemChatMessage( senderNickname: userNickname, content: content, timestamp: timestamp, isImage: isImage, - isMine: userId == currentUserId, chatDateInfo: timestamp.toChatDateInfo() + isMine: userId == currentUserId, chatDateInfo: timestamp.toLostItemChatDateInfo() ) } } - diff --git a/Koin/Data/DTOs/Decodable/Chat/ChatRoomDto.swift b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatRoomDto.swift similarity index 81% rename from Koin/Data/DTOs/Decodable/Chat/ChatRoomDto.swift rename to Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatRoomDto.swift index 151bfb23..ca1c2276 100644 --- a/Koin/Data/DTOs/Decodable/Chat/ChatRoomDto.swift +++ b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemChatRoomDto.swift @@ -1,5 +1,5 @@ // -// ChatRoomDto.swift +// LostItemChatRoomDto.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,7 +7,7 @@ import Foundation -struct ChatRoomDto: Codable { +struct LostItemChatRoomDto: Codable { let articleTitle, recentMessageContent: String let lostItemImageUrl: String? let unreadMessageCount: Int @@ -24,16 +24,16 @@ struct ChatRoomDto: Codable { case chatRoomId = "chat_room_id" } } -extension ChatRoomDto { - func toDomain() -> ChatRoomItem { +extension LostItemChatRoomDto { + func toDomain() -> LostItemChatRoomItem { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return ChatRoomItem( + return LostItemChatRoomItem( articleTitle: articleTitle, recentMessageContent: recentMessageContent, lostItemImageUrl: lostItemImageUrl, unreadMessageCount: unreadMessageCount, lastMessageAt: lastMessageAt, - chatDateInfo: lastMessageAt.toChatDateInfo(), + chatDateInfo: lastMessageAt.toLostItemChatDateInfo(), articleId: articleId, chatRoomId: chatRoomId ) diff --git a/Koin/Data/DTOs/Decodable/Chat/CreateChatRoomResponse.swift b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemCreateChatRoomResponse.swift similarity index 84% rename from Koin/Data/DTOs/Decodable/Chat/CreateChatRoomResponse.swift rename to Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemCreateChatRoomResponse.swift index ddf3ccf8..1c54f348 100644 --- a/Koin/Data/DTOs/Decodable/Chat/CreateChatRoomResponse.swift +++ b/Koin/Data/DTOs/Decodable/LostItem/LostItemChat/LostItemCreateChatRoomResponse.swift @@ -1,5 +1,5 @@ // -// CreateCharRoomResponse.swift +// LostItemCreateChatRoomResponse.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,7 +7,7 @@ import Foundation -struct CreateChatRoomResponse: Decodable { +struct LostItemCreateChatRoomResponse: Decodable { let articleId: Int let chatRoomId: Int let userId: Int diff --git a/Koin/Data/DTOs/Encodable/Chat/PostChatDetailRequest.swift b/Koin/Data/DTOs/Encodable/LostItem/LostItemChat/LostItemPostChatDetailRequest.swift similarity index 77% rename from Koin/Data/DTOs/Encodable/Chat/PostChatDetailRequest.swift rename to Koin/Data/DTOs/Encodable/LostItem/LostItemChat/LostItemPostChatDetailRequest.swift index b16a31eb..38dddc0d 100644 --- a/Koin/Data/DTOs/Encodable/Chat/PostChatDetailRequest.swift +++ b/Koin/Data/DTOs/Encodable/LostItem/LostItemChat/LostItemPostChatDetailRequest.swift @@ -1,5 +1,5 @@ // -// PostChatDetailRequest.swift +// LostItemPostChatDetailRequest.swift // koin // // Created by 홍기정 on 1/28/26. @@ -7,7 +7,7 @@ import Foundation -struct PostChatDetailRequest: Encodable { +struct LostItemPostChatDetailRequest: Encodable { let userNickname: String let content: String diff --git a/Koin/Data/Repository/DefaultChatRepository.swift b/Koin/Data/Repository/DefaultChatRepository.swift deleted file mode 100644 index 48fdc3ce..00000000 --- a/Koin/Data/Repository/DefaultChatRepository.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DefaultChatRepository.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Combine - -final class DefaultChatRepository: ChatRepository { - - private let service: ChatService - - init(service: ChatService) { - self.service = service - } - - func createChatRoom(articleId: Int) -> AnyPublisher { - service.createChatRoom(articleId: articleId) - } - - func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher { - service.blockUser(articleId: articleId, chatRoomId: chatRoomId) - } - - func fetchChatRoom() -> AnyPublisher<[ChatRoomDto], ErrorResponse> { - service.fetchChatRoom() - } - - func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatDetailDto], ErrorResponse> { - service.fetchChatDetail(articleId: articleId, chatRoomId: chatRoomId) - } - - func postChatDetail(articleId: Int, chatRoomId: Int, request: PostChatDetailRequest) -> AnyPublisher { - service.postChatDetail(articleId: articleId, chatRoomId: chatRoomId, request: request) - } -} diff --git a/Koin/Data/Repository/DefaultLostItemRepository.swift b/Koin/Data/Repository/DefaultLostItemRepository.swift index cd3b8fd0..745d6915 100644 --- a/Koin/Data/Repository/DefaultLostItemRepository.swift +++ b/Koin/Data/Repository/DefaultLostItemRepository.swift @@ -79,4 +79,24 @@ final class DefaultLostItemRepository: LostItemRepository { func unsubscribeKeyword(id: Int) -> AnyPublisher { return service.unsubscribeKeyword(id: id) } + + func createChatRoom(articleId: Int) -> AnyPublisher { + service.createChatRoom(articleId: articleId) + } + + func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher { + service.blockUser(articleId: articleId, chatRoomId: chatRoomId) + } + + func fetchChatRoom() -> AnyPublisher<[LostItemChatRoomDto], ErrorResponse> { + service.fetchChatRoom() + } + + func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatDetailDto], ErrorResponse> { + service.fetchChatDetail(articleId: articleId, chatRoomId: chatRoomId) + } + + func postChatDetail(articleId: Int, chatRoomId: Int, request: LostItemPostChatDetailRequest) -> AnyPublisher { + service.postChatDetail(articleId: articleId, chatRoomId: chatRoomId, request: request) + } } diff --git a/Koin/Data/Service/ChatService.swift b/Koin/Data/Service/ChatService.swift deleted file mode 100644 index ea5479bc..00000000 --- a/Koin/Data/Service/ChatService.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ChatService.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Alamofire -import Combine - -protocol ChatService { - func fetchChatRoom() -> AnyPublisher<[ChatRoomDto], ErrorResponse> - func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatDetailDto], ErrorResponse> - func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher - func createChatRoom(articleId: Int) -> AnyPublisher - func postChatDetail(articleId: Int, chatRoomId: Int, request: PostChatDetailRequest) -> AnyPublisher -} - -final class DefaultChatService: ChatService { - - private let networkService = NetworkService.shared - - func createChatRoom(articleId: Int) -> AnyPublisher { - return networkService.requestWithResponse(api: ChatAPI.createChatRoom(articleId)) - } - - func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher { - return networkService.request(api: ChatAPI.blockUser(articleId, chatRoomId)) - } - - func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatDetailDto], ErrorResponse> { - return networkService.requestWithResponse(api: ChatAPI.fetchChatDetail(articleId, chatRoomId)) - } - - func fetchChatRoom() -> AnyPublisher<[ChatRoomDto], ErrorResponse> { - return networkService.requestWithResponse(api: ChatAPI.fetchChatRoom) - } - - func postChatDetail(articleId: Int, chatRoomId: Int, request: PostChatDetailRequest) -> AnyPublisher { - return networkService.requestWithResponse(api: ChatAPI.postChatDetail(articleId, chatRoomId, request)) - } -} diff --git a/Koin/Data/Service/LostItemService.swift b/Koin/Data/Service/LostItemService.swift index 3c899b50..dfe98bf3 100644 --- a/Koin/Data/Service/LostItemService.swift +++ b/Koin/Data/Service/LostItemService.swift @@ -23,6 +23,12 @@ protocol LostItemService { func fetchKeywordSuggestion() -> AnyPublisher func fetchMyKeyword() -> AnyPublisher func unsubscribeKeyword(id: Int) -> AnyPublisher + + func fetchChatRoom() -> AnyPublisher<[LostItemChatRoomDto], ErrorResponse> + func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatDetailDto], ErrorResponse> + func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher + func createChatRoom(articleId: Int) -> AnyPublisher + func postChatDetail(articleId: Int, chatRoomId: Int, request: LostItemPostChatDetailRequest) -> AnyPublisher } final class DefaultLostItemService: LostItemService { @@ -76,4 +82,24 @@ final class DefaultLostItemService: LostItemService { func unsubscribeKeyword(id: Int) -> AnyPublisher { return networkService.request(api: LostItemAPI.unsubscribeKeyword(id)) } + + func createChatRoom(articleId: Int) -> AnyPublisher { + return networkService.requestWithResponse(api: LostItemAPI.createChatRoom(articleId)) + } + + func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher { + return networkService.request(api: LostItemAPI.blockUser(articleId, chatRoomId)) + } + + func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatDetailDto], ErrorResponse> { + return networkService.requestWithResponse(api: LostItemAPI.fetchChatDetail(articleId, chatRoomId)) + } + + func fetchChatRoom() -> AnyPublisher<[LostItemChatRoomDto], ErrorResponse> { + return networkService.requestWithResponse(api: LostItemAPI.fetchChatRoom) + } + + func postChatDetail(articleId: Int, chatRoomId: Int, request: LostItemPostChatDetailRequest) -> AnyPublisher { + return networkService.requestWithResponse(api: LostItemAPI.postChatDetail(articleId, chatRoomId, request)) + } } diff --git a/Koin/Data/Service/Network/API/ChatAPI.swift b/Koin/Data/Service/Network/API/ChatAPI.swift deleted file mode 100644 index 9f651217..00000000 --- a/Koin/Data/Service/Network/API/ChatAPI.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// ChatAPI.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Foundation -import Alamofire - -enum ChatAPI { - case fetchChatRoom - case fetchChatDetail(Int, Int) - case blockUser(Int, Int) - case createChatRoom(Int) - case postChatDetail(Int, Int, PostChatDetailRequest) -} - -extension ChatAPI: Router, URLRequestConvertible { - - public var baseURL: String { - return Bundle.main.baseUrl - } - - public var path: String { - switch self { - case .fetchChatRoom: return "/chatroom/lost-item" - case .fetchChatDetail(let articleId, let chatRoomId): return "/chatroom/lost-item/\(articleId)/\(chatRoomId)/messages" - case .blockUser(let articleId, let chatRoomId): return "/chatroom/lost-item/\(articleId)/\(chatRoomId)/block" - case .createChatRoom(let articleId): return "/chatroom/lost-item/\(articleId)" - case .postChatDetail(let articleId, let chatRoomId, _): return "/v2/chatroom/lost-item/\(articleId)/\(chatRoomId)/messages" - } - } - - public var method: Alamofire.HTTPMethod { - switch self { - case .fetchChatRoom, .fetchChatDetail: return .get - case .blockUser, .createChatRoom, .postChatDetail: return .post - } - } - - public var headers: [String: String] { - var baseHeaders: [String: String] = [:] - switch self { - case .createChatRoom, .postChatDetail: - baseHeaders["Content-Type"] = "application/json" - default: break - } - return baseHeaders - } - - public var parameters: Any? { - switch self { - case .fetchChatRoom, .fetchChatDetail, .blockUser, .createChatRoom: - return nil - case .postChatDetail(_, _, let request): - return try? JSONEncoder().encode(request) - } - } - - public var encoding: ParameterEncoding? { - switch self { - case .fetchChatRoom: return URLEncoding.default - case .postChatDetail: return JSONEncoding.default - case .fetchChatDetail: return nil - case .blockUser: return nil - case .createChatRoom: return nil - } - } -} diff --git a/Koin/Data/Service/Network/API/LostItemAPI.swift b/Koin/Data/Service/Network/API/LostItemAPI.swift index d9505603..95c80c49 100644 --- a/Koin/Data/Service/Network/API/LostItemAPI.swift +++ b/Koin/Data/Service/Network/API/LostItemAPI.swift @@ -22,6 +22,12 @@ enum LostItemAPI { case fetchKeywordSuggestion case fetchMyKeyword case unsubscribeKeyword(Int) + + case fetchChatRoom + case fetchChatDetail(Int, Int) + case blockUser(Int, Int) + case createChatRoom(Int) + case postChatDetail(Int, Int, LostItemPostChatDetailRequest) } extension LostItemAPI: Router, URLRequestConvertible { @@ -44,6 +50,12 @@ extension LostItemAPI: Router, URLRequestConvertible { case .fetchKeywordSuggestion: return "/articles/keyword/suggestions?type=LOST_ITEM" case .fetchMyKeyword: return "/articles/keyword/me?type=LOST_ITEM" case .unsubscribeKeyword(let id): return "/articles/keyword/\(id)" + + case .fetchChatRoom: return "/chatroom/lost-item" + case .fetchChatDetail(let articleId, let chatRoomId): return "/chatroom/lost-item/\(articleId)/\(chatRoomId)/messages" + case .blockUser(let articleId, let chatRoomId): return "/chatroom/lost-item/\(articleId)/\(chatRoomId)/block" + case .createChatRoom(let articleId): return "/chatroom/lost-item/\(articleId)" + case .postChatDetail(let articleId, let chatRoomId, _): return "/v2/chatroom/lost-item/\(articleId)/\(chatRoomId)/messages" } } @@ -62,11 +74,19 @@ extension LostItemAPI: Router, URLRequestConvertible { case .fetchKeywordSuggestion: return .get case .fetchMyKeyword: return .get case .unsubscribeKeyword: return .delete + + case .fetchChatRoom, .fetchChatDetail: return .get + case .blockUser, .createChatRoom, .postChatDetail: return .post } } public var headers: [String: String] { - return [:] + switch self { + case .createChatRoom, .postChatDetail: + return ["Content-Type": "application/json"] + default: + return [:] + } } public var parameters: Any? { @@ -86,6 +106,11 @@ extension LostItemAPI: Router, URLRequestConvertible { return try? request.toDictionary() case .fetchKeywordSuggestion, .fetchMyKeyword, .unsubscribeKeyword: return nil + + case .fetchChatRoom, .fetchChatDetail, .blockUser, .createChatRoom: + return nil + case .postChatDetail(_, _, let request): + return try? JSONEncoder().encode(request) } } @@ -104,6 +129,10 @@ extension LostItemAPI: Router, URLRequestConvertible { return JSONEncoding.default case .fetchKeywordSuggestion, .fetchMyKeyword, .unsubscribeKeyword: return nil + + case .fetchChatRoom: return URLEncoding.default + case .postChatDetail: return JSONEncoding.default + case .fetchChatDetail, .blockUser, .createChatRoom: return nil } } } diff --git a/Koin/Domain/Model/Chat/ChatDateInfo.swift b/Koin/Domain/Model/LostItem/LostItemChatDateInfo.swift similarity index 81% rename from Koin/Domain/Model/Chat/ChatDateInfo.swift rename to Koin/Domain/Model/LostItem/LostItemChatDateInfo.swift index 41458352..a287b858 100644 --- a/Koin/Domain/Model/Chat/ChatDateInfo.swift +++ b/Koin/Domain/Model/LostItem/LostItemChatDateInfo.swift @@ -1,5 +1,5 @@ // -// ChatDateInfo.swift +// LostItemChatDateInfo.swift // koin // // Created by 김나훈 on 2/20/25. @@ -7,7 +7,7 @@ import Foundation -struct ChatDateInfo { +struct LostItemChatDateInfo { let year: Int let month: Int let day: Int diff --git a/Koin/Domain/Model/Chat/ChatHistoryData.swift b/Koin/Domain/Model/LostItem/LostItemChatHistoryData.swift similarity index 65% rename from Koin/Domain/Model/Chat/ChatHistoryData.swift rename to Koin/Domain/Model/LostItem/LostItemChatHistoryData.swift index 83c4e37d..e79f75ff 100644 --- a/Koin/Domain/Model/Chat/ChatHistoryData.swift +++ b/Koin/Domain/Model/LostItem/LostItemChatHistoryData.swift @@ -1,5 +1,5 @@ // -// ChatHistoryData.swift +// LostItemChatHistoryData.swift // koin // // Created by 김나훈 on 2/20/25. @@ -7,11 +7,11 @@ import Foundation -struct ChatMessage { +struct LostItemChatMessage { let senderNickname: String let content: String let timestamp: String let isImage: Bool let isMine: Bool - let chatDateInfo: ChatDateInfo + let chatDateInfo: LostItemChatDateInfo } diff --git a/Koin/Domain/Model/Chat/ChatRoomItem.swift b/Koin/Domain/Model/LostItem/LostItemChatRoomItem.swift similarity index 73% rename from Koin/Domain/Model/Chat/ChatRoomItem.swift rename to Koin/Domain/Model/LostItem/LostItemChatRoomItem.swift index 65d68671..21016013 100644 --- a/Koin/Domain/Model/Chat/ChatRoomItem.swift +++ b/Koin/Domain/Model/LostItem/LostItemChatRoomItem.swift @@ -1,5 +1,5 @@ // -// ChatRoomItem.swift +// LostItemChatRoomItem.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,13 +7,13 @@ import Foundation -struct ChatRoomItem { +struct LostItemChatRoomItem { let articleTitle: String let recentMessageContent: String let lostItemImageUrl: String? let unreadMessageCount: Int let lastMessageAt: String - let chatDateInfo: ChatDateInfo + let chatDateInfo: LostItemChatDateInfo let articleId: Int let chatRoomId: Int } diff --git a/Koin/Domain/Repository/ChatRepository.swift b/Koin/Domain/Repository/ChatRepository.swift deleted file mode 100644 index ea09f708..00000000 --- a/Koin/Domain/Repository/ChatRepository.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ChatRepository.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Combine - -protocol ChatRepository { - func fetchChatRoom() -> AnyPublisher<[ChatRoomDto], ErrorResponse> - func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatDetailDto], ErrorResponse> - func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher - func createChatRoom(articleId: Int) -> AnyPublisher - func postChatDetail(articleId: Int, chatRoomId: Int, request: PostChatDetailRequest) -> AnyPublisher -} diff --git a/Koin/Domain/Repository/LostItemRepository.swift b/Koin/Domain/Repository/LostItemRepository.swift index d83f3323..f01c8aff 100644 --- a/Koin/Domain/Repository/LostItemRepository.swift +++ b/Koin/Domain/Repository/LostItemRepository.swift @@ -23,4 +23,10 @@ protocol LostItemRepository { func fetchKeywordSuggestion() -> AnyPublisher<[String], ErrorResponse> func fetchMyKeyword() -> AnyPublisher func unsubscribeKeyword(id: Int) -> AnyPublisher + + func fetchChatRoom() -> AnyPublisher<[LostItemChatRoomDto], ErrorResponse> + func fetchChatDetail(articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatDetailDto], ErrorResponse> + func blockUser(articleId: Int, chatRoomId: Int) -> AnyPublisher + func createChatRoom(articleId: Int) -> AnyPublisher + func postChatDetail(articleId: Int, chatRoomId: Int, request: LostItemPostChatDetailRequest) -> AnyPublisher } diff --git a/Koin/Domain/UseCase/Chat/CreateChatRoomUseCase.swift b/Koin/Domain/UseCase/Chat/CreateChatRoomUseCase.swift deleted file mode 100644 index 9ba32675..00000000 --- a/Koin/Domain/UseCase/Chat/CreateChatRoomUseCase.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CreateChatRoomUseCase.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Combine - -protocol CreateChatRoomUseCase { - func execute(articleId: Int) -> AnyPublisher -} -final class DefaultCreateChatRoomUseCase: CreateChatRoomUseCase { - - private let chatRepository: ChatRepository - - init(chatRepository: ChatRepository) { - self.chatRepository = chatRepository - } - - func execute(articleId: Int) -> AnyPublisher { - return chatRepository.createChatRoom(articleId: articleId) - } -} - diff --git a/Koin/Domain/UseCase/Chat/FetchChatRoomUseCase.swift b/Koin/Domain/UseCase/Chat/FetchChatRoomUseCase.swift deleted file mode 100644 index 6d12bfb6..00000000 --- a/Koin/Domain/UseCase/Chat/FetchChatRoomUseCase.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FetchChatRoomUseCase.swift -// koin -// -// Created by 김나훈 on 2/18/25. -// - -import Combine - -protocol FetchChatRoomUseCase { - func execute() -> AnyPublisher<[ChatRoomItem], ErrorResponse> -} - -final class DefaultFetchChatRoomUseCase: FetchChatRoomUseCase { - private let chatRepository: ChatRepository - - init(chatRepository: ChatRepository) { - self.chatRepository = chatRepository - } - - func execute() -> AnyPublisher<[ChatRoomItem], ErrorResponse> { - return chatRepository.fetchChatRoom() - .map { $0.map { $0.toDomain() } } - .eraseToAnyPublisher() - } -} diff --git a/Koin/Domain/UseCase/Chat/PostChatDetailUseCase.swift b/Koin/Domain/UseCase/Chat/PostChatDetailUseCase.swift deleted file mode 100644 index 04bcd860..00000000 --- a/Koin/Domain/UseCase/Chat/PostChatDetailUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PostChatDetailUseCase.swift -// koin -// -// Created by 홍기정 on 1/28/26. -// - -import Foundation -import Combine - -protocol PostChatDetailUseCase { - func execute(articleId: Int, chatRoomId: Int, message: String, isImage: Bool) -> AnyPublisher -} - -final class DefaultPostChatDetailUseCase: PostChatDetailUseCase { - - private let chatRepository: ChatRepository - - init(chatRepository: ChatRepository) { - self.chatRepository = chatRepository - } - - func execute(articleId: Int, chatRoomId: Int, message: String, isImage: Bool) -> AnyPublisher { - let request = PostChatDetailRequest(userNickname: UserDataManager.shared.nickname, content: message, isImage: isImage) - return chatRepository.postChatDetail(articleId: articleId, chatRoomId: chatRoomId, request: request) - } -} diff --git a/Koin/Domain/UseCase/Chat/BlockUserUseCase.swift b/Koin/Domain/UseCase/LostItem/LostItemBlockUserUseCase.swift similarity index 62% rename from Koin/Domain/UseCase/Chat/BlockUserUseCase.swift rename to Koin/Domain/UseCase/LostItem/LostItemBlockUserUseCase.swift index 9eae0303..e5f16862 100644 --- a/Koin/Domain/UseCase/Chat/BlockUserUseCase.swift +++ b/Koin/Domain/UseCase/LostItem/LostItemBlockUserUseCase.swift @@ -1,5 +1,5 @@ // -// BlockUserUseCase.swift +// LostItemBlockUserUseCase.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,14 +7,14 @@ import Combine -protocol BlockUserUseCase { +protocol LostItemBlockUserUseCase { func execute(articleId: Int, chatRoomId: Int) -> AnyPublisher } -final class DefaultBlockUserUseCase: BlockUserUseCase { +final class DefaultLostItemBlockUserUseCase: LostItemBlockUserUseCase { - private let chatRepository: ChatRepository + private let chatRepository: LostItemRepository - init(chatRepository: ChatRepository) { + init(chatRepository: LostItemRepository) { self.chatRepository = chatRepository } diff --git a/Koin/Domain/UseCase/LostItem/LostItemCreateChatRoomUseCase.swift b/Koin/Domain/UseCase/LostItem/LostItemCreateChatRoomUseCase.swift new file mode 100644 index 00000000..ddb4ab2d --- /dev/null +++ b/Koin/Domain/UseCase/LostItem/LostItemCreateChatRoomUseCase.swift @@ -0,0 +1,24 @@ +// +// LostItemCreateChatRoomUseCase.swift +// koin +// +// Created by 김나훈 on 2/18/25. +// + +import Combine + +protocol LostItemCreateChatRoomUseCase { + func execute(articleId: Int) -> AnyPublisher +} +final class DefaultLostItemCreateChatRoomUseCase: LostItemCreateChatRoomUseCase { + + private let chatRepository: LostItemRepository + + init(chatRepository: LostItemRepository) { + self.chatRepository = chatRepository + } + + func execute(articleId: Int) -> AnyPublisher { + return chatRepository.createChatRoom(articleId: articleId) + } +} diff --git a/Koin/Domain/UseCase/Chat/FetchChatDetailUseCase.swift b/Koin/Domain/UseCase/LostItem/LostItemFetchChatDetailUseCase.swift similarity index 57% rename from Koin/Domain/UseCase/Chat/FetchChatDetailUseCase.swift rename to Koin/Domain/UseCase/LostItem/LostItemFetchChatDetailUseCase.swift index 76732d9f..30dc230f 100644 --- a/Koin/Domain/UseCase/Chat/FetchChatDetailUseCase.swift +++ b/Koin/Domain/UseCase/LostItem/LostItemFetchChatDetailUseCase.swift @@ -1,5 +1,5 @@ // -// FetchChatDetailUseCase.swift +// LostItemFetchChatDetailUseCase.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,18 +7,18 @@ import Combine -protocol FetchChatDetailUseCase { - func execute(userId: Int, articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatMessage], ErrorResponse> +protocol LostItemFetchChatDetailUseCase { + func execute(userId: Int, articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatMessage], ErrorResponse> } -final class DefaultFetchChatDetailUseCase: FetchChatDetailUseCase { +final class DefaultLostItemFetchChatDetailUseCase: LostItemFetchChatDetailUseCase { - private let chatRepository: ChatRepository + private let chatRepository: LostItemRepository - init(chatRepository: ChatRepository) { + init(chatRepository: LostItemRepository) { self.chatRepository = chatRepository } - func execute(userId: Int, articleId: Int, chatRoomId: Int) -> AnyPublisher<[ChatMessage], ErrorResponse> { + func execute(userId: Int, articleId: Int, chatRoomId: Int) -> AnyPublisher<[LostItemChatMessage], ErrorResponse> { return chatRepository.fetchChatDetail(articleId: articleId, chatRoomId: chatRoomId) .map { dtos in dtos.map { $0.toDomain(currentUserId: userId) } diff --git a/Koin/Domain/UseCase/LostItem/LostItemFetchChatRoomUseCase.swift b/Koin/Domain/UseCase/LostItem/LostItemFetchChatRoomUseCase.swift new file mode 100644 index 00000000..77dd1e0a --- /dev/null +++ b/Koin/Domain/UseCase/LostItem/LostItemFetchChatRoomUseCase.swift @@ -0,0 +1,26 @@ +// +// LostItemFetchChatRoomUseCase.swift +// koin +// +// Created by 김나훈 on 2/18/25. +// + +import Combine + +protocol LostItemFetchChatRoomUseCase { + func execute() -> AnyPublisher<[LostItemChatRoomItem], ErrorResponse> +} + +final class DefaultLostItemFetchChatRoomUseCase: LostItemFetchChatRoomUseCase { + private let chatRepository: LostItemRepository + + init(chatRepository: LostItemRepository) { + self.chatRepository = chatRepository + } + + func execute() -> AnyPublisher<[LostItemChatRoomItem], ErrorResponse> { + return chatRepository.fetchChatRoom() + .map { $0.map { $0.toDomain() } } + .eraseToAnyPublisher() + } +} diff --git a/Koin/Domain/UseCase/LostItem/LostItemPostChatDetailUseCase.swift b/Koin/Domain/UseCase/LostItem/LostItemPostChatDetailUseCase.swift new file mode 100644 index 00000000..5264051a --- /dev/null +++ b/Koin/Domain/UseCase/LostItem/LostItemPostChatDetailUseCase.swift @@ -0,0 +1,27 @@ +// +// LostItemPostChatDetailUseCase.swift +// koin +// +// Created by 홍기정 on 1/28/26. +// + +import Foundation +import Combine + +protocol LostItemPostChatDetailUseCase { + func execute(articleId: Int, chatRoomId: Int, message: String, isImage: Bool) -> AnyPublisher +} + +final class DefaultLostItemPostChatDetailUseCase: LostItemPostChatDetailUseCase { + + private let chatRepository: LostItemRepository + + init(chatRepository: LostItemRepository) { + self.chatRepository = chatRepository + } + + func execute(articleId: Int, chatRoomId: Int, message: String, isImage: Bool) -> AnyPublisher { + let request = LostItemPostChatDetailRequest(userNickname: UserDataManager.shared.nickname, content: message, isImage: isImage) + return chatRepository.postChatDetail(articleId: articleId, chatRoomId: chatRoomId, request: request) + } +} diff --git a/Koin/Presentation/Home/Category/CategoryHostingController.swift b/Koin/Presentation/Home/Category/CategoryHostingController.swift index 5f13f4cd..1a39c77a 100644 --- a/Koin/Presentation/Home/Category/CategoryHostingController.swift +++ b/Koin/Presentation/Home/Category/CategoryHostingController.swift @@ -186,7 +186,7 @@ extension CategoryHostingController { } private func makeChatListViewController() -> UIViewController { - return ChatListTableViewController(viewModel: ChatListTableViewModel()) + return LostItemChatListTableViewController(viewModel: LostItemChatListTableViewModel()) } private func makeLandViewController() -> UIViewController { diff --git a/Koin/Presentation/Home/Home/HomeHostingController.swift b/Koin/Presentation/Home/Home/HomeHostingController.swift index 4ecac938..a9e36210 100644 --- a/Koin/Presentation/Home/Home/HomeHostingController.swift +++ b/Koin/Presentation/Home/Home/HomeHostingController.swift @@ -243,7 +243,7 @@ extension HomeHostingController { return } navigationController?.pushViewController( - ChatListTableViewController(viewModel: ChatListTableViewModel()), + LostItemChatListTableViewController(viewModel: LostItemChatListTableViewModel()), animated: true ) case "lostitem": diff --git a/Koin/Presentation/Home/Notification/NotificationViewController.swift b/Koin/Presentation/Home/Notification/NotificationViewController.swift index 9e48cf44..135430e8 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewController.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewController.swift @@ -195,8 +195,8 @@ extension NotificationViewController { } private func navigateToChat(articleId: Int, chatRoomId: Int) { - let viewModel = ChatViewModel(articleId: articleId, chatRoomId: chatRoomId, articleTitle: nil) - let viewController = ChatViewController(viewModel: viewModel) + let viewModel = LostItemChatViewModel(articleId: articleId, chatRoomId: chatRoomId, articleTitle: nil) + let viewController = LostItemChatViewController(viewModel: viewModel) navigationController?.pushViewController(viewController, animated: true) } @@ -213,13 +213,13 @@ extension NotificationViewController { private func navigateToLostItemData(lostItemId: Int) { let userRepository = DefaultUserRepository(service: DefaultUserService()) let lostItemRepository = DefaultLostItemRepository(service: DefaultLostItemService()) - let chatRepository = DefaultChatRepository(service: DefaultChatService()) + let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: userRepository) let fetchLostItemDataUseCase = DefaultFetchLostItemDataUseCase(repository: lostItemRepository) let fetchLostItemListUseCase = DefaultFetchLostItemListUseCase(repository: lostItemRepository) let changeLostItemStateUseCase = DefaultChangeLostItemStateUseCase(repository: lostItemRepository) let deleteLostItemUseCase = DefaultDeleteLostItemUseCase(repository: lostItemRepository) - let createChatRoomUseCase = DefaultCreateChatRoomUseCase(chatRepository: chatRepository) + let createChatRoomUseCase = DefaultLostItemCreateChatRoomUseCase(chatRepository: chatRepository) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) let viewModel = LostItemDataViewModel( checkLoginUseCase: checkLoginUseCase, diff --git a/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemBlockCheckModalViewController.swift similarity index 93% rename from Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemBlockCheckModalViewController.swift index 927726fd..9483bf70 100644 --- a/Koin/Presentation/Chat/Chat/BlockCheckModalViewController.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemBlockCheckModalViewController.swift @@ -1,5 +1,5 @@ // -// BlockCheckModalViewController.swift +// LostItemBlockCheckModalViewController.swift // koin // // Created by 김나훈 on 2/18/25. @@ -7,7 +7,7 @@ import UIKit -final class BlockCheckModalViewController: UIViewController { +final class LostItemBlockCheckModalViewController: UIViewController { private let onBlockButtonTapped: () -> Void private let blockButton = UIButton().then { @@ -61,7 +61,7 @@ final class BlockCheckModalViewController: UIViewController { } -extension BlockCheckModalViewController { +extension LostItemBlockCheckModalViewController { private func setUpLayOuts() { [blockButton].forEach { diff --git a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatDateHeaderView.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatDateHeaderView.swift similarity index 84% rename from Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatDateHeaderView.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatDateHeaderView.swift index dd1271c9..5426efcf 100644 --- a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatDateHeaderView.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatDateHeaderView.swift @@ -1,5 +1,5 @@ // -// ChatDateHeaderView.swift +// LostItemChatDateHeaderView.swift // koin // // Created by 김나훈 on 2/20/25. @@ -7,7 +7,7 @@ import UIKit -final class ChatDateHeaderView: UITableViewHeaderFooterView { +final class LostItemChatDateHeaderView: UITableViewHeaderFooterView { // MARK: - UI Components @@ -34,13 +34,13 @@ final class ChatDateHeaderView: UITableViewHeaderFooterView { } -extension ChatDateHeaderView { - func configure(date: ChatDateInfo) { +extension LostItemChatDateHeaderView { + func configure(date: LostItemChatDateInfo) { dateLabel.text = "\(date.year)년 \(date.month)월 \(date.day)일" } } -extension ChatDateHeaderView { +extension LostItemChatDateHeaderView { private func setUpLayouts() { contentView.addSubview(dateLabel) diff --git a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatHistoryTableView.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatHistoryTableView.swift similarity index 77% rename from Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatHistoryTableView.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatHistoryTableView.swift index 24b2e17d..e762d84c 100644 --- a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatHistoryTableView.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatHistoryTableView.swift @@ -1,5 +1,5 @@ // -// ChatHistoryTableView.swift +// LostItemChatHistoryTableView.swift // koin // // Created by 김나훈 on 2/20/25. @@ -8,10 +8,10 @@ import Combine import UIKit -final class ChatHistoryTableView: UITableView { +final class LostItemChatHistoryTableView: UITableView { // MARK: - Properties - private var chatSections: [(date: ChatDateInfo, messages: [ChatMessage])] = [] + private var chatSections: [(date: LostItemChatDateInfo, messages: [LostItemChatMessage])] = [] let imageTapPublisher = PassthroughSubject() // MARK: - Initialization @@ -30,9 +30,9 @@ final class ChatHistoryTableView: UITableView { dataSource = self sectionHeaderTopPadding = 0 separatorStyle = .none - register(ChatImageTableViewCell.self, forCellReuseIdentifier: "ChatImageTableViewCell") - register(ChatTextTableViewCell.self, forCellReuseIdentifier: ChatTextTableViewCell.identifier) - register(ChatDateHeaderView.self, forHeaderFooterViewReuseIdentifier: ChatDateHeaderView.identifier) + register(LostItemChatImageTableViewCell.self, forCellReuseIdentifier: LostItemChatImageTableViewCell.identifier) + register(LostItemChatTextTableViewCell.self, forCellReuseIdentifier: LostItemChatTextTableViewCell.identifier) + register(LostItemChatDateHeaderView.self, forHeaderFooterViewReuseIdentifier: LostItemChatDateHeaderView.identifier) } override func layoutSubviews() { super.layoutSubviews() @@ -47,14 +47,14 @@ final class ChatHistoryTableView: UITableView { } // MARK: - 데이터 세팅 - func setChatHistory(item: [ChatMessage]) { + func setChatHistory(item: [LostItemChatMessage]) { chatSections = groupMessagesByDate(messages: item) reloadData() scrollToBottom(animated: false) } - private func groupMessagesByDate(messages: [ChatMessage]) -> [(date: ChatDateInfo, messages: [ChatMessage])] { - var groupedMessages: [(date: ChatDateInfo, messages: [ChatMessage])] = [] + private func groupMessagesByDate(messages: [LostItemChatMessage]) -> [(date: LostItemChatDateInfo, messages: [LostItemChatMessage])] { + var groupedMessages: [(date: LostItemChatDateInfo, messages: [LostItemChatMessage])] = [] for message in messages { if let lastSection = groupedMessages.last, lastSection.date.day == message.chatDateInfo.day { @@ -69,7 +69,7 @@ final class ChatHistoryTableView: UITableView { return groupedMessages } - func appendNewMessage(_ message: ChatMessage) { + func appendNewMessage(_ message: LostItemChatMessage) { if let lastSection = chatSections.last, lastSection.date.day == message.chatDateInfo.day { // 같은 날짜(day)면 기존 섹션에 메시지 추가 chatSections[chatSections.count - 1].messages.append(message) @@ -88,7 +88,7 @@ final class ChatHistoryTableView: UITableView { } // MARK: - UITableViewDataSource -extension ChatHistoryTableView: UITableViewDataSource { +extension LostItemChatHistoryTableView: UITableViewDataSource { private func scrollToBottom(animated: Bool) { guard !chatSections.isEmpty else { return } @@ -115,7 +115,7 @@ extension ChatHistoryTableView: UITableViewDataSource { let message = chatSections[indexPath.section].messages[indexPath.row] if message.isImage { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "ChatImageTableViewCell", for: indexPath) as? ChatImageTableViewCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: LostItemChatImageTableViewCell.identifier, for: indexPath) as? LostItemChatImageTableViewCell else { return UITableViewCell() } cell.configure(message: message) @@ -124,7 +124,7 @@ extension ChatHistoryTableView: UITableViewDataSource { }.store(in: &cell.cancellables) return cell } else { - guard let cell = tableView.dequeueReusableCell(withIdentifier: ChatTextTableViewCell.identifier, for: indexPath) as? ChatTextTableViewCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: LostItemChatTextTableViewCell.identifier, for: indexPath) as? LostItemChatTextTableViewCell else { return UITableViewCell() } cell.configure(message: message) @@ -134,10 +134,10 @@ extension ChatHistoryTableView: UITableViewDataSource { } // MARK: - UITableViewDelegate (헤더) -extension ChatHistoryTableView: UITableViewDelegate { +extension LostItemChatHistoryTableView: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: ChatDateHeaderView.identifier) as? ChatDateHeaderView else { + guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: LostItemChatDateHeaderView.identifier) as? LostItemChatDateHeaderView else { return nil } header.configure(date: chatSections[section].date) diff --git a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatImageTableViewCell.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatImageTableViewCell.swift similarity index 91% rename from Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatImageTableViewCell.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatImageTableViewCell.swift index 5a838617..bfdaf8eb 100644 --- a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatImageTableViewCell.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatImageTableViewCell.swift @@ -1,5 +1,5 @@ // -// ChatImageTableViewCell.swift +// LostItemChatImageTableViewCell.swift // koin // // Created by 김나훈 on 2/20/25. @@ -8,7 +8,7 @@ import Combine import UIKit -final class ChatImageTableViewCell: UITableViewCell { +final class LostItemChatImageTableViewCell: UITableViewCell { // MARK: - Properties private var imageUrl: String? @@ -43,7 +43,7 @@ final class ChatImageTableViewCell: UITableViewCell { configureView() } - func configure(message: ChatMessage) { + func configure(message: LostItemChatMessage) { imageUrl = message.content textImageView.loadImageWithSpinner(from: message.content) timestampLabel.text = String(format: "%02d:%02d", message.chatDateInfo.hour, message.chatDateInfo.minute) @@ -60,14 +60,14 @@ final class ChatImageTableViewCell: UITableViewCell { } } -extension ChatImageTableViewCell { +extension LostItemChatImageTableViewCell { private func setUpLayouts() { [textImageView, timestampLabel].forEach { contentView.addSubview($0) } } - private func setUpConstraints(message: ChatMessage) { + private func setUpConstraints(message: LostItemChatMessage) { textImageView.snp.remakeConstraints { if message.isMine { $0.trailing.equalToSuperview().offset(-16) diff --git a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatTextTableViewCell.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatTextTableViewCell.swift similarity index 90% rename from Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatTextTableViewCell.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatTextTableViewCell.swift index e034f1db..d05c152e 100644 --- a/Koin/Presentation/Chat/Chat/ChatHistoryTableView/ChatTextTableViewCell.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatHistoryTableView/LostItemChatTextTableViewCell.swift @@ -1,5 +1,5 @@ // -// ChatTextTableViewCell.swift +// LostItemChatTextTableViewCell.swift // koin // // Created by 김나훈 on 2/20/25. @@ -7,7 +7,7 @@ import UIKit -final class ChatTextTableViewCell: UITableViewCell { +final class LostItemChatTextTableViewCell: UITableViewCell { // MARK: - UI Components private let messageLabel = InsetLabel(top: 10, left: 12, bottom: 10, right: 12).then { @@ -37,7 +37,7 @@ final class ChatTextTableViewCell: UITableViewCell { configureView() } - func configure(message: ChatMessage) { + func configure(message: LostItemChatMessage) { messageLabel.text = message.content messageLabel.backgroundColor = message.isMine ? UIColor.appColor(.neutral100) : UIColor.appColor(.info100) timestampLabel.text = String(format: "%02d:%02d", message.chatDateInfo.hour, message.chatDateInfo.minute) @@ -46,14 +46,14 @@ final class ChatTextTableViewCell: UITableViewCell { } -extension ChatTextTableViewCell { +extension LostItemChatTextTableViewCell { private func setUpLayouts() { [messageLabel, timestampLabel].forEach { contentView.addSubview($0) } } - private func setUpConstraints(message: ChatMessage) { + private func setUpConstraints(message: LostItemChatMessage) { messageLabel.snp.remakeConstraints { if message.isMine { $0.trailing.equalToSuperview().offset(-16) diff --git a/Koin/Presentation/Chat/Chat/ChatViewController.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift similarity index 94% rename from Koin/Presentation/Chat/Chat/ChatViewController.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift index b30a46f5..14f1e93c 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewController.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift @@ -1,5 +1,5 @@ // -// ChatViewController.swift +// LostItemChatViewController.swift // koin // // Created by 김나훈 on 2/16/25. @@ -9,11 +9,11 @@ import Combine import PhotosUI import UIKit -final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerViewControllerDelegate { +final class LostItemChatViewController: UIViewController, UITextViewDelegate, PHPickerViewControllerDelegate { // // MARK: - Properties - private let viewModel: ChatViewModel - private let inputSubject: PassthroughSubject = .init() + private let viewModel: LostItemChatViewModel + private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] private var messageInputBottomConstraint: NSLayoutConstraint! private var textViewHeightConstraint: NSLayoutConstraint! @@ -44,11 +44,11 @@ final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerVi $0.setImage(UIImage.appImage(asset: .send), for: .normal) } - private let chatHistoryTableView = ChatHistoryTableView().then { + private let chatHistoryTableView = LostItemChatHistoryTableView().then { $0.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) } - init(viewModel: ChatViewModel) { + init(viewModel: LostItemChatViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) let rightButton = UIBarButtonItem(image: UIImage.appImage(asset: .threeCircle), style: .plain, target: self, action: #selector(rightButtonTapped)) @@ -116,7 +116,7 @@ final class ChatViewController: UIViewController, UITextViewDelegate, PHPickerVi } } -extension ChatViewController{ +extension LostItemChatViewController{ @objc private func sendButtonTapped() { if textView.text.isEmpty || textView.textColor == .appColor(.neutral500) { return } @@ -164,7 +164,7 @@ extension ChatViewController{ @objc private func rightButtonTapped() { dismissKeyboard() - let blockCheckModalViewController = BlockCheckModalViewController(onBlockButtonTapped: { [weak self] in + let blockCheckModalViewController = LostItemBlockCheckModalViewController(onBlockButtonTapped: { [weak self] in self?.presentBlockUserConfirmationModal() }) blockCheckModalViewController.modalPresentationStyle = .overFullScreen @@ -252,7 +252,7 @@ extension ChatViewController{ } } -extension ChatViewController { +extension LostItemChatViewController { private func setUpLayOuts() { [bottomBackgroundView, chatHistoryTableView, messageInputView].forEach { diff --git a/Koin/Presentation/Chat/Chat/ChatViewModel.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewModel.swift similarity index 85% rename from Koin/Presentation/Chat/Chat/ChatViewModel.swift rename to Koin/Presentation/LostItem/LostItemChat/LostItemChatViewModel.swift index 82b61e6c..3e4a0b57 100644 --- a/Koin/Presentation/Chat/Chat/ChatViewModel.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewModel.swift @@ -1,5 +1,5 @@ // -// ChatViewModel.swift +// LostItemChatViewModel.swift // koin // // Created by 김나훈 on 2/16/25. @@ -8,7 +8,7 @@ import Combine import Foundation -final class ChatViewModel: ViewModelProtocol { +final class LostItemChatViewModel: ViewModelProtocol { // MARK: - Input @@ -25,7 +25,7 @@ final class ChatViewModel: ViewModelProtocol { enum Output { case updateTitle(String) - case showChatHistory([ChatMessage]) + case showChatHistory([LostItemChatMessage]) case showToast(String, Bool) } @@ -33,13 +33,13 @@ final class ChatViewModel: ViewModelProtocol { private let outputSubject = PassthroughSubject() private var subscriptions: Set = [] private var pollingSubscriptions: AnyCancellable? - private let chatRepository = DefaultChatRepository(service: DefaultChatService()) - private lazy var fetchChatDetailUseCase = DefaultFetchChatDetailUseCase(chatRepository: chatRepository) - private lazy var blockUserUserCase = DefaultBlockUserUseCase(chatRepository: chatRepository) - private lazy var postChatDetailUseCase = DefaultPostChatDetailUseCase(chatRepository: chatRepository) + private let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) + private lazy var fetchChatDetailUseCase = DefaultLostItemFetchChatDetailUseCase(chatRepository: chatRepository) + private lazy var blockUserUserCase = DefaultLostItemBlockUserUseCase(chatRepository: chatRepository) + private lazy var postChatDetailUseCase = DefaultLostItemPostChatDetailUseCase(chatRepository: chatRepository) private let fetchUserDataUseCase = DefaultFetchUserDataUseCase(userRepository: DefaultUserRepository(service: DefaultUserService())) private lazy var uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: DefaultCoreRepository(service: DefaultCoreService())) - private lazy var fetchChatRoomUseCase = DefaultFetchChatRoomUseCase(chatRepository: chatRepository) + private lazy var fetchChatRoomUseCase = DefaultLostItemFetchChatRoomUseCase(chatRepository: chatRepository) let articleId: Int let chatRoomId: Int private var articleTitle: String? @@ -76,7 +76,7 @@ final class ChatViewModel: ViewModelProtocol { } -extension ChatViewModel { +extension LostItemChatViewModel { private func uploadFiles(files: [Data]) { uploadFileUseCase.execute(files: files, domain: .lostItem).sink { [weak self] completion in @@ -106,10 +106,10 @@ extension ChatViewModel { pollingSubscriptions = Timer.publish(every: 1, on: .main, in: .common) .autoconnect() .prepend(Date()) - .flatMap { [weak self] _ -> AnyPublisher<[ChatMessage], Never> in + .flatMap { [weak self] _ -> AnyPublisher<[LostItemChatMessage], Never> in guard let self else { return Empty().eraseToAnyPublisher() } return fetchChatDetailUseCase.execute(userId: UserDataManager.shared.id, articleId: articleId, chatRoomId: chatRoomId) - .catch { error -> AnyPublisher<[ChatMessage], Never> in + .catch { error -> AnyPublisher<[LostItemChatMessage], Never> in return Empty().eraseToAnyPublisher() } .eraseToAnyPublisher() diff --git a/Koin/Presentation/Chat/ChatList/ChatListTableViewController.swift b/Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewController.swift similarity index 89% rename from Koin/Presentation/Chat/ChatList/ChatListTableViewController.swift rename to Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewController.swift index 5f0d88f2..e26279f8 100644 --- a/Koin/Presentation/Chat/ChatList/ChatListTableViewController.swift +++ b/Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewController.swift @@ -1,5 +1,5 @@ // -// ChatListTableViewController.swift +// LostItemChatListTableViewController.swift // koin // // Created by 김나훈 on 2/18/25. @@ -8,17 +8,17 @@ import Combine import UIKit -final class ChatListTableViewController: UITableViewController { +final class LostItemChatListTableViewController: UITableViewController { // MARK: - Properties - private let viewModel: ChatListTableViewModel - private let inputSubject: PassthroughSubject = .init() + private let viewModel: LostItemChatListTableViewModel + private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] // MARK: - UI Components - init(viewModel: ChatListTableViewModel) { + init(viewModel: LostItemChatListTableViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) navigationItem.title = "쪽지" @@ -35,7 +35,7 @@ final class ChatListTableViewController: UITableViewController { super.viewDidLoad() configureView() bind() - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ChatCell") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.identifier) tableView.separatorStyle = .none } @@ -65,11 +65,11 @@ final class ChatListTableViewController: UITableViewController { } } -extension ChatListTableViewController { +extension LostItemChatListTableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let chat = viewModel.chatList[indexPath.row] - let viewController = ChatViewController(viewModel: ChatViewModel(articleId: chat.articleId, chatRoomId: chat.chatRoomId, articleTitle: chat.articleTitle)) + let viewController = LostItemChatViewController(viewModel: LostItemChatViewModel(articleId: chat.articleId, chatRoomId: chat.chatRoomId, articleTitle: chat.articleTitle)) inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.messageListSelect, .click, "쪽지")) navigationController?.pushViewController(viewController, animated: true) } @@ -86,7 +86,7 @@ extension ChatListTableViewController { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath) + let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.identifier, for: indexPath) let chat = viewModel.chatList[indexPath.row] cell.contentView.subviews.forEach { $0.removeFromSuperview() } let thumbnailContainerView = UIView().then { @@ -138,7 +138,7 @@ extension ChatListTableViewController { } titleLabel.text = chat.articleTitle contentLabel.text = "\(chat.recentMessageContent)" - recentTimeLabel.text = chat.lastMessageAt.toChatDateInfo().showingText + recentTimeLabel.text = chat.lastMessageAt.toLostItemChatDateInfo().showingText unreadMessageLabel.text = String(chat.unreadMessageCount) unreadMessageLabel.isHidden = chat.unreadMessageCount == 0 @@ -185,7 +185,7 @@ extension ChatListTableViewController { } -extension ChatListTableViewController { +extension LostItemChatListTableViewController { private func setUpLayOuts() { [].forEach { diff --git a/Koin/Presentation/Chat/ChatList/ChatListTableViewModel.swift b/Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewModel.swift similarity index 79% rename from Koin/Presentation/Chat/ChatList/ChatListTableViewModel.swift rename to Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewModel.swift index 571a5995..3aa4f667 100644 --- a/Koin/Presentation/Chat/ChatList/ChatListTableViewModel.swift +++ b/Koin/Presentation/LostItem/LostItemChatList/LostItemChatListTableViewModel.swift @@ -1,5 +1,5 @@ // -// ChatListTableViewModel.swift +// LostItemChatListTableViewModel.swift // koin // // Created by 김나훈 on 2/18/25. @@ -8,7 +8,7 @@ import Combine import Foundation -final class ChatListTableViewModel: ViewModelProtocol { +final class LostItemChatListTableViewModel: ViewModelProtocol { // MARK: - Input @@ -28,19 +28,18 @@ final class ChatListTableViewModel: ViewModelProtocol { private let outputSubject = PassthroughSubject() private var subscriptions: Set = [] private var pollingSubscriptions: AnyCancellable? - private(set) var chatList: [ChatRoomItem] = [] { + private(set) var chatList: [LostItemChatRoomItem] = [] { didSet { outputSubject.send(.showChatRoom) } } - private let chatRepository = DefaultChatRepository(service: DefaultChatService()) - private lazy var fetchChatRoomUseCase = DefaultFetchChatRoomUseCase(chatRepository: chatRepository) + private let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) + private lazy var fetchChatRoomUseCase = DefaultLostItemFetchChatRoomUseCase(chatRepository: chatRepository) private let logAnalyticsEventUseCase: LogAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) // MARK: - Initialization - init() { - } + init() {} func transform(with input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] input in @@ -60,17 +59,17 @@ final class ChatListTableViewModel: ViewModelProtocol { } -extension ChatListTableViewModel { +extension LostItemChatListTableViewModel { private func fetchChatRooms() { pollingSubscriptions?.cancel() pollingSubscriptions = Timer.publish(every: 1, on: .main, in: .common) .autoconnect() .prepend(Date()) - .flatMap { [weak self] _ -> AnyPublisher<[ChatRoomItem], Never> in + .flatMap { [weak self] _ -> AnyPublisher<[LostItemChatRoomItem], Never> in guard let self else { return Empty().eraseToAnyPublisher() } return fetchChatRoomUseCase.execute() - .catch { error -> AnyPublisher<[ChatRoomItem], Never> in + .catch { error -> AnyPublisher<[LostItemChatRoomItem], Never> in return Empty().eraseToAnyPublisher() }.eraseToAnyPublisher() } diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift index b31edbc7..4f28859e 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewController.swift @@ -101,12 +101,12 @@ final class LostItemDataViewController: UIViewController { lostItemDataTableView.cellTappedPublisher.sink { [weak self] id in let userRepository = DefaultUserRepository(service: DefaultUserService()) let lostItemRepository = DefaultLostItemRepository(service: DefaultLostItemService()) - let chatRepository = DefaultChatRepository(service: DefaultChatService()) + let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: userRepository) let fetchLostItemDataUseCase = DefaultFetchLostItemDataUseCase(repository: lostItemRepository) let fetchLostItemListUseCase = DefaultFetchLostItemListUseCase(repository: lostItemRepository) let changeLostItemStateUseCase = DefaultChangeLostItemStateUseCase(repository: lostItemRepository) - let createChatRoomUseCase = DefaultCreateChatRoomUseCase(chatRepository: chatRepository) + let createChatRoomUseCase = DefaultLostItemCreateChatRoomUseCase(chatRepository: chatRepository) let deleteLostItemUseCase = DefaultDeleteLostItemUseCase(repository: lostItemRepository) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) let viewModel = LostItemDataViewModel( @@ -289,13 +289,13 @@ extension LostItemDataViewController { navigationController?.present(modalViewController, animated: true) } - private func navigateToChat(_ createChatRoomResponse: CreateChatRoomResponse) { - let chatViewModel = ChatViewModel( + private func navigateToChat(_ createChatRoomResponse: LostItemCreateChatRoomResponse) { + let chatViewModel = LostItemChatViewModel( articleId: createChatRoomResponse.articleId, chatRoomId: createChatRoomResponse.chatRoomId, articleTitle: createChatRoomResponse.articleTitle ) - let viewController = ChatViewController(viewModel: chatViewModel) + let viewController = LostItemChatViewController(viewModel: chatViewModel) navigationController?.pushViewController(viewController, animated: true) } diff --git a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewModel.swift b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewModel.swift index cbcc7cc2..12a69ea8 100644 --- a/Koin/Presentation/LostItem/LostItemData/LostItemDataViewModel.swift +++ b/Koin/Presentation/LostItem/LostItemData/LostItemDataViewModel.swift @@ -28,7 +28,7 @@ final class LostItemDataViewModel: ViewModelProtocol { case deletedData(Int) case popViewController case checkedLogin((CheckLoginOption, Bool)) - case navigateToChat(CreateChatRoomResponse) + case navigateToChat(LostItemCreateChatRoomResponse) } enum CheckLoginOption { @@ -42,7 +42,7 @@ final class LostItemDataViewModel: ViewModelProtocol { private let fetchLostItemListUseCase: FetchLostItemListUseCase private let changeLostItemStateUseCase: ChangeLostItemStateUseCase private let deleteLostItemUseCase: DeleteLostItemUseCase - private let createChatRoomUseCase: CreateChatRoomUseCase + private let createChatRoomUseCase: LostItemCreateChatRoomUseCase private let logAnalyticsEventUseCase: LogAnalyticsEventUseCase var type: LostItemType? private let outputSubject = PassthroughSubject() @@ -57,7 +57,7 @@ final class LostItemDataViewModel: ViewModelProtocol { fetchLostItemListUseCase: FetchLostItemListUseCase, changeLostItemStateUseCase: ChangeLostItemStateUseCase, deleteLostItemUseCase: DeleteLostItemUseCase, - createChatRoomUseCase: CreateChatRoomUseCase, + createChatRoomUseCase: LostItemCreateChatRoomUseCase, logAnalyticsEventUseCase: LogAnalyticsEventUseCase, id: Int) { self.checkLoginUseCase = checkLoginUseCase diff --git a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift index 72979769..99e09adc 100644 --- a/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift +++ b/Koin/Presentation/LostItem/LostItemList/LostItemListViewController.swift @@ -125,13 +125,13 @@ final class LostItemListViewController: UIViewController { let userRepository = DefaultUserRepository(service: DefaultUserService()) let lostItemRepository = DefaultLostItemRepository(service: DefaultLostItemService()) - let chatRepository = DefaultChatRepository(service: DefaultChatService()) + let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: userRepository) let fetchLostItemDataUseCase = DefaultFetchLostItemDataUseCase(repository: lostItemRepository) let fetchLostItemListUseCase = DefaultFetchLostItemListUseCase(repository: lostItemRepository) let changeLostItemStateUseCase = DefaultChangeLostItemStateUseCase(repository: lostItemRepository) let deleteLostItemUseCase = DefaultDeleteLostItemUseCase(repository: lostItemRepository) - let createChatRoomUseCase = DefaultCreateChatRoomUseCase(chatRepository: chatRepository) + let createChatRoomUseCase = DefaultLostItemCreateChatRoomUseCase(chatRepository: chatRepository) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) let viewModel = LostItemDataViewModel( checkLoginUseCase: checkLoginUseCase, diff --git a/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift b/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift index 92b2e467..d89f1ade 100644 --- a/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift +++ b/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift @@ -244,13 +244,13 @@ extension PostLostItemViewController: UITextViewDelegate, PHPickerViewController private func navigateToLostItemData(_ id: Int) { let userRepository = DefaultUserRepository(service: DefaultUserService()) let lostItemRepository = DefaultLostItemRepository(service: DefaultLostItemService()) - let chatRepository = DefaultChatRepository(service: DefaultChatService()) + let chatRepository = DefaultLostItemRepository(service: DefaultLostItemService()) let checkLoginUseCase = DefaultCheckLoginUseCase(userRepository: userRepository) let fetchLostItemDataUseCase = DefaultFetchLostItemDataUseCase(repository: lostItemRepository) let fetchLostItemListUseCase = DefaultFetchLostItemListUseCase(repository: lostItemRepository) let changeLostItemStateUseCase = DefaultChangeLostItemStateUseCase(repository: lostItemRepository) let deleteLostItemUseCase = DefaultDeleteLostItemUseCase(repository: lostItemRepository) - let createChatRoomUseCase = DefaultCreateChatRoomUseCase(chatRepository: chatRepository) + let createChatRoomUseCase = DefaultLostItemCreateChatRoomUseCase(chatRepository: chatRepository) let logAnalyticsEventUseCase = DefaultLogAnalyticsEventUseCase(repository: GA4AnalyticsRepository(service: GA4AnalyticsService())) let viewModel = LostItemDataViewModel( checkLoginUseCase: checkLoginUseCase, diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 45f89572..b42a9960 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -409,8 +409,8 @@ 7CC42B962F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B952F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift */; }; 7CC42B982F5E812F00940CE1 /* CallVanChatLeftCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B972F5E812F00940CE1 /* CallVanChatLeftCell.swift */; }; 7CC42B9C2F5E834E00940CE1 /* CallVanChatRightCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B9B2F5E834E00940CE1 /* CallVanChatRightCell.swift */; }; - 7CCB1E4B2F29D87C00472669 /* PostChatDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4A2F29D87C00472669 /* PostChatDetailUseCase.swift */; }; - 7CCB1E4E2F29DB3200472669 /* PostChatDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4D2F29DB3200472669 /* PostChatDetailRequest.swift */; }; + 7CCB1E4B2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4A2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift */; }; + 7CCB1E4E2F29DB3200472669 /* LostItemPostChatDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4D2F29DB3200472669 /* LostItemPostChatDetailRequest.swift */; }; 7CD411EA2FEB92280079467B /* HomeTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD411E92FEB92280079467B /* HomeTabBarItem.swift */; }; 7CEB3B4B2FBFF4B700AF2B81 /* DeleteDeviceTokenUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CEB3B4A2FBFF4B700AF2B81 /* DeleteDeviceTokenUseCase.swift */; }; 7CED90BC2EF2D1F900457128 /* ShopSummaryTableViewHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FD02EF2D1F900457128 /* ShopSummaryTableViewHeaderView.swift */; }; @@ -420,10 +420,10 @@ 7CED90C32EF2D1F900457128 /* ShopSummaryPriceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FCC2EF2D1F900457128 /* ShopSummaryPriceTableViewCell.swift */; }; 7CED90C62EF2D1F900457128 /* TimetableCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED909A2EF2D1F900457128 /* TimetableCollectionViewCell.swift */; }; 7CED90CA2EF2D1F900457128 /* LandOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90B02EF2D1F900457128 /* LandOptionCollectionViewCell.swift */; }; - 7CED90CB2EF2D1F900457128 /* ChatListTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EF92EF2D1F900457128 /* ChatListTableViewModel.swift */; }; + 7CED90CB2EF2D1F900457128 /* LostItemChatListTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EF92EF2D1F900457128 /* LostItemChatListTableViewModel.swift */; }; 7CED90CE2EF2D1F900457128 /* ForceModifyUserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8ED42EF2D1F900457128 /* ForceModifyUserViewController.swift */; }; 7CED90CF2EF2D1F900457128 /* ChangeMyProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F0C2EF2D1F900457128 /* ChangeMyProfileViewModel.swift */; }; - 7CED90D02EF2D1F900457128 /* ChatHistoryTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFC2EF2D1F900457128 /* ChatHistoryTableView.swift */; }; + 7CED90D02EF2D1F900457128 /* LostItemChatHistoryTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFC2EF2D1F900457128 /* LostItemChatHistoryTableView.swift */; }; 7CED90D12EF2D1F900457128 /* NoticeSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F572EF2D1F900457128 /* NoticeSearchViewController.swift */; }; 7CED90D22EF2D1F900457128 /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90152EF2D1F900457128 /* TagCollectionViewCell.swift */; }; 7CED90D32EF2D1F900457128 /* LandCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90A92EF2D1F900457128 /* LandCollectionView.swift */; }; @@ -431,7 +431,7 @@ 7CED90D72EF2D1F900457128 /* RecommendedKeywordCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F5A2EF2D1F900457128 /* RecommendedKeywordCollectionView.swift */; }; 7CED90D92EF2D1F900457128 /* DiningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90742EF2D1F900457128 /* DiningViewController.swift */; }; 7CED90DA2EF2D1F900457128 /* RecentSearchTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F532EF2D1F900457128 /* RecentSearchTableView.swift */; }; - 7CED90DB2EF2D1F900457128 /* ChatImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFE2EF2D1F900457128 /* ChatImageTableViewCell.swift */; }; + 7CED90DB2EF2D1F900457128 /* LostItemChatImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFE2EF2D1F900457128 /* LostItemChatImageTableViewCell.swift */; }; 7CED90DC2EF2D1F900457128 /* ShopSummaryDeliveryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FC42EF2D1F900457128 /* ShopSummaryDeliveryButton.swift */; }; 7CED90DD2EF2D1F900457128 /* AddDirectCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90912EF2D1F900457128 /* AddDirectCollectionViewCell.swift */; }; 7CED90DF2EF2D1F900457128 /* LandDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90B72EF2D1F900457128 /* LandDetailViewModel.swift */; }; @@ -446,9 +446,9 @@ 7CED90F02EF2D1F900457128 /* RevokeModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F0B2EF2D1F900457128 /* RevokeModalViewController.swift */; }; 7CED90F12EF2D1F900457128 /* ForceUpdateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EDF2EF2D1F900457128 /* ForceUpdateViewModel.swift */; }; 7CED90F22EF2D1F900457128 /* ShopSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FBF2EF2D1F900457128 /* ShopSearchViewModel.swift */; }; - 7CED90F42EF2D1F900457128 /* ChatListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFA2EF2D1F900457128 /* ChatListTableViewController.swift */; }; + 7CED90F42EF2D1F900457128 /* LostItemChatListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFA2EF2D1F900457128 /* LostItemChatListTableViewController.swift */; }; 7CED90F52EF2D1F900457128 /* DiningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90732EF2D1F900457128 /* DiningViewModel.swift */; }; - 7CED90F62EF2D1F900457128 /* ChatDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFF2EF2D1F900457128 /* ChatDateHeaderView.swift */; }; + 7CED90F62EF2D1F900457128 /* LostItemChatDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFF2EF2D1F900457128 /* LostItemChatDateHeaderView.swift */; }; 7CED90F72EF2D1F900457128 /* ReviewImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90032EF2D1F900457128 /* ReviewImageCollectionViewCell.swift */; }; 7CED90FA2EF2D1F900457128 /* LandDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90B82EF2D1F900457128 /* LandDetailViewController.swift */; }; 7CED90FB2EF2D1F900457128 /* TabBarCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F3E2EF2D1F900457128 /* TabBarCollectionViewCell.swift */; }; @@ -461,7 +461,7 @@ 7CED91042EF2D1F900457128 /* TimetableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90822EF2D1F900457128 /* TimetableCell.swift */; }; 7CED91052EF2D1F900457128 /* SelectTypeFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F2B2EF2D1F900457128 /* SelectTypeFormViewController.swift */; }; 7CED91072EF2D1F900457128 /* AddDirectHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90922EF2D1F900457128 /* AddDirectHeaderView.swift */; }; - 7CED91082EF2D1F900457128 /* BlockCheckModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F012EF2D1F900457128 /* BlockCheckModalViewController.swift */; }; + 7CED91082EF2D1F900457128 /* LostItemBlockCheckModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F012EF2D1F900457128 /* LostItemBlockCheckModalViewController.swift */; }; 7CED91092EF2D1F900457128 /* FacilityInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90A42EF2D1F900457128 /* FacilityInfoViewController.swift */; }; 7CED910A2EF2D1F900457128 /* LeftAlignedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90132EF2D1F900457128 /* LeftAlignedFlowLayout.swift */; }; 7CED910B2EF2D1F900457128 /* AddClassCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90952EF2D1F900457128 /* AddClassCollectionView.swift */; }; @@ -470,7 +470,7 @@ 7CED910E2EF2D1F900457128 /* ShopSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FBE2EF2D1F900457128 /* ShopSearchViewController.swift */; }; 7CED910F2EF2D1F900457128 /* ShopCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EC32EF2D1F900457128 /* ShopCollectionView.swift */; }; 7CED91122EF2D1F900457128 /* LectureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90882EF2D1F900457128 /* LectureView.swift */; }; - 7CED91142EF2D1F900457128 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F032EF2D1F900457128 /* ChatViewController.swift */; }; + 7CED91142EF2D1F900457128 /* LostItemChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F032EF2D1F900457128 /* LostItemChatViewController.swift */; }; 7CED91152EF2D1F900457128 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F232EF2D1F900457128 /* LoginViewModel.swift */; }; 7CED91162EF2D1F900457128 /* ShopSortOptionSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FB52EF2D1F900457128 /* ShopSortOptionSheetViewController.swift */; }; 7CED91172EF2D1F900457128 /* TimetableText+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90A02EF2D1F900457128 /* TimetableText+.swift */; }; @@ -572,13 +572,13 @@ 7CED91BA2EF2D1F900457128 /* CertificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F0E2EF2D1F900457128 /* CertificationView.swift */; }; 7CED91BB2EF2D1F900457128 /* EnterFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F282EF2D1F900457128 /* EnterFormViewController.swift */; }; 7CED91BD2EF2D1F900457128 /* RecommendedSearchCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F512EF2D1F900457128 /* RecommendedSearchCollectionViewCell.swift */; }; - 7CED91C02EF2D1F900457128 /* ChatTextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFD2EF2D1F900457128 /* ChatTextTableViewCell.swift */; }; + 7CED91C02EF2D1F900457128 /* LostItemChatTextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8EFD2EF2D1F900457128 /* LostItemChatTextTableViewCell.swift */; }; 7CED91C12EF2D1F900457128 /* ShopViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FB72EF2D1F900457128 /* ShopViewModel.swift */; }; 7CED91C22EF2D1F900457128 /* ReviewImageCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90022EF2D1F900457128 /* ReviewImageCollectionView.swift */; }; 7CED91C62EF2D1F900457128 /* HotArticlesNoticeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F652EF2D1F900457128 /* HotArticlesNoticeTableViewCell.swift */; }; 7CED91C92EF2D1F900457128 /* CategoryCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FAB2EF2D1F900457128 /* CategoryCollectionView.swift */; }; 7CED91CB2EF2D1F900457128 /* ShopSummaryImagesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FC22EF2D1F900457128 /* ShopSummaryImagesCollectionViewCell.swift */; }; - 7CED91CD2EF2D1F900457128 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F022EF2D1F900457128 /* ChatViewModel.swift */; }; + 7CED91CD2EF2D1F900457128 /* LostItemChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F022EF2D1F900457128 /* LostItemChatViewModel.swift */; }; 7CED91CE2EF2D1F900457128 /* NotiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F142EF2D1F900457128 /* NotiViewModel.swift */; }; 7CED91CF2EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F622EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift */; }; 7CED91D22EF2D1F900457128 /* LandImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90B32EF2D1F900457128 /* LandImageCollectionViewCell.swift */; }; @@ -908,9 +908,9 @@ D2A8F8BE2C4A78F20090C7A4 /* CoopShopData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A8F8BD2C4A78F20090C7A4 /* CoopShopData.swift */; }; D2A8F8C02C4BCD470090C7A4 /* ShareMenuListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A8F8BF2C4BCD470090C7A4 /* ShareMenuListUseCase.swift */; }; D2A8F8C22C4BD36C0090C7A4 /* ShareService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A8F8C12C4BD36C0090C7A4 /* ShareService.swift */; }; - D2B2193B2D66626900EAF5B1 /* ChatHistoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2193A2D66626900EAF5B1 /* ChatHistoryData.swift */; }; + D2B2193B2D66626900EAF5B1 /* LostItemChatHistoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2193A2D66626900EAF5B1 /* LostItemChatHistoryData.swift */; }; D2B2193D2D66828300EAF5B1 /* WebSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2193C2D66828200EAF5B1 /* WebSocketManager.swift */; }; - D2B2193F2D66DDF900EAF5B1 /* ChatDateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2193E2D66DDF900EAF5B1 /* ChatDateInfo.swift */; }; + D2B2193F2D66DDF900EAF5B1 /* LostItemChatDateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2193E2D66DDF900EAF5B1 /* LostItemChatDateInfo.swift */; }; D2B7EBCA2D37C81B00EE46B0 /* PostLostItemRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7EBC92D37C81B00EE46B0 /* PostLostItemRequest.swift */; }; D2B7EBDA2D382C8800EE46B0 /* UserTypeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7EBD92D382C8800EE46B0 /* UserTypeResponse.swift */; }; D2B7EBDC2D382D5400EE46B0 /* CheckAuthUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7EBDB2D382D5400EE46B0 /* CheckAuthUseCase.swift */; }; @@ -931,18 +931,14 @@ D2D422722BB2A7BE00A8AF04 /* Confirmable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D422712BB2A7BE00A8AF04 /* Confirmable.swift */; }; D2D422752BB2AB3A00A8AF04 /* PasswordConfirmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D422742BB2AB3A00A8AF04 /* PasswordConfirmer.swift */; }; D2D422772BB2EA3900A8AF04 /* FindPasswordRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D422762BB2EA3900A8AF04 /* FindPasswordRequest.swift */; }; - D2D462602D63A81500C60864 /* ChatRoomDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4625F2D63A81500C60864 /* ChatRoomDto.swift */; }; - D2D462622D63B90100C60864 /* ChatRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462612D63B90100C60864 /* ChatRepository.swift */; }; - D2D462662D63B98F00C60864 /* FetchChatRoomUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462652D63B98F00C60864 /* FetchChatRoomUseCase.swift */; }; - D2D4626A2D63BA5200C60864 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462692D63BA5200C60864 /* ChatService.swift */; }; - D2D4626C2D63BAC600C60864 /* ChatAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4626B2D63BAC600C60864 /* ChatAPI.swift */; }; - D2D4626E2D63BD1400C60864 /* DefaultChatRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4626D2D63BD1400C60864 /* DefaultChatRepository.swift */; }; - D2D462712D63C5ED00C60864 /* ChatRoomItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462702D63C5ED00C60864 /* ChatRoomItem.swift */; }; - D2D462732D63CACC00C60864 /* ChatDetailDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462722D63CACC00C60864 /* ChatDetailDto.swift */; }; - D2D462752D63CB3E00C60864 /* FetchChatDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462742D63CB3E00C60864 /* FetchChatDetailUseCase.swift */; }; - D2D4627A2D63D4D400C60864 /* BlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462792D63D4D400C60864 /* BlockUserUseCase.swift */; }; - D2D4627C2D6482AA00C60864 /* CreateChatRoomResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4627B2D6482AA00C60864 /* CreateChatRoomResponse.swift */; }; - D2D4627E2D64835B00C60864 /* CreateChatRoomUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4627D2D64835A00C60864 /* CreateChatRoomUseCase.swift */; }; + D2D462602D63A81500C60864 /* LostItemChatRoomDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4625F2D63A81500C60864 /* LostItemChatRoomDto.swift */; }; + D2D462662D63B98F00C60864 /* LostItemFetchChatRoomUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462652D63B98F00C60864 /* LostItemFetchChatRoomUseCase.swift */; }; + D2D462712D63C5ED00C60864 /* LostItemChatRoomItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462702D63C5ED00C60864 /* LostItemChatRoomItem.swift */; }; + D2D462732D63CACC00C60864 /* LostItemChatDetailDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462722D63CACC00C60864 /* LostItemChatDetailDto.swift */; }; + D2D462752D63CB3E00C60864 /* LostItemFetchChatDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462742D63CB3E00C60864 /* LostItemFetchChatDetailUseCase.swift */; }; + D2D4627A2D63D4D400C60864 /* LostItemBlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D462792D63D4D400C60864 /* LostItemBlockUserUseCase.swift */; }; + D2D4627C2D6482AA00C60864 /* LostItemCreateChatRoomResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4627B2D6482AA00C60864 /* LostItemCreateChatRoomResponse.swift */; }; + D2D4627E2D64835B00C60864 /* LostItemCreateChatRoomUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4627D2D64835A00C60864 /* LostItemCreateChatRoomUseCase.swift */; }; D2D7C0D02C28285600C85A85 /* LandRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D7C0CF2C28285600C85A85 /* LandRepository.swift */; }; D2D7C0D52C28298500C85A85 /* FetchLandListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D7C0D42C28298500C85A85 /* FetchLandListUseCase.swift */; }; D2D7C0D72C28299200C85A85 /* FetchLandDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D7C0D62C28299200C85A85 /* FetchLandDetailUseCase.swift */; }; @@ -1415,8 +1411,8 @@ 7CC42B952F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatDateHeaderView.swift; sourceTree = ""; }; 7CC42B972F5E812F00940CE1 /* CallVanChatLeftCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatLeftCell.swift; sourceTree = ""; }; 7CC42B9B2F5E834E00940CE1 /* CallVanChatRightCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatRightCell.swift; sourceTree = ""; }; - 7CCB1E4A2F29D87C00472669 /* PostChatDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostChatDetailUseCase.swift; sourceTree = ""; }; - 7CCB1E4D2F29DB3200472669 /* PostChatDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostChatDetailRequest.swift; sourceTree = ""; }; + 7CCB1E4A2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemPostChatDetailUseCase.swift; sourceTree = ""; }; + 7CCB1E4D2F29DB3200472669 /* LostItemPostChatDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemPostChatDetailRequest.swift; sourceTree = ""; }; 7CD411E92FEB92280079467B /* HomeTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabBarItem.swift; sourceTree = ""; }; 7CEB3B4A2FBFF4B700AF2B81 /* DeleteDeviceTokenUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDeviceTokenUseCase.swift; sourceTree = ""; }; 7CED8EC32EF2D1F900457128 /* ShopCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopCollectionView.swift; sourceTree = ""; }; @@ -1435,15 +1431,15 @@ 7CED8EEF2EF2D1F900457128 /* DatePickerDropdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerDropdownView.swift; sourceTree = ""; }; 7CED8EF12EF2D1F900457128 /* PostLostItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLostItemViewModel.swift; sourceTree = ""; }; 7CED8EF22EF2D1F900457128 /* PostLostItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLostItemViewController.swift; sourceTree = ""; }; - 7CED8EF92EF2D1F900457128 /* ChatListTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTableViewModel.swift; sourceTree = ""; }; - 7CED8EFA2EF2D1F900457128 /* ChatListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTableViewController.swift; sourceTree = ""; }; - 7CED8EFC2EF2D1F900457128 /* ChatHistoryTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistoryTableView.swift; sourceTree = ""; }; - 7CED8EFD2EF2D1F900457128 /* ChatTextTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextTableViewCell.swift; sourceTree = ""; }; - 7CED8EFE2EF2D1F900457128 /* ChatImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatImageTableViewCell.swift; sourceTree = ""; }; - 7CED8EFF2EF2D1F900457128 /* ChatDateHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDateHeaderView.swift; sourceTree = ""; }; - 7CED8F012EF2D1F900457128 /* BlockCheckModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockCheckModalViewController.swift; sourceTree = ""; }; - 7CED8F022EF2D1F900457128 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = ""; }; - 7CED8F032EF2D1F900457128 /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; + 7CED8EF92EF2D1F900457128 /* LostItemChatListTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatListTableViewModel.swift; sourceTree = ""; }; + 7CED8EFA2EF2D1F900457128 /* LostItemChatListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatListTableViewController.swift; sourceTree = ""; }; + 7CED8EFC2EF2D1F900457128 /* LostItemChatHistoryTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatHistoryTableView.swift; sourceTree = ""; }; + 7CED8EFD2EF2D1F900457128 /* LostItemChatTextTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatTextTableViewCell.swift; sourceTree = ""; }; + 7CED8EFE2EF2D1F900457128 /* LostItemChatImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatImageTableViewCell.swift; sourceTree = ""; }; + 7CED8EFF2EF2D1F900457128 /* LostItemChatDateHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatDateHeaderView.swift; sourceTree = ""; }; + 7CED8F012EF2D1F900457128 /* LostItemBlockCheckModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemBlockCheckModalViewController.swift; sourceTree = ""; }; + 7CED8F022EF2D1F900457128 /* LostItemChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatViewModel.swift; sourceTree = ""; }; + 7CED8F032EF2D1F900457128 /* LostItemChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatViewController.swift; sourceTree = ""; }; 7CED8F072EF2D1F900457128 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 7CED8F082EF2D1F900457128 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 7CED8F0A2EF2D1F900457128 /* ChangeMyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeMyProfileViewController.swift; sourceTree = ""; }; @@ -1919,9 +1915,9 @@ D2A8F8BD2C4A78F20090C7A4 /* CoopShopData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoopShopData.swift; sourceTree = ""; }; D2A8F8BF2C4BCD470090C7A4 /* ShareMenuListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCase.swift; sourceTree = ""; }; D2A8F8C12C4BD36C0090C7A4 /* ShareService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareService.swift; sourceTree = ""; }; - D2B2193A2D66626900EAF5B1 /* ChatHistoryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistoryData.swift; sourceTree = ""; }; + D2B2193A2D66626900EAF5B1 /* LostItemChatHistoryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatHistoryData.swift; sourceTree = ""; }; D2B2193C2D66828200EAF5B1 /* WebSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketManager.swift; sourceTree = ""; }; - D2B2193E2D66DDF900EAF5B1 /* ChatDateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDateInfo.swift; sourceTree = ""; }; + D2B2193E2D66DDF900EAF5B1 /* LostItemChatDateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatDateInfo.swift; sourceTree = ""; }; D2B7EBC92D37C81B00EE46B0 /* PostLostItemRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLostItemRequest.swift; sourceTree = ""; }; D2B7EBD92D382C8800EE46B0 /* UserTypeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTypeResponse.swift; sourceTree = ""; }; D2B7EBDB2D382D5400EE46B0 /* CheckAuthUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckAuthUseCase.swift; sourceTree = ""; }; @@ -1943,18 +1939,14 @@ D2D422712BB2A7BE00A8AF04 /* Confirmable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Confirmable.swift; sourceTree = ""; }; D2D422742BB2AB3A00A8AF04 /* PasswordConfirmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordConfirmer.swift; sourceTree = ""; }; D2D422762BB2EA3900A8AF04 /* FindPasswordRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindPasswordRequest.swift; sourceTree = ""; }; - D2D4625F2D63A81500C60864 /* ChatRoomDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomDto.swift; sourceTree = ""; }; - D2D462612D63B90100C60864 /* ChatRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRepository.swift; sourceTree = ""; }; - D2D462652D63B98F00C60864 /* FetchChatRoomUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchChatRoomUseCase.swift; sourceTree = ""; }; - D2D462692D63BA5200C60864 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; }; - D2D4626B2D63BAC600C60864 /* ChatAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAPI.swift; sourceTree = ""; }; - D2D4626D2D63BD1400C60864 /* DefaultChatRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultChatRepository.swift; sourceTree = ""; }; - D2D462702D63C5ED00C60864 /* ChatRoomItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomItem.swift; sourceTree = ""; }; - D2D462722D63CACC00C60864 /* ChatDetailDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDetailDto.swift; sourceTree = ""; }; - D2D462742D63CB3E00C60864 /* FetchChatDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchChatDetailUseCase.swift; sourceTree = ""; }; - D2D462792D63D4D400C60864 /* BlockUserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserUseCase.swift; sourceTree = ""; }; - D2D4627B2D6482AA00C60864 /* CreateChatRoomResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateChatRoomResponse.swift; sourceTree = ""; }; - D2D4627D2D64835A00C60864 /* CreateChatRoomUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateChatRoomUseCase.swift; sourceTree = ""; }; + D2D4625F2D63A81500C60864 /* LostItemChatRoomDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatRoomDto.swift; sourceTree = ""; }; + D2D462652D63B98F00C60864 /* LostItemFetchChatRoomUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemFetchChatRoomUseCase.swift; sourceTree = ""; }; + D2D462702D63C5ED00C60864 /* LostItemChatRoomItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatRoomItem.swift; sourceTree = ""; }; + D2D462722D63CACC00C60864 /* LostItemChatDetailDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemChatDetailDto.swift; sourceTree = ""; }; + D2D462742D63CB3E00C60864 /* LostItemFetchChatDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemFetchChatDetailUseCase.swift; sourceTree = ""; }; + D2D462792D63D4D400C60864 /* LostItemBlockUserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemBlockUserUseCase.swift; sourceTree = ""; }; + D2D4627B2D6482AA00C60864 /* LostItemCreateChatRoomResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemCreateChatRoomResponse.swift; sourceTree = ""; }; + D2D4627D2D64835A00C60864 /* LostItemCreateChatRoomUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemCreateChatRoomUseCase.swift; sourceTree = ""; }; D2D7C0CF2C28285600C85A85 /* LandRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandRepository.swift; sourceTree = ""; }; D2D7C0D42C28298500C85A85 /* FetchLandListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchLandListUseCase.swift; sourceTree = ""; }; D2D7C0D62C28299200C85A85 /* FetchLandDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchLandDetailUseCase.swift; sourceTree = ""; }; @@ -2227,6 +2219,7 @@ 7C372ED72F1B7F3100149729 /* LostItem */ = { isa = PBXGroup; children = ( + 7CCB1E4C2F29DB2500472669 /* LostItemChat */, 7C372ED82F1B7F3900149729 /* FetchLostItemListRequest.swift */, 7C8ADD2C2F20B43F00F85BDE /* UpdateLostItemRequest.swift */, D2FFA9842D6333BB00EF8E56 /* ReportLostItemRequest.swift */, @@ -2830,6 +2823,7 @@ 7C8ADD362F20C0A500F85BDE /* LostItem */ = { isa = PBXGroup; children = ( + D2D4625E2D63A78000C60864 /* LostItemChat */, 7C8ADD372F20C0B200F85BDE /* LostItemListDto.swift */, 7C8ADD392F20C10500F85BDE /* LostItemListDataDto.swift */, 7C61D3142F212FB3005FAC3A /* LostItemDataDto.swift */, @@ -2854,6 +2848,11 @@ 7CB11C7F2FB32AF1004E0C80 /* UnsubscribeLostItemKeywordUseCase.swift */, 7C7BD71E302B2B37003C5A15 /* PostLostItemUseCase.swift */, 7C7BD720302B2C25003C5A15 /* ReportLostItemUseCase.swift */, + D2D462652D63B98F00C60864 /* LostItemFetchChatRoomUseCase.swift */, + D2D462742D63CB3E00C60864 /* LostItemFetchChatDetailUseCase.swift */, + D2D462792D63D4D400C60864 /* LostItemBlockUserUseCase.swift */, + D2D4627D2D64835A00C60864 /* LostItemCreateChatRoomUseCase.swift */, + 7CCB1E4A2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift */, ); path = LostItem; sourceTree = ""; @@ -2911,6 +2910,9 @@ 7CA63A2F2F1B13F100226F86 /* LostItemStats.swift */, 7C372F262F1D644A00149729 /* LostItemData.swift */, 7CB11C7B2FB32817004E0C80 /* LostItemKeyword.swift */, + D2D462702D63C5ED00C60864 /* LostItemChatRoomItem.swift */, + D2B2193A2D66626900EAF5B1 /* LostItemChatHistoryData.swift */, + D2B2193E2D66DDF900EAF5B1 /* LostItemChatDateInfo.swift */, ); path = LostItem; sourceTree = ""; @@ -3270,12 +3272,12 @@ path = CallVanChatTableView; sourceTree = ""; }; - 7CCB1E4C2F29DB2500472669 /* Chat */ = { + 7CCB1E4C2F29DB2500472669 /* LostItemChat */ = { isa = PBXGroup; children = ( - 7CCB1E4D2F29DB3200472669 /* PostChatDetailRequest.swift */, + 7CCB1E4D2F29DB3200472669 /* LostItemPostChatDetailRequest.swift */, ); - path = Chat; + path = LostItemChat; sourceTree = ""; }; 7CED8EC52EF2D1F900457128 /* ShopCollectionView */ = { @@ -3354,48 +3356,41 @@ 7CED8EF32EF2D1F900457128 /* PostLostItem */, 7C61D3372F21FADF005FAC3A /* EditLostItem */, 7CB11C162FB1CFED004E0C80 /* LostItemKeyword */, + 7CED8EFB2EF2D1F900457128 /* LostItemChatList */, + 7CED8F042EF2D1F900457128 /* LostItemChat */, ); path = LostItem; sourceTree = ""; }; - 7CED8EFB2EF2D1F900457128 /* ChatList */ = { + 7CED8EFB2EF2D1F900457128 /* LostItemChatList */ = { isa = PBXGroup; children = ( - 7CED8EF92EF2D1F900457128 /* ChatListTableViewModel.swift */, - 7CED8EFA2EF2D1F900457128 /* ChatListTableViewController.swift */, + 7CED8EF92EF2D1F900457128 /* LostItemChatListTableViewModel.swift */, + 7CED8EFA2EF2D1F900457128 /* LostItemChatListTableViewController.swift */, ); - path = ChatList; + path = LostItemChatList; sourceTree = ""; }; - 7CED8F002EF2D1F900457128 /* ChatHistoryTableView */ = { + 7CED8F002EF2D1F900457128 /* LostItemChatHistoryTableView */ = { isa = PBXGroup; children = ( - 7CED8EFC2EF2D1F900457128 /* ChatHistoryTableView.swift */, - 7CED8EFD2EF2D1F900457128 /* ChatTextTableViewCell.swift */, - 7CED8EFE2EF2D1F900457128 /* ChatImageTableViewCell.swift */, - 7CED8EFF2EF2D1F900457128 /* ChatDateHeaderView.swift */, + 7CED8EFC2EF2D1F900457128 /* LostItemChatHistoryTableView.swift */, + 7CED8EFD2EF2D1F900457128 /* LostItemChatTextTableViewCell.swift */, + 7CED8EFE2EF2D1F900457128 /* LostItemChatImageTableViewCell.swift */, + 7CED8EFF2EF2D1F900457128 /* LostItemChatDateHeaderView.swift */, ); - path = ChatHistoryTableView; + path = LostItemChatHistoryTableView; sourceTree = ""; }; - 7CED8F042EF2D1F900457128 /* Chat */ = { + 7CED8F042EF2D1F900457128 /* LostItemChat */ = { isa = PBXGroup; children = ( - 7CED8F002EF2D1F900457128 /* ChatHistoryTableView */, - 7CED8F012EF2D1F900457128 /* BlockCheckModalViewController.swift */, - 7CED8F022EF2D1F900457128 /* ChatViewModel.swift */, - 7CED8F032EF2D1F900457128 /* ChatViewController.swift */, + 7CED8F002EF2D1F900457128 /* LostItemChatHistoryTableView */, + 7CED8F012EF2D1F900457128 /* LostItemBlockCheckModalViewController.swift */, + 7CED8F022EF2D1F900457128 /* LostItemChatViewModel.swift */, + 7CED8F032EF2D1F900457128 /* LostItemChatViewController.swift */, ); - path = Chat; - sourceTree = ""; - }; - 7CED8F052EF2D1F900457128 /* Chat */ = { - isa = PBXGroup; - children = ( - 7CED8EFB2EF2D1F900457128 /* ChatList */, - 7CED8F042EF2D1F900457128 /* Chat */, - ); - path = Chat; + path = LostItemChat; sourceTree = ""; }; 7CED8F092EF2D1F900457128 /* Settings */ = { @@ -4315,7 +4310,6 @@ 7C457B2A2FCE1BA80011D338 /* Home */, 7C016541300A9FF80013DD7B /* Department */, 7CED8EF42EF2D1F900457128 /* LostItem */, - 7CED8F052EF2D1F900457128 /* Chat */, 7CED8F202EF2D1F900457128 /* Setting */, 7CED8F3B2EF2D1F900457128 /* Login */, 7CED8F722EF2D1F900457128 /* Notice */, @@ -4874,7 +4868,6 @@ 7C8A943E2FD424E400DEA6F5 /* Home */, 7CA60D712F66217E007C08C1 /* CallVan */, 7C8ADD362F20C0A500F85BDE /* LostItem */, - D2D4625E2D63A78000C60864 /* Chat */, 833D9C202C6B4F4C00982145 /* NoticeList */, D29F21EE2C4F611F00994554 /* Noti */, D29F21ED2C4F610400994554 /* User */, @@ -5179,7 +5172,6 @@ children = ( 7C640405300F3EA000A9589F /* Department */, 7CA60D6E2F661B29007C08C1 /* CallVan */, - 7CCB1E4C2F29DB2500472669 /* Chat */, 7C372ED72F1B7F3100149729 /* LostItem */, 833D9C3A2C705E3F00982145 /* NoticeList */, 7C04A47E2FB3380700214992 /* Noti */, @@ -5203,36 +5195,14 @@ path = UserInputConfirmer; sourceTree = ""; }; - D2D4625E2D63A78000C60864 /* Chat */ = { + D2D4625E2D63A78000C60864 /* LostItemChat */ = { isa = PBXGroup; children = ( - D2D4625F2D63A81500C60864 /* ChatRoomDto.swift */, - D2D462722D63CACC00C60864 /* ChatDetailDto.swift */, - D2D4627B2D6482AA00C60864 /* CreateChatRoomResponse.swift */, + D2D4625F2D63A81500C60864 /* LostItemChatRoomDto.swift */, + D2D462722D63CACC00C60864 /* LostItemChatDetailDto.swift */, + D2D4627B2D6482AA00C60864 /* LostItemCreateChatRoomResponse.swift */, ); - path = Chat; - sourceTree = ""; - }; - D2D462642D63B97600C60864 /* Chat */ = { - isa = PBXGroup; - children = ( - D2D462652D63B98F00C60864 /* FetchChatRoomUseCase.swift */, - D2D462742D63CB3E00C60864 /* FetchChatDetailUseCase.swift */, - D2D462792D63D4D400C60864 /* BlockUserUseCase.swift */, - D2D4627D2D64835A00C60864 /* CreateChatRoomUseCase.swift */, - 7CCB1E4A2F29D87C00472669 /* PostChatDetailUseCase.swift */, - ); - path = Chat; - sourceTree = ""; - }; - D2D4626F2D63C58900C60864 /* Chat */ = { - isa = PBXGroup; - children = ( - D2D462702D63C5ED00C60864 /* ChatRoomItem.swift */, - D2B2193A2D66626900EAF5B1 /* ChatHistoryData.swift */, - D2B2193E2D66DDF900EAF5B1 /* ChatDateInfo.swift */, - ); - path = Chat; + path = LostItemChat; sourceTree = ""; }; D2D7C0D12C28293200C85A85 /* Land */ = { @@ -5271,7 +5241,6 @@ 7C8A00372FD400000011D338 /* Home */, 7CC42B452F5993DF00940CE1 /* CallVan */, 7C8ADD412F20C3CE00F85BDE /* LostItem */, - D2D462642D63B97600C60864 /* Chat */, 833D9C3F2C705F0E00982145 /* NoticeList */, D208A7C72CABB467007040E7 /* Core */, D20AB9502C5534DD006BC684 /* Noti */, @@ -5291,11 +5260,10 @@ D2FB23492BFD451F0098BA2B /* Model */ = { isa = PBXGroup; children = ( - 7C8A00362FD400000011D338 /* Home */, 7C7CCE6E2F866E8900E3A54B /* Core */, + 7C8A00362FD400000011D338 /* Home */, 7CA63A1F2F1AF73500226F86 /* LostItem */, D815DD972E08216B004A72BE /* Order */, - D2D4626F2D63C58900C60864 /* Chat */, D215726D2CEE28C30061E725 /* Timetable */, 833D9C102C6B269000982145 /* NoticeList */, D29F21EF2C4FD22900994554 /* Land */, @@ -5321,7 +5289,6 @@ D2D7C0CF2C28285600C85A85 /* LandRepository.swift */, D20AB94D2C553427006BC684 /* NotiRepository.swift */, 83DD0D7C2C4F91AD00278ABD /* BusRepository.swift */, - D2D462612D63B90100C60864 /* ChatRepository.swift */, 833D9C382C7058C200982145 /* NoticeListRepository.swift */, D208A79F2CA10F1A007040E7 /* AbTestRepository.swift */, D208A7C52CABB426007040E7 /* CoreRepository.swift */, @@ -5348,7 +5315,6 @@ 833D9C482C7068CB00982145 /* DefaultNoticeListRepository.swift */, D208A7D22CAD7436007040E7 /* DefaultAbTestRepository.swift */, D208A7CE2CABB85B007040E7 /* DefaultCoreRepository.swift */, - D2D4626D2D63BD1400C60864 /* DefaultChatRepository.swift */, 7CA60D642F6619B7007C08C1 /* DefaultCallVanRepository.swift */, 7CA686792FFCB758007937E9 /* DefaultNotificationHistoryRepository.swift */, 7C640401300F39BB00A9589F /* DefaultDepartmentRepository.swift */, @@ -5411,7 +5377,6 @@ 833D9C462C7067AE00982145 /* NoticeListAPI.swift */, D208A7AD2CA117AA007040E7 /* AbTestAPI.swift */, D208A7CC2CABB7A3007040E7 /* CoreAPI.swift */, - D2D4626B2D63BAC600C60864 /* ChatAPI.swift */, 7C8ADD342F20BF4600F85BDE /* LostItemAPI.swift */, 7CA60D6C2F661A6D007C08C1 /* CallVanAPI.swift */, 7C8A94432FD4279B00DEA6F5 /* HomeAPI.swift */, @@ -5432,7 +5397,6 @@ D2FB23722C04305A0098BA2B /* Service */ = { isa = PBXGroup; children = ( - D2D462692D63BA5200C60864 /* ChatService.swift */, D20AB9572C553621006BC684 /* NotiService.swift */, D2FB236D2C0410510098BA2B /* LogAnalyticsService.swift */, D29F21C22C4DE27C00994554 /* UserService.swift */, @@ -5706,7 +5670,7 @@ D22C54582C68B033000826DA /* ReportReviewRequest.swift in Sources */, D20AD6F92E02A11D0067760A /* FindIdSmsResponse.swift in Sources */, D27DD0D22BA608310081FD36 /* AppDelegate.swift in Sources */, - D2D4627C2D6482AA00C60864 /* CreateChatRoomResponse.swift in Sources */, + D2D4627C2D6482AA00C60864 /* LostItemCreateChatRoomResponse.swift in Sources */, D29F21D12C4E164C00994554 /* DefaultTimetableRepository.swift in Sources */, D22C54562C68B033000826DA /* WriteReviewRequest.swift in Sources */, 7C17EEA82EBF4F25008BCA89 /* ShopSearch.swift in Sources */, @@ -5723,7 +5687,7 @@ D29F21F62C4FD2C700994554 /* FetchLandDetailRequest.swift in Sources */, D208A7A02CA10F1A007040E7 /* AbTestRepository.swift in Sources */, D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */, - D2B2193B2D66626900EAF5B1 /* ChatHistoryData.swift in Sources */, + D2B2193B2D66626900EAF5B1 /* LostItemChatHistoryData.swift in Sources */, 83DD0DBB2C551E5100278ABD /* BusTimetableInfo.swift in Sources */, B47839052E70FAFE00D002E3 /* OrderShopSummary.swift in Sources */, D249CD3D2C159350000813F9 /* DateProvider.swift in Sources */, @@ -5783,12 +5747,12 @@ 7C8ADD352F20BF4600F85BDE /* LostItemAPI.swift in Sources */, 7C8A94422FD4278800DEA6F5 /* HomeService.swift in Sources */, 83DD0D762C4F6FDE00278ABD /* BusPlace.swift in Sources */, - D2B2193F2D66DDF900EAF5B1 /* ChatDateInfo.swift in Sources */, + D2B2193F2D66DDF900EAF5B1 /* LostItemChatDateInfo.swift in Sources */, D89F8FD92DF0A8A4000265DC /* DefaultTextField.swift in Sources */, 7CC42B392F58284100940CE1 /* CallVanNotificationTableView.swift in Sources */, 7CC42B332F58247D00940CE1 /* CallVanNotification.swift in Sources */, B46B8CC22E76CB7300A8E797 /* ShopSummaryDto.swift in Sources */, - D2D462662D63B98F00C60864 /* FetchChatRoomUseCase.swift in Sources */, + D2D462662D63B98F00C60864 /* LostItemFetchChatRoomUseCase.swift in Sources */, 7CA0D6232EC74ADE00E9A282 /* ZoomedImageCollectionViewCell.swift in Sources */, D2BD36962BAA027A00FAE609 /* TokenRefreshRequest.swift in Sources */, D2D7C0D72C28299200C85A85 /* FetchLandDetailUseCase.swift in Sources */, @@ -5905,7 +5869,7 @@ D2FFA9702D5F4DC500EF8E56 /* CheckLoginUseCase.swift in Sources */, 833D9C452C70672C00982145 /* NoticeListService.swift in Sources */, 7CA60D652F6619B7007C08C1 /* DefaultCallVanRepository.swift in Sources */, - D2D462602D63A81500C60864 /* ChatRoomDto.swift in Sources */, + D2D462602D63A81500C60864 /* LostItemChatRoomDto.swift in Sources */, D20AD6ED2E028FA20067760A /* FindIdEmailRequest.swift in Sources */, 7C5583622F11779400D3A980 /* ShopInfoFooterView.swift in Sources */, 831ABDC62CA2A7C20099B70C /* FetchRecentSearchedWordUseCase.swift in Sources */, @@ -5987,11 +5951,11 @@ 7CED90C62EF2D1F900457128 /* TimetableCollectionViewCell.swift in Sources */, 7C016556300AA2E10013DD7B /* DepartmentCategory.swift in Sources */, 7CED90CA2EF2D1F900457128 /* LandOptionCollectionViewCell.swift in Sources */, - 7CED90CB2EF2D1F900457128 /* ChatListTableViewModel.swift in Sources */, + 7CED90CB2EF2D1F900457128 /* LostItemChatListTableViewModel.swift in Sources */, 7CED90CE2EF2D1F900457128 /* ForceModifyUserViewController.swift in Sources */, 7CED90CF2EF2D1F900457128 /* ChangeMyProfileViewModel.swift in Sources */, 7C0C18912F67FA58001781B3 /* ReportCallVanUserUseCase.swift in Sources */, - 7CED90D02EF2D1F900457128 /* ChatHistoryTableView.swift in Sources */, + 7CED90D02EF2D1F900457128 /* LostItemChatHistoryTableView.swift in Sources */, 7CF77B543003B276003642C1 /* HomeLogoView.swift in Sources */, 7C016558300AA3790013DD7B /* DepartmentTask.swift in Sources */, 7CED90D12EF2D1F900457128 /* NoticeSearchViewController.swift in Sources */, @@ -6002,11 +5966,11 @@ 7CED90D72EF2D1F900457128 /* RecommendedKeywordCollectionView.swift in Sources */, 7CED90D92EF2D1F900457128 /* DiningViewController.swift in Sources */, 7C7BD725302B3E25003C5A15 /* NoticeAISummaryView.swift in Sources */, - 7CCB1E4B2F29D87C00472669 /* PostChatDetailUseCase.swift in Sources */, + 7CCB1E4B2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift in Sources */, 7CED90DA2EF2D1F900457128 /* RecentSearchTableView.swift in Sources */, 7C7BD727302D8002003C5A15 /* NoticeAISummaryDto.swift in Sources */, 7C61D35F2F24000C005FAC3A /* LostItemDataButtonsView.swift in Sources */, - 7CED90DB2EF2D1F900457128 /* ChatImageTableViewCell.swift in Sources */, + 7CED90DB2EF2D1F900457128 /* LostItemChatImageTableViewCell.swift in Sources */, 7CED90DC2EF2D1F900457128 /* ShopSummaryDeliveryButton.swift in Sources */, 7CED90DD2EF2D1F900457128 /* AddDirectCollectionViewCell.swift in Sources */, 7CED90DF2EF2D1F900457128 /* LandDetailViewModel.swift in Sources */, @@ -6023,9 +5987,9 @@ 7CED90F12EF2D1F900457128 /* ForceUpdateViewModel.swift in Sources */, 7CA60D6D2F661A6D007C08C1 /* CallVanAPI.swift in Sources */, 7CED90F22EF2D1F900457128 /* ShopSearchViewModel.swift in Sources */, - 7CED90F42EF2D1F900457128 /* ChatListTableViewController.swift in Sources */, + 7CED90F42EF2D1F900457128 /* LostItemChatListTableViewController.swift in Sources */, 7CED90F52EF2D1F900457128 /* DiningViewModel.swift in Sources */, - 7CED90F62EF2D1F900457128 /* ChatDateHeaderView.swift in Sources */, + 7CED90F62EF2D1F900457128 /* LostItemChatDateHeaderView.swift in Sources */, 7CED90F72EF2D1F900457128 /* ReviewImageCollectionViewCell.swift in Sources */, 7CED90FA2EF2D1F900457128 /* LandDetailViewController.swift in Sources */, 7CED90FB2EF2D1F900457128 /* TabBarCollectionViewCell.swift in Sources */, @@ -6042,10 +6006,10 @@ 7CB11C802FB32AF1004E0C80 /* UnsubscribeLostItemKeywordUseCase.swift in Sources */, 7CED91022EF2D1F900457128 /* ShopCollectionViewCell.swift in Sources */, 7CED91042EF2D1F900457128 /* TimetableCell.swift in Sources */, - 7CCB1E4E2F29DB3200472669 /* PostChatDetailRequest.swift in Sources */, + 7CCB1E4E2F29DB3200472669 /* LostItemPostChatDetailRequest.swift in Sources */, 7CED91052EF2D1F900457128 /* SelectTypeFormViewController.swift in Sources */, 7CED91072EF2D1F900457128 /* AddDirectHeaderView.swift in Sources */, - 7CED91082EF2D1F900457128 /* BlockCheckModalViewController.swift in Sources */, + 7CED91082EF2D1F900457128 /* LostItemBlockCheckModalViewController.swift in Sources */, 7CED91092EF2D1F900457128 /* FacilityInfoViewController.swift in Sources */, 7CED910A2EF2D1F900457128 /* LeftAlignedFlowLayout.swift in Sources */, 7CED910B2EF2D1F900457128 /* AddClassCollectionView.swift in Sources */, @@ -6055,7 +6019,7 @@ 7CED910F2EF2D1F900457128 /* ShopCollectionView.swift in Sources */, 7CED91122EF2D1F900457128 /* LectureView.swift in Sources */, 7CA60D7D2F665128007C08C1 /* DeleteNotificationUseCase.swift in Sources */, - 7CED91142EF2D1F900457128 /* ChatViewController.swift in Sources */, + 7CED91142EF2D1F900457128 /* LostItemChatViewController.swift in Sources */, 7CED91152EF2D1F900457128 /* LoginViewModel.swift in Sources */, 7C7F56B42FFB6A6400847151 /* NotificationPopUpViewController.swift in Sources */, 7CED91162EF2D1F900457128 /* ShopSortOptionSheetViewController.swift in Sources */, @@ -6326,14 +6290,14 @@ 7CED91BB2EF2D1F900457128 /* EnterFormViewController.swift in Sources */, 7CF77B4130027314003642C1 /* ProfileUserInfoView.swift in Sources */, 7CED91BD2EF2D1F900457128 /* RecommendedSearchCollectionViewCell.swift in Sources */, - 7CED91C02EF2D1F900457128 /* ChatTextTableViewCell.swift in Sources */, + 7CED91C02EF2D1F900457128 /* LostItemChatTextTableViewCell.swift in Sources */, 7CED91C12EF2D1F900457128 /* ShopViewModel.swift in Sources */, 7CED91C22EF2D1F900457128 /* ReviewImageCollectionView.swift in Sources */, 7CED91C62EF2D1F900457128 /* HotArticlesNoticeTableViewCell.swift in Sources */, 7C016576300BD9610013DD7B /* FetchDepartmentByCategoryUseCase.swift in Sources */, 7CED91C92EF2D1F900457128 /* CategoryCollectionView.swift in Sources */, 7CED91CB2EF2D1F900457128 /* ShopSummaryImagesCollectionViewCell.swift in Sources */, - 7CED91CD2EF2D1F900457128 /* ChatViewModel.swift in Sources */, + 7CED91CD2EF2D1F900457128 /* LostItemChatViewModel.swift in Sources */, 7C640400300F395300A9589F /* DepartmentRepository.swift in Sources */, 7CED91CE2EF2D1F900457128 /* NotiViewModel.swift in Sources */, 7CED91CF2EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift in Sources */, @@ -6409,7 +6373,7 @@ D20AB9582C553621006BC684 /* NotiService.swift in Sources */, 7CF77B6530056EB9003642C1 /* NoticeKeywordSearchButton.swift in Sources */, D29F21DF2C4F2E3000994554 /* FetchShopDataRequest.swift in Sources */, - D2D4627A2D63D4D400C60864 /* BlockUserUseCase.swift in Sources */, + D2D4627A2D63D4D400C60864 /* LostItemBlockUserUseCase.swift in Sources */, D21572562CEDC1830061E725 /* ModifyFrameUseCase.swift in Sources */, D2FB23572BFD50120098BA2B /* DefaultShopRepository.swift in Sources */, 833D9C602C7457EE00982145 /* NoticeDataInfo.swift in Sources */, @@ -6418,7 +6382,7 @@ D2FB23602BFDABBB0098BA2B /* Router.swift in Sources */, D89F8FDB2DF12847000265DC /* StatefulButton.swift in Sources */, D208A7BB2CA169DF007040E7 /* FetchBeneficialShopUseCase.swift in Sources */, - D2D462712D63C5ED00C60864 /* ChatRoomItem.swift in Sources */, + D2D462712D63C5ED00C60864 /* LostItemChatRoomItem.swift in Sources */, 7C8ADD2D2F20B43F00F85BDE /* UpdateLostItemRequest.swift in Sources */, 7C7BD723302B3D8F003C5A15 /* NoticeAISummary.swift in Sources */, D2B7EBCA2D37C81B00EE46B0 /* PostLostItemRequest.swift in Sources */, @@ -6513,7 +6477,7 @@ D2DA83912C181A1400A2F156 /* FetchShopDataUseCase.swift in Sources */, 7CD411EA2FEB92280079467B /* HomeTabBarItem.swift in Sources */, D208A7AC2CA112D0007040E7 /* AssignAbTestResponse.swift in Sources */, - D2D462732D63CACC00C60864 /* ChatDetailDto.swift in Sources */, + D2D462732D63CACC00C60864 /* LostItemChatDetailDto.swift in Sources */, 7C333EA330303678009D2B89 /* ModalContentView.swift in Sources */, D20AD6FF2E02A4AB0067760A /* FindIdEmailUseCase.swift in Sources */, D215724E2CEDBEC60061E725 /* FrameDto.swift in Sources */, @@ -6529,8 +6493,7 @@ 8380FDC32BC1CBB800120036 /* BusCourseDto.swift in Sources */, 83DD0D742C4F6FD000278ABD /* BusType.swift in Sources */, 7C372F242F1CDB4400149729 /* LostItemDataContentView.swift in Sources */, - D2D4626E2D63BD1400C60864 /* DefaultChatRepository.swift in Sources */, - D2D462752D63CB3E00C60864 /* FetchChatDetailUseCase.swift in Sources */, + D2D462752D63CB3E00C60864 /* LostItemFetchChatDetailUseCase.swift in Sources */, 83DD0DBF2C55258200278ABD /* FetchShuttleBusRoutesUseCase.swift in Sources */, D2078BF62BC4CCEB00A39861 /* EventDto.swift in Sources */, D20AD6F32E029F7F0067760A /* SendVerificationEmailUseCase.swift in Sources */, @@ -6557,7 +6520,6 @@ 7C17EEB22EBF7175008BCA89 /* FetchSearchShopUseCase.swift in Sources */, D2B2193D2D66828300EAF5B1 /* WebSocketManager.swift in Sources */, 7C7CCE702F866E9400E3A54B /* AppPath.swift in Sources */, - D2D4626A2D63BA5200C60864 /* ChatService.swift in Sources */, 7C61D3472F220EA2005FAC3A /* EditLostItemCategoryView.swift in Sources */, 7CED92462EF3DC2800457128 /* OrderCategoryCollectionViewCell.swift in Sources */, 7C8A94402FD426B400DEA6F5 /* HomeRepository.swift in Sources */, @@ -6592,8 +6554,7 @@ D85939752DC12C9D00CE4CB2 /* CheckDuplicatedIdUsecase.swift in Sources */, 7CF77B3C300254C8003642C1 /* ProfileView.swift in Sources */, 83BB83132CEC814800946623 /* FetchKeywordNoticePhraseUseCase.swift in Sources */, - D2D4627E2D64835B00C60864 /* CreateChatRoomUseCase.swift in Sources */, - D2D4626C2D63BAC600C60864 /* ChatAPI.swift in Sources */, + D2D4627E2D64835B00C60864 /* LostItemCreateChatRoomUseCase.swift in Sources */, D21572502CEDC1680061E725 /* CreateFrameUseCase.swift in Sources */, D2009F762C17270F00211D1B /* CalendarDate.swift in Sources */, D29F21CC2C4E157E00994554 /* FetchDeptListUseCase.swift in Sources */, @@ -6602,7 +6563,6 @@ D21572662CEDDDC60061E725 /* FetchMySemesterUseCase.swift in Sources */, B68D2CEB2EA50C1E00F3B479 /* UIViewController+Toast.swift in Sources */, D249CD2A2C12DD34000813F9 /* DiningAPI.swift in Sources */, - D2D462622D63B90100C60864 /* ChatRepository.swift in Sources */, 83E770FC2D05B6DD00EB3F84 /* FetchEmergencyNoticeUseCase.swift in Sources */, 833D9C122C6B26A900982145 /* NoticeListType.swift in Sources */, D2FB23452BFB6D090098BA2B /* CustomNavigationController.swift in Sources */, From e131e85f9bbd6c84f038435545dccd9a11bca03c Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Wed, 19 Aug 2026 23:17:00 +0900 Subject: [PATCH 065/101] =?UTF-8?q?chore:=20NotificationItem=EC=9D=84=20No?= =?UTF-8?q?tificationHistoryItem=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....swift => NotificationHistoryRecord.swift} | 4 ++-- .../NotificationHistoryService.swift | 22 +++++++++---------- ...DefaultNotificationHistoryRepository.swift | 4 ++-- ...em.swift => NotificationHistoryItem.swift} | 19 ++++++++-------- .../NotificationHistoryRepository.swift | 2 +- .../Home/FetchNotificationListUseCase.swift | 4 ++-- .../Notification/NotificationViewModel.swift | 2 +- NotificationService/NotificationService.swift | 2 +- koin.xcodeproj/project.pbxproj | 20 ++++++++--------- 9 files changed, 39 insertions(+), 40 deletions(-) rename Common/NotificationHistory/{NotificationRecord.swift => NotificationHistoryRecord.swift} (89%) rename Koin/Domain/Model/Home/{NotificationItem.swift => NotificationHistoryItem.swift} (84%) diff --git a/Common/NotificationHistory/NotificationRecord.swift b/Common/NotificationHistory/NotificationHistoryRecord.swift similarity index 89% rename from Common/NotificationHistory/NotificationRecord.swift rename to Common/NotificationHistory/NotificationHistoryRecord.swift index 99974e1e..4a956787 100644 --- a/Common/NotificationHistory/NotificationRecord.swift +++ b/Common/NotificationHistory/NotificationHistoryRecord.swift @@ -1,5 +1,5 @@ // -// NotificationRecord.swift +// NotificationHistoryRecord.swift // koin // // Created by 홍기정 on 7/6/26. @@ -9,7 +9,7 @@ import SwiftData import Foundation @Model -final class NotificationRecord { +final class NotificationHistoryRecord { var body: String var title: String var category: AppPath diff --git a/Common/NotificationHistory/NotificationHistoryService.swift b/Common/NotificationHistory/NotificationHistoryService.swift index 23304b2c..1ad0d49b 100644 --- a/Common/NotificationHistory/NotificationHistoryService.swift +++ b/Common/NotificationHistory/NotificationHistoryService.swift @@ -9,8 +9,8 @@ import SwiftData import Foundation protocol NotificationHistoryService { - func insert(record: NotificationRecord) async throws - func fetchAll() async throws -> [NotificationRecord] + func insert(record: NotificationHistoryRecord) async throws + func fetchAll() async throws -> [NotificationHistoryRecord] func markAsRead(messageId: String) async throws func markAllAsRead() async throws func delete(messageId: String) async throws @@ -25,13 +25,13 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { // MARK: - Initializer init() { container = try? ModelContainer( - for: NotificationRecord.self, + for: NotificationHistoryRecord.self, configurations: .init(groupContainer: .identifier("group.com.bcsdlab.koin")) ) } // MARK: - Create - func insert(record: NotificationRecord) async throws { + func insert(record: NotificationHistoryRecord) async throws { guard let container else { throw SwiftDataError.loadIssueModelContainer } @@ -48,7 +48,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } // MARK: - Read - func fetchAll() async throws -> [NotificationRecord] { + func fetchAll() async throws -> [NotificationHistoryRecord] { guard let container else { throw SwiftDataError.loadIssueModelContainer } @@ -60,7 +60,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } return try await MainActor.run { - var descriptor = FetchDescriptor( + var descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.createdAt, order: .reverse)] ) descriptor.fetchLimit = .max @@ -76,7 +76,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } try await MainActor.run { - var descriptor = FetchDescriptor( + var descriptor = FetchDescriptor( predicate: #Predicate { notification in notification.messageId == messageId } @@ -97,7 +97,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } try await MainActor.run { - var descriptor = FetchDescriptor() + var descriptor = FetchDescriptor() descriptor.fetchLimit = .max try container.mainContext.enumerate(descriptor) { notification in notification.isRead = true @@ -115,7 +115,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } try await MainActor.run { - try container.mainContext.delete(model: NotificationRecord.self, where: #Predicate { notification in + try container.mainContext.delete(model: NotificationHistoryRecord.self, where: #Predicate { notification in notification.messageId == messageId }) try container.mainContext.save() @@ -130,7 +130,7 @@ final class DefaultNotificationHistoryService: NotificationHistoryService { } try await MainActor.run { - try container.mainContext.delete(model: NotificationRecord.self) + try container.mainContext.delete(model: NotificationHistoryRecord.self) try container.mainContext.save() } @@ -149,7 +149,7 @@ extension DefaultNotificationHistoryService { } try await MainActor.run { - try container.mainContext.delete(model: NotificationRecord.self, where: #Predicate { notification in + try container.mainContext.delete(model: NotificationHistoryRecord.self, where: #Predicate { notification in notification.createdAt < expirationDate }) try container.mainContext.save() diff --git a/Koin/Data/Repository/DefaultNotificationHistoryRepository.swift b/Koin/Data/Repository/DefaultNotificationHistoryRepository.swift index 49acccc8..889c0dcf 100644 --- a/Koin/Data/Repository/DefaultNotificationHistoryRepository.swift +++ b/Koin/Data/Repository/DefaultNotificationHistoryRepository.swift @@ -15,10 +15,10 @@ final class DefaultNotificationHistoryRepository: NotificationHistoryRepository self.service = service } - func fetchAll() async throws -> [NotificationItem] { + func fetchAll() async throws -> [NotificationHistoryItem] { try await service.fetchAll() .compactMap { - NotificationItem.init(from: $0) + NotificationHistoryItem.init(from: $0) } } diff --git a/Koin/Domain/Model/Home/NotificationItem.swift b/Koin/Domain/Model/Home/NotificationHistoryItem.swift similarity index 84% rename from Koin/Domain/Model/Home/NotificationItem.swift rename to Koin/Domain/Model/Home/NotificationHistoryItem.swift index 5cd986f1..f2c324eb 100644 --- a/Koin/Domain/Model/Home/NotificationItem.swift +++ b/Koin/Domain/Model/Home/NotificationHistoryItem.swift @@ -1,5 +1,5 @@ // -// NotificationItem.swift +// NotificationHistoryItem.swift // koin // // Created by 홍기정 on 6/3/26. @@ -7,7 +7,7 @@ import Foundation -struct NotificationItem { +struct NotificationHistoryItem { let id: String var isRead: Bool let icon: ImageAsset @@ -18,10 +18,9 @@ struct NotificationItem { let dateText: String } -extension NotificationItem { - - init?(from record: NotificationRecord) { - guard let icon = NotificationItem.icon(for: record.category) else { +extension NotificationHistoryItem { + init?(from record: NotificationHistoryRecord) { + guard let icon = NotificationHistoryItem.icon(for: record.category) else { return nil } self.id = record.messageId @@ -31,9 +30,9 @@ extension NotificationItem { self.uri = record.schemeUri self.title = record.title self.content = record.body - self.dateText = NotificationItem.dateText(for: record.createdAt) + self.dateText = NotificationHistoryItem.dateText(for: record.createdAt) } - + static func icon(for appPath: AppPath) -> ImageAsset? { switch appPath { case .shop: @@ -52,7 +51,7 @@ extension NotificationItem { return nil } } - + static func dateText(for createdAt: Date) -> String { let now = Date() let compareComponents = Calendar.current.dateComponents( @@ -60,7 +59,7 @@ extension NotificationItem { from: createdAt, to: now ) - + if let day = compareComponents.day, 1 <= day { return "\(day)일 전" } diff --git a/Koin/Domain/Repository/NotificationHistoryRepository.swift b/Koin/Domain/Repository/NotificationHistoryRepository.swift index 4ef807fa..2937bc1c 100644 --- a/Koin/Domain/Repository/NotificationHistoryRepository.swift +++ b/Koin/Domain/Repository/NotificationHistoryRepository.swift @@ -8,7 +8,7 @@ import Combine protocol NotificationHistoryRepository { - func fetchAll() async throws -> [NotificationItem] + func fetchAll() async throws -> [NotificationHistoryItem] func deleteAll() async throws func delete(id: String) async throws func markAsRead(id: String) async throws diff --git a/Koin/Domain/UseCase/Home/FetchNotificationListUseCase.swift b/Koin/Domain/UseCase/Home/FetchNotificationListUseCase.swift index 250edd34..c2dddbdc 100644 --- a/Koin/Domain/UseCase/Home/FetchNotificationListUseCase.swift +++ b/Koin/Domain/UseCase/Home/FetchNotificationListUseCase.swift @@ -9,7 +9,7 @@ import Combine import Foundation protocol FetchNotificationHistoryUseCase { - func execute() async throws -> [NotificationItem] + func execute() async throws -> [NotificationHistoryItem] } final class DefaultFetchNotificationHistoryUseCase: FetchNotificationHistoryUseCase { @@ -20,7 +20,7 @@ final class DefaultFetchNotificationHistoryUseCase: FetchNotificationHistoryUseC self.notificationHistoryRepository = notificationHistoryRepository } - func execute() async throws -> [NotificationItem] { + func execute() async throws -> [NotificationHistoryItem] { try await notificationHistoryRepository.fetchAll() } } diff --git a/Koin/Presentation/Home/Notification/NotificationViewModel.swift b/Koin/Presentation/Home/Notification/NotificationViewModel.swift index 6c0f6d92..e2551809 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewModel.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewModel.swift @@ -21,7 +21,7 @@ final class NotificationViewModel: ViewModelProtocol { } enum Output { - case updateNotifications([NotificationItem]) + case updateNotifications([NotificationHistoryItem]) case showToast(String) } diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 9512a76f..7ffa9230 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -70,7 +70,7 @@ extension NotificationService { throw NotificationHistoryError.parsingError } - let notificationRecord = NotificationRecord( + let notificationRecord = NotificationHistoryRecord( body: body, title: title, category: appPath, diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index b42a9960..14f123bc 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -237,8 +237,8 @@ 7C7F43282F65F31500CC5860 /* CallVanReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F43272F65F31500CC5860 /* CallVanReportRequest.swift */; }; 7C7F432A2F65F42300CC5860 /* CallVanRecruitmentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F43292F65F42300CC5860 /* CallVanRecruitmentState.swift */; }; 7C7F56B42FFB6A6400847151 /* NotificationPopUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56B32FFB6A6400847151 /* NotificationPopUpViewController.swift */; }; - 7C7F56B72FFB9FE000847151 /* NotificationRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56B62FFB9FE000847151 /* NotificationRecord.swift */; }; - 7C7F56B82FFBA06200847151 /* NotificationRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56B62FFB9FE000847151 /* NotificationRecord.swift */; }; + 7C7F56B72FFB9FE000847151 /* NotificationHistoryRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56B62FFB9FE000847151 /* NotificationHistoryRecord.swift */; }; + 7C7F56B82FFBA06200847151 /* NotificationHistoryRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56B62FFB9FE000847151 /* NotificationHistoryRecord.swift */; }; 7C7F56B92FFBA0A700847151 /* AppPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */; }; 7C7F56BB2FFBA18100847151 /* NotificationHistoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */; }; 7C7F56BC2FFBA18100847151 /* NotificationHistoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */; }; @@ -282,7 +282,7 @@ 7C8BFCD62FD032A000963679 /* View+linespacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCD52FD032A000963679 /* View+linespacing.swift */; }; 7C8BFCE12FD04B8000963679 /* HomeDiningItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCDD2FD04B8000963679 /* HomeDiningItem.swift */; }; 7C8BFCE32FD04B8000963679 /* HomeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCDE2FD04B8000963679 /* HomeHeader.swift */; }; - 7C8BFCE42FD04B8000963679 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCE02FD04B8000963679 /* NotificationItem.swift */; }; + 7C8BFCE42FD04B8000963679 /* NotificationHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCE02FD04B8000963679 /* NotificationHistoryItem.swift */; }; 7C8BFCE52FD04B8000963679 /* CategoryModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCDC2FD04B8000963679 /* CategoryModels.swift */; }; 7C8BFCEB2FD04BA000963679 /* FetchHomeDiningListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCE62FD04BA000963679 /* FetchHomeDiningListUseCase.swift */; }; 7C8BFCEC2FD04BA000963679 /* FetchHeaderUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCE72FD04BA000963679 /* FetchHeaderUseCase.swift */; }; @@ -1258,7 +1258,7 @@ 7C7F43272F65F31500CC5860 /* CallVanReportRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanReportRequest.swift; sourceTree = ""; }; 7C7F43292F65F42300CC5860 /* CallVanRecruitmentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanRecruitmentState.swift; sourceTree = ""; }; 7C7F56B32FFB6A6400847151 /* NotificationPopUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPopUpViewController.swift; sourceTree = ""; }; - 7C7F56B62FFB9FE000847151 /* NotificationRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRecord.swift; sourceTree = ""; }; + 7C7F56B62FFB9FE000847151 /* NotificationHistoryRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHistoryRecord.swift; sourceTree = ""; }; 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHistoryService.swift; sourceTree = ""; }; 7C82FF772E9D18C7006335A7 /* OrderShopDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopDetail.swift; sourceTree = ""; }; 7C85D2742EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopMenusAndGroupsFromShopUseCase.swift; sourceTree = ""; }; @@ -1301,7 +1301,7 @@ 7C8BFCDC2FD04B8000963679 /* CategoryModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryModels.swift; sourceTree = ""; }; 7C8BFCDD2FD04B8000963679 /* HomeDiningItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDiningItem.swift; sourceTree = ""; }; 7C8BFCDE2FD04B8000963679 /* HomeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeHeader.swift; sourceTree = ""; }; - 7C8BFCE02FD04B8000963679 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; + 7C8BFCE02FD04B8000963679 /* NotificationHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHistoryItem.swift; sourceTree = ""; }; 7C8BFCE62FD04BA000963679 /* FetchHomeDiningListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchHomeDiningListUseCase.swift; sourceTree = ""; }; 7C8BFCE72FD04BA000963679 /* FetchHeaderUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchHeaderUseCase.swift; sourceTree = ""; }; 7C8BFCE82FD04BA000963679 /* FetchHomeCountsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchHomeCountsUseCase.swift; sourceTree = ""; }; @@ -2773,7 +2773,7 @@ 7C8BFCDD2FD04B8000963679 /* HomeDiningItem.swift */, 7C8BFCDE2FD04B8000963679 /* HomeHeader.swift */, 7C8BFCF02FD04B800963679 /* HomeCounts.swift */, - 7C8BFCE02FD04B8000963679 /* NotificationItem.swift */, + 7C8BFCE02FD04B8000963679 /* NotificationHistoryItem.swift */, ); path = Home; sourceTree = ""; @@ -2952,7 +2952,7 @@ isa = PBXGroup; children = ( 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */, - 7C7F56B62FFB9FE000847151 /* NotificationRecord.swift */, + 7C7F56B62FFB9FE000847151 /* NotificationHistoryRecord.swift */, ); path = NotificationHistory; sourceTree = ""; @@ -5654,7 +5654,7 @@ 839D71192BEB1570001BC7F7 /* NotificationService.swift in Sources */, 7CA686902FFCF42E007937E9 /* NotificationHistoryError.swift in Sources */, 7C7F56B92FFBA0A700847151 /* AppPath.swift in Sources */, - 7C7F56B82FFBA06200847151 /* NotificationRecord.swift in Sources */, + 7C7F56B82FFBA06200847151 /* NotificationHistoryRecord.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5833,7 +5833,7 @@ 7C01656B300ABE9E0013DD7B /* DepartmentFooterView.swift in Sources */, 7C05D0782FBC29ED00F4D825 /* ShopBenefitViewModel.swift in Sources */, D29F21F32C4FD23F00994554 /* LandItem.swift in Sources */, - 7C7F56B72FFB9FE000847151 /* NotificationRecord.swift in Sources */, + 7C7F56B72FFB9FE000847151 /* NotificationHistoryRecord.swift in Sources */, D20AB95A2C55369F006BC684 /* NotiAPI.swift in Sources */, 7C457AE72FCDE7240011D338 /* SwiftUIViewModelProtocol.swift in Sources */, 833D9C392C7058C300982145 /* NoticeListRepository.swift in Sources */, @@ -6440,7 +6440,7 @@ 7C8BFCE12FD04B8000963679 /* HomeDiningItem.swift in Sources */, 7C8BFCE32FD04B8000963679 /* HomeHeader.swift in Sources */, 7CF77B4430027321003642C1 /* TimeTableView.swift in Sources */, - 7C8BFCE42FD04B8000963679 /* NotificationItem.swift in Sources */, + 7C8BFCE42FD04B8000963679 /* NotificationHistoryItem.swift in Sources */, 7C8BFCE52FD04B8000963679 /* CategoryModels.swift in Sources */, 7C8BFCF12FD04B800963679 /* HomeCounts.swift in Sources */, D2908D262BB8D11A0008F908 /* SemesterDto.swift in Sources */, From 54f6fa646260525af86abe36fd37a07dcc47ef25 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Wed, 19 Aug 2026 23:32:02 +0900 Subject: [PATCH 066/101] =?UTF-8?q?refactor:=20NotificationHistoryItem?= =?UTF-8?q?=EC=9D=98=20=EB=A1=9C=EA=B9=85=20value=EB=A5=BC=20NotificationH?= =?UTF-8?q?ostoryItem=EC=9D=98=20computed=20property=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/Home/NotificationHistoryItem.swift | 19 +++++++++++++++ .../NotificationViewController.swift | 23 +------------------ .../Notification/NotificationViewModel.swift | 14 ++++++++--- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/Koin/Domain/Model/Home/NotificationHistoryItem.swift b/Koin/Domain/Model/Home/NotificationHistoryItem.swift index f2c324eb..1a42978c 100644 --- a/Koin/Domain/Model/Home/NotificationHistoryItem.swift +++ b/Koin/Domain/Model/Home/NotificationHistoryItem.swift @@ -19,6 +19,25 @@ struct NotificationHistoryItem { } extension NotificationHistoryItem { + var logValue: String? { + switch appPath { + case .shop: + return "주변상점" + case .dining: + return "식단" + case .keyword: + return "키워드알림" + case .chat: + return "분실물 채팅" + case .callvan: + return "콜밴팟" + case .callvanChat: + return "콜밴팟 채팅" + default: + return nil + } + } + init?(from record: NotificationHistoryRecord) { guard let icon = NotificationHistoryItem.icon(for: record.category) else { return nil diff --git a/Koin/Presentation/Home/Notification/NotificationViewController.swift b/Koin/Presentation/Home/Notification/NotificationViewController.swift index 135430e8..785ae8f8 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewController.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewController.swift @@ -83,7 +83,7 @@ private extension NotificationViewController { notificationTableView.tapNotificationPublisher .sink { [weak self] item in - self?.inputSubject.send(.markAsRead(id: item.id)) + self?.inputSubject.send(.selectNotification(id: id)) self?.makeLogEvent(notification: item) self?.handleNavigation(item) } @@ -91,27 +91,6 @@ private extension NotificationViewController { } } -extension NotificationViewController { - private func makeLogEvent(notification: NotificationItem) { - let logValue: String - switch notification.appPath { - case .shop: - logValue = "주변상점" - case .dining: - logValue = "식단" - case .keyword: - logValue = "키워드알림" - case .chat: - logValue = "분실물 채팅" - case .callvan: - logValue = "콜밴팟" - case .callvanChat: - logValue = "콜밴팟 채팅" - } - inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.notificationList, .click, logValue)) - } -} - extension NotificationViewController { private func updateStateViews(isEmpty: Bool) { loadingIndicator.stopAnimating() diff --git a/Koin/Presentation/Home/Notification/NotificationViewModel.swift b/Koin/Presentation/Home/Notification/NotificationViewModel.swift index e2551809..b74d2b61 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewModel.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewModel.swift @@ -13,15 +13,16 @@ final class NotificationViewModel: ViewModelProtocol { enum Input { case viewDidLoad case reload + case selectNotification(id: String) case deleteNotification(id: String) case deleteAllNotifications - case markAsRead(id: String) case markAllAsRead case logEvent(EventLabelType, EventParameter.EventCategory, Any) } enum Output { case updateNotifications([NotificationHistoryItem]) + case selectedNotification(NotificationHistoryItem) case showToast(String) } @@ -56,12 +57,12 @@ final class NotificationViewModel: ViewModelProtocol { switch input { case .viewDidLoad, .reload: self?.loadNotifications() + case .selectNotification(let id): + self?.selectNotification(id: id) case .deleteNotification(let id): self?.deleteNotification(id: id) case .deleteAllNotifications: self?.deleteAllNotifications() - case .markAsRead(let id): - self?.markAsRead(id: id) case .markAllAsRead: self?.markAllAsRead() case let .logEvent(label, category, value): @@ -88,6 +89,13 @@ private extension NotificationViewModel { } } } + if let logValue = notification.logValue { + makeLogAnalyticsEvent( + label: EventParameter.EventLabel.Campus.notificationList, + category: .click, + value: logValue + ) + } private func deleteNotification(id: String) { Task { From af38e7791048d974188ceea2b8a2ec097f8dd2cb Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 20 Aug 2026 00:25:48 +0900 Subject: [PATCH 067/101] =?UTF-8?q?refactor:=20Notification=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationViewController.swift | 76 +++------- .../Notification/NotificationViewModel.swift | 50 ++++-- ...tionRowModel+NotificationHistoryItem.swift | 19 +++ .../Models/NotificationRowModel.swift | 15 ++ .../Views/NotificationEmptyView.swift} | 6 +- .../Views/NotificationListView.swift | 142 ++++++++++++++++++ .../NotificationPopUpViewController.swift | 4 +- .../NotificationFooterView.swift | 1 + .../NotificationTableView.swift | 11 +- .../NotificationTableViewCell.swift | 2 +- koin.xcodeproj/project.pbxproj | 58 ++++++- 11 files changed, 299 insertions(+), 85 deletions(-) create mode 100644 Koin/Presentation/Home/Notification/Support/NotificationRowModel+NotificationHistoryItem.swift create mode 100644 Koin/Presentation/Shared/Notification/Models/NotificationRowModel.swift rename Koin/Presentation/{Home/Notification/Subviews/NotificationEmptyBackgroundView.swift => Shared/Notification/Views/NotificationEmptyView.swift} (92%) create mode 100644 Koin/Presentation/Shared/Notification/Views/NotificationListView.swift rename Koin/Presentation/{Home/Notification/Subviews => Shared/Notification/Views}/NotificationPopUpViewController.swift (98%) rename Koin/Presentation/{Home/Notification/Subviews => Shared/Notification/Views}/NotificationTableView/NotificationFooterView.swift (98%) rename Koin/Presentation/{Home/Notification/Subviews => Shared/Notification/Views}/NotificationTableView/NotificationTableView.swift (95%) rename Koin/Presentation/{Home/Notification/Subviews => Shared/Notification/Views}/NotificationTableView/NotificationTableViewCell.swift (98%) diff --git a/Koin/Presentation/Home/Notification/NotificationViewController.swift b/Koin/Presentation/Home/Notification/NotificationViewController.swift index 785ae8f8..f745f104 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewController.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewController.swift @@ -8,7 +8,6 @@ import UIKit import Combine import SnapKit -import Then final class NotificationViewController: UIViewController { @@ -18,12 +17,7 @@ final class NotificationViewController: UIViewController { private var subscriptions = Set() // MARK: - UI Components - private let notificationTableView = NotificationTableView() - private let refreshControl = UIRefreshControl() - - private let loadingIndicator = UIActivityIndicatorView(style: .medium).then { - $0.hidesWhenStopped = true - } + private let notificationListView = NotificationListView() // MARK: - Initialization init(viewModel: NotificationViewModel) { @@ -39,10 +33,9 @@ final class NotificationViewController: UIViewController { super.viewDidLoad() configureView() configureNavigationBar() - setAddTargets() bind() + notificationListView.startLoading() inputSubject.send(.viewDidLoad) - loadingIndicator.startAnimating() title = "알림" } @@ -63,53 +56,42 @@ private extension NotificationViewController { switch event { case .updateNotifications(let notifications): - self.notificationTableView.update(notifications: notifications) - self.updateStateViews(isEmpty: notifications.isEmpty) + notificationListView.update(items: notifications.map { NotificationRowModel(from: $0) }) + case .selectedNotification(let notification): + handleNavigation(notification) case .showToast(let message): showToastMessage(message: message) } } .store(in: &subscriptions) - notificationTableView.deletePublisher + notificationListView.deletePublisher .sink { [weak self] id in guard let self else { return } self.inputSubject.send(.deleteNotification(id: id)) - self.updateStateViews(isEmpty: self.notificationTableView.isEmpty) self.showToastMessage(message: "알림이 삭제되었습니다.") self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.notificationListDelete, .click, "알림 삭제")) } .store(in: &subscriptions) - notificationTableView.tapNotificationPublisher - .sink { [weak self] item in + notificationListView.itemTappedPublisher + .sink { [weak self] id in self?.inputSubject.send(.selectNotification(id: id)) - self?.makeLogEvent(notification: item) - self?.handleNavigation(item) } .store(in: &subscriptions) - } -} -extension NotificationViewController { - private func updateStateViews(isEmpty: Bool) { - loadingIndicator.stopAnimating() - refreshControl.endRefreshing() - - UIView.animate( - withDuration: 0.2, - delay: 0, - options: [.curveEaseInOut, .beginFromCurrentState] - ) { [weak self] in - self?.notificationTableView.backgroundView?.alpha = isEmpty ? 1 : 0 - } + notificationListView.refreshPublisher + .sink { [weak self] in + self?.inputSubject.send(.reload) + } + .store(in: &subscriptions) } } // MARK: - Navigation extension NotificationViewController { - private func handleNavigation(_ item: NotificationItem) { + private func handleNavigation(_ item: NotificationHistoryItem) { guard let uri: String = item.uri, let parsedQuery = parseQuery(uri: uri) else { return @@ -320,21 +302,16 @@ private extension NotificationViewController { showPopUpView() } - @objc func didPullToRefresh() { - inputSubject.send(.reload) - } - private func showPopUpView() { let popUpViewController = NotificationPopUpViewController( markAllAsRead: { [weak self] in self?.inputSubject.send(.markAllAsRead) - self?.notificationTableView.markAllAsRead() + self?.notificationListView.markAllAsRead() self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.notificationListReadAll, .click, "모두 읽음으로 표시")) }, deleteAll: { [weak self] in self?.inputSubject.send(.deleteAllNotifications) - self?.notificationTableView.deleteAll() - self?.updateStateViews(isEmpty: true) + self?.notificationListView.deleteAll() self?.showToastMessage(message: "알림이 삭제되었습니다.") self?.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.notificationListDeleteAll, .click, "알림 전체 삭제")) } @@ -350,11 +327,6 @@ private extension NotificationViewController { // MARK: - Configure private extension NotificationViewController { - - private func setAddTargets() { - refreshControl.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged) - } - private func configureNavigationBar() { let rightBarButtonItem = UIBarButtonItem( @@ -374,28 +346,16 @@ private extension NotificationViewController { private func setUpStyles() { view.backgroundColor = UIColor.ColorSystem.Neutral.gray0 - - notificationTableView.refreshControl = refreshControl - - notificationTableView.backgroundView = NotificationEmptyBackgroundView().then { - $0.alpha = 0 - } } private func setUpLayouts() { - [notificationTableView, loadingIndicator].forEach { - view.addSubview($0) - } + view.addSubview(notificationListView) } private func setUpConstraints() { - notificationTableView.snp.makeConstraints { + notificationListView.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide.snp.top) $0.leading.trailing.bottom.equalToSuperview() } - - loadingIndicator.snp.makeConstraints { - $0.center.equalToSuperview() - } } } diff --git a/Koin/Presentation/Home/Notification/NotificationViewModel.swift b/Koin/Presentation/Home/Notification/NotificationViewModel.swift index b74d2b61..d8063747 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewModel.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewModel.swift @@ -33,6 +33,7 @@ final class NotificationViewModel: ViewModelProtocol { private let updateNotificationHistoryUseCase: UpdateNotificationHistoryUseCase private let logAnalyticsEventUseCase: LogAnalyticsEventUseCase private let outputSubject = PassthroughSubject() + private var notificationHistoryItems: [NotificationHistoryItem] = [] private var subscriptions = Set() // MARK: - Initializer @@ -83,41 +84,72 @@ private extension NotificationViewModel { Task { do { let notifications = try await fetchNotificationHistoryUseCase.execute() + self.notificationHistoryItems = notifications outputSubject.send(.updateNotifications(notifications)) } catch { outputSubject.send(.showToast(error.localizedDescription)) } } } - if let logValue = notification.logValue { - makeLogAnalyticsEvent( - label: EventParameter.EventLabel.Campus.notificationList, - category: .click, - value: logValue - ) + + private func selectNotification(id: String) { + guard let notification = notificationHistoryItems.first(where: { $0.id == id }), + let logValue = notification.logValue else { + return } + markAsRead(id: id) + + outputSubject.send(.selectedNotification(notification)) + + makeLogAnalyticsEvent( + label: EventParameter.EventLabel.Campus.notificationList, + category: .click, + value: logValue + ) + } private func deleteNotification(id: String) { + notificationHistoryItems.removeAll { $0.id == id } Task { - try? await deleteNotificationHistoryUseCase.delete(id: id) + do { + try await deleteNotificationHistoryUseCase.delete(id: id) + } catch { + outputSubject.send(.showToast(error.localizedDescription)) + } } } private func deleteAllNotifications() { + notificationHistoryItems.removeAll() Task { - try? await deleteNotificationHistoryUseCase.deleteAll() + do { + try await deleteNotificationHistoryUseCase.deleteAll() + } catch { + outputSubject.send(.showToast(error.localizedDescription)) + } } } private func markAsRead(id: String) { + if let index = notificationHistoryItems.firstIndex(where: { $0.id == id }) { + notificationHistoryItems[index].isRead = true + } + Task { try? await updateNotificationHistoryUseCase.markAsRead(id: id) } } private func markAllAsRead() { + for index in notificationHistoryItems.indices { + notificationHistoryItems[index].isRead = true + } Task { - try? await updateNotificationHistoryUseCase.markAllAsRead() + do { + try await updateNotificationHistoryUseCase.markAllAsRead() + } catch { + outputSubject.send(.showToast(error.localizedDescription)) + } } } diff --git a/Koin/Presentation/Home/Notification/Support/NotificationRowModel+NotificationHistoryItem.swift b/Koin/Presentation/Home/Notification/Support/NotificationRowModel+NotificationHistoryItem.swift new file mode 100644 index 00000000..26b2018b --- /dev/null +++ b/Koin/Presentation/Home/Notification/Support/NotificationRowModel+NotificationHistoryItem.swift @@ -0,0 +1,19 @@ +// +// NotificationRowModel+NotificationHistoryItem.swift +// koin +// +// Created by 홍기정 on 8/19/26. +// + +extension NotificationRowModel { + init(from item: NotificationHistoryItem) { + self.init( + id: item.id, + isRead: item.isRead, + icon: item.icon, + title: item.title, + content: item.content, + dateText: item.dateText + ) + } +} diff --git a/Koin/Presentation/Shared/Notification/Models/NotificationRowModel.swift b/Koin/Presentation/Shared/Notification/Models/NotificationRowModel.swift new file mode 100644 index 00000000..90ca3160 --- /dev/null +++ b/Koin/Presentation/Shared/Notification/Models/NotificationRowModel.swift @@ -0,0 +1,15 @@ +// +// NotificationRowModel.swift +// koin +// +// Created by 홍기정 on 8/19/26. +// + +struct NotificationRowModel { + let id: String + var isRead: Bool + let icon: ImageAsset + let title: String + let content: String + let dateText: String +} diff --git a/Koin/Presentation/Home/Notification/Subviews/NotificationEmptyBackgroundView.swift b/Koin/Presentation/Shared/Notification/Views/NotificationEmptyView.swift similarity index 92% rename from Koin/Presentation/Home/Notification/Subviews/NotificationEmptyBackgroundView.swift rename to Koin/Presentation/Shared/Notification/Views/NotificationEmptyView.swift index f0c2525f..934ea7b9 100644 --- a/Koin/Presentation/Home/Notification/Subviews/NotificationEmptyBackgroundView.swift +++ b/Koin/Presentation/Shared/Notification/Views/NotificationEmptyView.swift @@ -1,5 +1,5 @@ // -// NotificationEmptyBackgroundView.swift +// NotificationEmptyView.swift // koin // // Created by 홍기정 on 6/3/26. @@ -9,7 +9,7 @@ import UIKit import SnapKit import Then -final class NotificationEmptyBackgroundView: UIView { +final class NotificationEmptyView: UIView { // MARK: - UI Components private let layoutGuide = UILayoutGuide() @@ -27,7 +27,7 @@ final class NotificationEmptyBackgroundView: UIView { } } -extension NotificationEmptyBackgroundView { +extension NotificationEmptyView { private func configureView() { setUpStyles() diff --git a/Koin/Presentation/Shared/Notification/Views/NotificationListView.swift b/Koin/Presentation/Shared/Notification/Views/NotificationListView.swift new file mode 100644 index 00000000..c29ababb --- /dev/null +++ b/Koin/Presentation/Shared/Notification/Views/NotificationListView.swift @@ -0,0 +1,142 @@ +// +// NotificationListView.swift +// koin +// +// Created by 홍기정 on 8/19/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +final class NotificationListView: UIView { + + // MARK: - Properties + let refreshPublisher = PassthroughSubject() + let itemTappedPublisher = PassthroughSubject() + let deletePublisher = PassthroughSubject() + private var subscriptions = Set() + + // MARK: - UI Components + private let tableView = NotificationTableView() + private let refreshControl = UIRefreshControl() + private let loadingIndicator = UIActivityIndicatorView(style: .medium) + private let emptyView = NotificationEmptyView() + + // MARK: - Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureView() + bind() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + func startLoading() { + loadingIndicator.startAnimating() + } + + func update(items: [NotificationRowModel]) { + tableView.update(notifications: items) + updateStateViews(isEmpty: items.isEmpty) + } + + func markAllAsRead() { + tableView.markAllAsRead() + } + + func deleteAll() { + tableView.deleteAll() + updateStateViews(isEmpty: true) + } +} + +// MARK: - Bind + +private extension NotificationListView { + private func bind() { + tableView.tapNotificationPublisher + .sink { [weak self] id in + self?.itemTappedPublisher.send(id) + } + .store(in: &subscriptions) + + tableView.deletePublisher + .sink { [weak self] id in + guard let self else { return } + updateStateViews(isEmpty: tableView.isEmpty) + deletePublisher.send(id) + } + .store(in: &subscriptions) + } +} + +// MARK: - State + +private extension NotificationListView { + private func updateStateViews(isEmpty: Bool) { + loadingIndicator.stopAnimating() + refreshControl.endRefreshing() + + UIView.animate( + withDuration: 0.2, + delay: 0, + options: [.curveEaseInOut, .beginFromCurrentState] + ) { [weak self] in + self?.tableView.backgroundView?.alpha = isEmpty ? 1 : 0 + } + } +} + +// MARK: - Configure + +private extension NotificationListView { + private func configureView() { + setUpAddTargets() + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + backgroundColor = .appColor(.neutral0) + + loadingIndicator.do { + $0.hidesWhenStopped = true + } + + tableView.do { + $0.refreshControl = refreshControl + $0.backgroundView = emptyView + } + + emptyView.do { + $0.alpha = 0 + } + } + + private func setUpLayouts() { + [tableView, loadingIndicator].forEach { + addSubview($0) + } + } + private func setUpConstraints() { + tableView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + loadingIndicator.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + private func setUpAddTargets() { + refreshControl.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged) + } + + @objc private func didPullToRefresh() { + refreshPublisher.send() + } +} diff --git a/Koin/Presentation/Home/Notification/Subviews/NotificationPopUpViewController.swift b/Koin/Presentation/Shared/Notification/Views/NotificationPopUpViewController.swift similarity index 98% rename from Koin/Presentation/Home/Notification/Subviews/NotificationPopUpViewController.swift rename to Koin/Presentation/Shared/Notification/Views/NotificationPopUpViewController.swift index 05f6bc1c..44f14c71 100644 --- a/Koin/Presentation/Home/Notification/Subviews/NotificationPopUpViewController.swift +++ b/Koin/Presentation/Shared/Notification/Views/NotificationPopUpViewController.swift @@ -18,8 +18,8 @@ final class NotificationPopUpViewController: UIViewController { private let deleteAllButton = UIButton() // MARK: - Properties - @objc private let markAllAsRead: ()->Void - @objc private let deleteAll: ()->Void + private let markAllAsRead: ()->Void + private let deleteAll: ()->Void private var minimizedTransform: CGAffineTransform { let scale = 0.4 diff --git a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationFooterView.swift b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationFooterView.swift similarity index 98% rename from Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationFooterView.swift rename to Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationFooterView.swift index 6b46ab2d..a6ac8bed 100644 --- a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationFooterView.swift +++ b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationFooterView.swift @@ -11,6 +11,7 @@ import Then final class NotificationFooterView: UIView { + // MARK: - UI Components private let label = UILabel() override init(frame: CGRect) { diff --git a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableView.swift b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableView.swift similarity index 95% rename from Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableView.swift rename to Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableView.swift index 28541a1d..e08a3c77 100644 --- a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableView.swift +++ b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableView.swift @@ -20,13 +20,13 @@ final class NotificationTableView: UITableView { // MARK: - Publisher let deletePublisher = PassthroughSubject() - let tapNotificationPublisher = PassthroughSubject() + let tapNotificationPublisher = PassthroughSubject() // MARK: - UI Components private let realFooterView = NotificationFooterView() // MARK: - Properties - private var notifications: [NotificationItem] = [] + private var notifications: [NotificationRowModel] = [] var isEmpty: Bool { notifications.isEmpty } @@ -43,7 +43,7 @@ final class NotificationTableView: UITableView { } // MARK: - Public - func update(notifications: [NotificationItem]) { + func update(notifications: [NotificationRowModel]) { performBatchUpdates { self.notifications = notifications recalculateFooterHeightIfNeeded() @@ -173,8 +173,9 @@ extension NotificationTableView: UITableViewDelegate { guard notifications.indices.contains(indexPath.row) else { return } - tapNotificationPublisher.send(notifications[indexPath.row]) - didSelectNotification(id: notifications[indexPath.row].id) + let id = notifications[indexPath.row].id + tapNotificationPublisher.send(id) + didSelectNotification(id: id) } func tableView( diff --git a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableViewCell.swift b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableViewCell.swift similarity index 98% rename from Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableViewCell.swift rename to Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableViewCell.swift index ca19ef7c..fea8fb9e 100644 --- a/Koin/Presentation/Home/Notification/Subviews/NotificationTableView/NotificationTableViewCell.swift +++ b/Koin/Presentation/Shared/Notification/Views/NotificationTableView/NotificationTableViewCell.swift @@ -27,7 +27,7 @@ final class NotificationTableViewCell: UITableViewCell { } // MARK: - Public - func configure(item: NotificationItem) { + func configure(item: NotificationRowModel) { iconImageView.image = .appImage(asset: item.icon)?.withRenderingMode(.alwaysTemplate) titleLabel.text = item.title contentLabel.text = item.content diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 14f123bc..6731f5bf 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -254,7 +254,7 @@ 7C8A000F2FD200000011D338 /* DiningIndicatorChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A00102FD200000011D338 /* DiningIndicatorChip.swift */; }; 7C8A941E2FD0A94F00DEA6F5 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A941D2FD0A94F00DEA6F5 /* NotificationViewModel.swift */; }; 7C8A941F2FD0A94F00DEA6F5 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A941C2FD0A94F00DEA6F5 /* NotificationViewController.swift */; }; - 7C8A94222FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift */; }; + 7C8A94222FD0A96400DEA6F5 /* NotificationEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyView.swift */; }; 7C8A94272FD0A97400DEA6F5 /* NotificationFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A94242FD0A97400DEA6F5 /* NotificationFooterView.swift */; }; 7C8A94282FD0A97400DEA6F5 /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A94262FD0A97400DEA6F5 /* NotificationTableViewCell.swift */; }; 7C8A94292FD0A97400DEA6F5 /* NotificationTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8A94252FD0A97400DEA6F5 /* NotificationTableView.swift */; }; @@ -763,6 +763,9 @@ B68D2CEB2EA50C1E00F3B479 /* UIViewController+Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68D2CEA2EA50C1E00F3B479 /* UIViewController+Toast.swift */; }; B6A0D62A2EEB01E600A19521 /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = B6A0D6292EEB01E600A19521 /* Debug.xcconfig */; }; B6A0D62C2EEB01F100A19521 /* Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = B6A0D62B2EEB01F100A19521 /* Release.xcconfig */; }; + D19A00013000000000000002 /* NotificationRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000001 /* NotificationRowModel.swift */; }; + D19A00013000000000000004 /* NotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000003 /* NotificationListView.swift */; }; + D19A00013000000000000006 /* NotificationRowModel+NotificationHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */; }; D2009F762C17270F00211D1B /* CalendarDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2009F752C17270F00211D1B /* CalendarDate.swift */; }; D2009F7F2C1784DA00211D1B /* MockAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2009F7E2C1784DA00211D1B /* MockAnalyticsService.swift */; }; D2078BF62BC4CCEB00A39861 /* EventDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2078BF52BC4CCEB00A39861 /* EventDto.swift */; }; @@ -1272,7 +1275,7 @@ 7C8A00102FD200000011D338 /* DiningIndicatorChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningIndicatorChip.swift; sourceTree = ""; }; 7C8A941C2FD0A94F00DEA6F5 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; 7C8A941D2FD0A94F00DEA6F5 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = ""; }; - 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationEmptyBackgroundView.swift; sourceTree = ""; }; + 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationEmptyView.swift; sourceTree = ""; }; 7C8A94242FD0A97400DEA6F5 /* NotificationFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFooterView.swift; sourceTree = ""; }; 7C8A94252FD0A97400DEA6F5 /* NotificationTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableView.swift; sourceTree = ""; }; 7C8A94262FD0A97400DEA6F5 /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = ""; }; @@ -1768,6 +1771,9 @@ B68D2CEA2EA50C1E00F3B479 /* UIViewController+Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Toast.swift"; sourceTree = ""; }; B6A0D6292EEB01E600A19521 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B6A0D62B2EEB01F100A19521 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + D19A00013000000000000001 /* NotificationRowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowModel.swift; sourceTree = ""; }; + D19A00013000000000000003 /* NotificationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListView.swift; sourceTree = ""; }; + D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationRowModel+NotificationHistoryItem.swift"; sourceTree = ""; }; D2009F752C17270F00211D1B /* CalendarDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDate.swift; sourceTree = ""; }; D2009F7E2C1784DA00211D1B /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = ""; }; D2078BF52BC4CCEB00A39861 /* EventDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDto.swift; sourceTree = ""; }; @@ -2379,7 +2385,7 @@ 7C457B282FCE1BA80011D338 /* Notification */ = { isa = PBXGroup; children = ( - 7C8A94202FD0A95100DEA6F5 /* Subviews */, + D19A0001300000000000000A /* Support */, 7C8A941C2FD0A94F00DEA6F5 /* NotificationViewController.swift */, 7C8A941D2FD0A94F00DEA6F5 /* NotificationViewModel.swift */, ); @@ -2792,14 +2798,15 @@ path = Home; sourceTree = ""; }; - 7C8A94202FD0A95100DEA6F5 /* Subviews */ = { + 7C8A94202FD0A95100DEA6F5 /* Views */ = { isa = PBXGroup; children = ( 7C8A94232FD0A96700DEA6F5 /* NotificationTableView */, - 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift */, + 7C8A94212FD0A96400DEA6F5 /* NotificationEmptyView.swift */, + D19A00013000000000000003 /* NotificationListView.swift */, 7C7F56B32FFB6A6400847151 /* NotificationPopUpViewController.swift */, ); - path = Subviews; + path = Views; sourceTree = ""; }; 7C8A94232FD0A96700DEA6F5 /* NotificationTableView */ = { @@ -4307,6 +4314,7 @@ isa = PBXGroup; children = ( 7C86751030062FF3003CB942 /* Core */, + D19A00013000000000000007 /* Shared */, 7C457B2A2FCE1BA80011D338 /* Home */, 7C016541300A9FF80013DD7B /* Department */, 7CED8EF42EF2D1F900457128 /* LostItem */, @@ -4654,6 +4662,39 @@ path = Configuration; sourceTree = ""; }; + D19A00013000000000000007 /* Shared */ = { + isa = PBXGroup; + children = ( + D19A00013000000000000008 /* Notification */, + ); + path = Shared; + sourceTree = ""; + }; + D19A00013000000000000008 /* Notification */ = { + isa = PBXGroup; + children = ( + D19A00013000000000000009 /* Models */, + 7C8A94202FD0A95100DEA6F5 /* Views */, + ); + path = Notification; + sourceTree = ""; + }; + D19A00013000000000000009 /* Models */ = { + isa = PBXGroup; + children = ( + D19A00013000000000000001 /* NotificationRowModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + D19A0001300000000000000A /* Support */ = { + isa = PBXGroup; + children = ( + D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */, + ); + path = Support; + sourceTree = ""; + }; D2009F7D2C1784C000211D1B /* MockService */ = { isa = PBXGroup; children = ( @@ -6441,6 +6482,9 @@ 7C8BFCE32FD04B8000963679 /* HomeHeader.swift in Sources */, 7CF77B4430027321003642C1 /* TimeTableView.swift in Sources */, 7C8BFCE42FD04B8000963679 /* NotificationHistoryItem.swift in Sources */, + D19A00013000000000000002 /* NotificationRowModel.swift in Sources */, + D19A00013000000000000004 /* NotificationListView.swift in Sources */, + D19A00013000000000000006 /* NotificationRowModel+NotificationHistoryItem.swift in Sources */, 7C8BFCE52FD04B8000963679 /* CategoryModels.swift in Sources */, 7C8BFCF12FD04B800963679 /* HomeCounts.swift in Sources */, D2908D262BB8D11A0008F908 /* SemesterDto.swift in Sources */, @@ -6514,7 +6558,7 @@ 7C333EA53030429B009D2B89 /* ModalButtonView.swift in Sources */, D22C54712C6A2A60000826DA /* FetchShopReviewUseCase.swift in Sources */, 7C6403D3300BE8A100A9589F /* DepartmentEmptyView.swift in Sources */, - 7C8A94222FD0A96400DEA6F5 /* NotificationEmptyBackgroundView.swift in Sources */, + 7C8A94222FD0A96400DEA6F5 /* NotificationEmptyView.swift in Sources */, 8354BE522C7A0566009D4D7A /* CoreDataService.swift in Sources */, D2D7C0D52C28298500C85A85 /* FetchLandListUseCase.swift in Sources */, 7C17EEB22EBF7175008BCA89 /* FetchSearchShopUseCase.swift in Sources */, From 1326e9190dd1c131f090d412ed4bb1704e7f2332 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 20 Aug 2026 01:10:30 +0900 Subject: [PATCH 068/101] =?UTF-8?q?refactor:=20CallVanChat=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CallVanChatViewController.swift | 168 ++++-------------- .../Support/ChatListModel+CallVanChat.swift | 31 ++++ .../Shared/Chat/Models/ChatListModel.swift | 11 ++ .../Chat/Models/ChatMessageRowModel.swift | 28 +++ .../Shared/Chat/Views/ChatInputView.swift | 151 ++++++++++++++++ .../Shared/Chat/Views/ChatListView.swift | 117 ++++++++++++ .../ChatTableView/ChatDateHeaderView.swift} | 6 +- .../Views/ChatTableView/ChatLeftCell.swift} | 37 ++-- .../Views/ChatTableView/ChatRightCell.swift} | 37 ++-- .../Views/ChatTableView/ChatTableView.swift} | 39 ++-- koin.xcodeproj/project.pbxproj | 98 +++++++--- 11 files changed, 505 insertions(+), 218 deletions(-) create mode 100644 Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift create mode 100644 Koin/Presentation/Shared/Chat/Models/ChatListModel.swift create mode 100644 Koin/Presentation/Shared/Chat/Models/ChatMessageRowModel.swift create mode 100644 Koin/Presentation/Shared/Chat/Views/ChatInputView.swift create mode 100644 Koin/Presentation/Shared/Chat/Views/ChatListView.swift rename Koin/Presentation/{CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatDateHeaderView.swift => Shared/Chat/Views/ChatTableView/ChatDateHeaderView.swift} (91%) rename Koin/Presentation/{CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatLeftCell.swift => Shared/Chat/Views/ChatTableView/ChatLeftCell.swift} (89%) rename Koin/Presentation/{CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatRightCell.swift => Shared/Chat/Views/ChatTableView/ChatRightCell.swift} (87%) rename Koin/Presentation/{CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatTableView.swift => Shared/Chat/Views/ChatTableView/ChatTableView.swift} (67%) diff --git a/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift b/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift index 0b5e974e..3dc7395d 100644 --- a/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift +++ b/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift @@ -17,7 +17,6 @@ final class CallVanChatViewController: UIViewController { private let inputSubject = PassthroughSubject() private let viewModel: CallVanChatViewModel private var subscriptions: Set = [] - private let textViewPlaceHolder = "메시지 보내기" // MARK: - TitleView private lazy var titleLabel = UILabel() @@ -27,11 +26,7 @@ final class CallVanChatViewController: UIViewController { private lazy var titleView = UIView() // MARK: - UI Components - private let callVanChatTableView = CallVanChatTableView() - private let wrapperView = UIView() - private let sendImageButton = UIButton() - private let messageTextView = UITextView() - private let sendMessageButton = UIButton() + private let chatListView = ChatListView() // MARK: - Initializer init(viewModel: CallVanChatViewModel) { @@ -46,10 +41,7 @@ final class CallVanChatViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureView() - setDelegate() - setAddTargets() bind() - setGesture() configureNavigationBar(style: .empty) inputSubject.send(.viewDidLoad) } @@ -71,55 +63,47 @@ final class CallVanChatViewController: UIViewController { case let .showToast(message): showToastMessage(message: message) case let .update(callVanChat): - callVanChatTableView.configure(callVanChat: callVanChat) + chatListView.update(model: ChatListModel(from: callVanChat)) case let .updateData(callVanData): configureNavigationBar(callVanData) } }.store(in: &subscriptions) - - callVanChatTableView.imageTappedPublisher.receive(on: DispatchQueue.main).sink { [weak self] imageUrl in - let zoomedImageViewController = ZoomedImageViewControllerB(shouldShowTitle: false) - zoomedImageViewController.configure(url: imageUrl) - zoomedImageViewController.modalTransitionStyle = .crossDissolve - zoomedImageViewController.modalPresentationStyle = .overFullScreen - self?.present(zoomedImageViewController, animated: true) - }.store(in: &subscriptions) - } -} -extension CallVanChatViewController { - - private func setGesture() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAround)) - tapGesture.cancelsTouchesInView = false - callVanChatTableView.addGestureRecognizer(tapGesture) - } - - private func setAddTargets() { - sendImageButton.addTarget(self, action: #selector(sendImageButtonTapped), for: .touchUpInside) - sendMessageButton.addTarget(self, action: #selector(sendMessageButtonTapped), for: .touchUpInside) - } - - @objc private func didTapAround() { - dismissKeyboard() - } - - @objc private func sendMessageButtonTapped() { - guard messageTextView.textColor == UIColor.appColor(.neutral800) else { - return - } - let text = messageTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) - if !text.isEmpty { - inputSubject.send(.sendMessage(text)) - inputSubject.send(.logEvent(label: EventParameter.EventLabel.Campus.callvanChatSend, category: .click, value: "")) - messageTextView.text = "" - } + chatListView.messageSendPublisher + .sink { [weak self] message in + self?.inputSubject.send(.sendMessage(message)) + self?.inputSubject.send( + .logEvent( + label: EventParameter.EventLabel.Campus.callvanChatSend, + category: .click, + value: "" + ) + ) + } + .store(in: &subscriptions) + + chatListView.imageSendTappedPublisher + .sink { [weak self] in + self?.presentImagePicker() + } + .store(in: &subscriptions) + + chatListView.imageTappedPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] imageUrl in + let zoomedImageViewController = ZoomedImageViewControllerB(shouldShowTitle: false) + zoomedImageViewController.configure(url: imageUrl) + zoomedImageViewController.modalTransitionStyle = .crossDissolve + zoomedImageViewController.modalPresentationStyle = .overFullScreen + self?.present(zoomedImageViewController, animated: true) + } + .store(in: &subscriptions) } } extension CallVanChatViewController: PHPickerViewControllerDelegate { - @objc private func sendImageButtonTapped() { + private func presentImagePicker() { var configuration = PHPickerConfiguration() configuration.filter = .images configuration.selectionLimit = 1 @@ -153,27 +137,6 @@ extension CallVanChatViewController: PHPickerViewControllerDelegate { } } -extension CallVanChatViewController: UITextViewDelegate { - - private func setDelegate() { - messageTextView.delegate = self - } - - func textViewDidBeginEditing(_ textView: UITextView) { - if textView.textColor == UIColor.appColor(.neutral500) { - textView.text = "" - textView.textColor = UIColor.appColor(.neutral800) - } - } - - func textViewDidEndEditing(_ textView: UITextView) { - if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { - textView.text = textViewPlaceHolder - textView.textColor = UIColor.appColor(.neutral500) - } - } -} - extension CallVanChatViewController { private func configureNavigationBar(_ callVanData: CallVanData) { @@ -213,41 +176,6 @@ extension CallVanChatViewController { private func setUpStyles() { view.backgroundColor = UIColor.appColor(.neutral100) - - // MARK: - UI Components - callVanChatTableView.do { - $0.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi)) - $0.backgroundColor = .white - $0.separatorStyle = .none - $0.showsVerticalScrollIndicator = false - } - wrapperView.do { - $0.backgroundColor = UIColor.appColor(.neutral100) - } - sendImageButton.do { - $0.setImage(UIImage.appImage(asset: .callVanSendImage), for: .normal) - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - } - messageTextView.do { - let font = UIFont.appFont(.pretendardRegular, size: 12) - let height: CGFloat = 32 - let topBottomInset = (height - font.lineHeight) / 2 - - $0.isScrollEnabled = false - $0.font = font - $0.backgroundColor = UIColor.appColor(.neutral0) - $0.textContainerInset = UIEdgeInsets(top: topBottomInset, left: 16, bottom: topBottomInset, right: 16) - $0.layer.cornerRadius = 12 - $0.text = textViewPlaceHolder - $0.textColor = UIColor.appColor(.neutral500) - } - sendMessageButton.do { - $0.setImage(UIImage.appImage(asset: .callVanSendMessage), for: .normal) - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - } } private func setUpLayouts() { @@ -260,12 +188,7 @@ extension CallVanChatViewController { } // MARK: - UI Components - [sendImageButton, messageTextView, sendMessageButton].forEach { - wrapperView.addSubview($0) - } - [callVanChatTableView, wrapperView].forEach { - view.addSubview($0) - } + view.addSubview(chatListView) } private func setUpConstraints() { @@ -286,30 +209,9 @@ extension CallVanChatViewController { } // MARK: - UI Components - callVanChatTableView.snp.makeConstraints { + chatListView.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalTo(wrapperView.snp.top) - } - wrapperView.snp.makeConstraints { - $0.leading.trailing.equalToSuperview() - $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top) - } - sendImageButton.snp.makeConstraints { - $0.size.equalTo(32) - $0.top.equalTo(wrapperView).offset(8) - $0.leading.equalTo(wrapperView).offset(24) - } - sendMessageButton.snp.makeConstraints { - $0.size.equalTo(32) - $0.top.equalTo(wrapperView).offset(8) - $0.trailing.equalTo(wrapperView).offset(-24) - } - messageTextView.snp.makeConstraints { - $0.top.bottom.equalTo(wrapperView).inset(8) - $0.leading.equalTo(sendImageButton.snp.trailing).offset(8) - $0.trailing.equalTo(sendMessageButton.snp.leading).offset(-8) - $0.bottom.greaterThanOrEqualTo(sendImageButton) + $0.leading.trailing.bottom.equalToSuperview() } } } diff --git a/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift b/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift new file mode 100644 index 00000000..0181c2da --- /dev/null +++ b/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift @@ -0,0 +1,31 @@ +// +// ChatListModel+CallVanChat.swift +// koin +// +// Created by 홍기정 on 8/20/26. +// + +extension ChatListModel { + init(from chat: CallVanChat) { + self.init( + dates: chat.dates, + messages: chat.messages.map { messages in + messages.map { ChatMessageRowModel(from: $0) } + } + ) + } +} + +extension ChatMessageRowModel { + init(from message: CallVanChatMessage) { + self.init( + alignment: message.isMine ? .right : .left, + content: message.isImage ? .image(message.content) : .text(message.content), + senderNickname: message.senderNickname, + timeText: message.time, + showsProfile: message.showProfile, + isLeftUser: message.isLeftUser, + profileImage: message.profileImage + ) + } +} diff --git a/Koin/Presentation/Shared/Chat/Models/ChatListModel.swift b/Koin/Presentation/Shared/Chat/Models/ChatListModel.swift new file mode 100644 index 00000000..f94e9072 --- /dev/null +++ b/Koin/Presentation/Shared/Chat/Models/ChatListModel.swift @@ -0,0 +1,11 @@ +// +// ChatListModel.swift +// koin +// +// Created by 홍기정 on 8/20/26. +// + +struct ChatListModel { + let dates: [String] + let messages: [[ChatMessageRowModel]] +} diff --git a/Koin/Presentation/Shared/Chat/Models/ChatMessageRowModel.swift b/Koin/Presentation/Shared/Chat/Models/ChatMessageRowModel.swift new file mode 100644 index 00000000..ed35aaf3 --- /dev/null +++ b/Koin/Presentation/Shared/Chat/Models/ChatMessageRowModel.swift @@ -0,0 +1,28 @@ +// +// ChatMessageRowModel.swift +// koin +// +// Created by 홍기정 on 8/20/26. +// + +import UIKit + +struct ChatMessageRowModel { + let alignment: ChatMessageAlignment + let content: ChatMessageContent + let senderNickname: String + let timeText: String + let showsProfile: Bool + let isLeftUser: Bool + let profileImage: UIImage? +} + +enum ChatMessageAlignment { + case left + case right +} + +enum ChatMessageContent { + case text(String) + case image(String) +} diff --git a/Koin/Presentation/Shared/Chat/Views/ChatInputView.swift b/Koin/Presentation/Shared/Chat/Views/ChatInputView.swift new file mode 100644 index 00000000..471959cf --- /dev/null +++ b/Koin/Presentation/Shared/Chat/Views/ChatInputView.swift @@ -0,0 +1,151 @@ +// +// ChatInputView.swift +// koin +// +// Created by 홍기정 on 8/20/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +final class ChatInputView: UIView { + + // MARK: - Properties + let messageSendPublisher = PassthroughSubject() + let imageSendTappedPublisher = PassthroughSubject() + private let textViewPlaceHolder = "메시지 보내기" + + // MARK: - UI Components + private let sendImageButton = UIButton() + private let messageTextView = UITextView() + private let sendMessageButton = UIButton() + + // MARK: - Initializer + override init(frame: CGRect) { + super.init(frame: frame) + configureView() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UITextViewDelegate + +extension ChatInputView: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if textView.textColor == UIColor.appColor(.neutral500) { + textView.text = "" + textView.textColor = UIColor.appColor(.neutral800) + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + textView.text = textViewPlaceHolder + textView.textColor = UIColor.appColor(.neutral500) + } + } +} + +// MARK: - Actions + +private extension ChatInputView { + @objc private func sendImageButtonTapped() { + imageSendTappedPublisher.send() + } + + @objc private func sendMessageButtonTapped() { + guard messageTextView.textColor == UIColor.appColor(.neutral800) else { + return + } + + let text = messageTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { return } + + messageSendPublisher.send(text) + messageTextView.text = "" + } +} + +// MARK: - Configure + +private extension ChatInputView { + private func configureView() { + setAddTargets() + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setAddTargets() { + sendImageButton.addTarget(self, action: #selector(sendImageButtonTapped), for: .touchUpInside) + sendMessageButton.addTarget(self, action: #selector(sendMessageButtonTapped), for: .touchUpInside) + } + + private func setUpStyles() { + backgroundColor = UIColor.appColor(.neutral100) + + sendImageButton.do { + $0.setImage(UIImage.appImage(asset: .callVanSendImage), for: .normal) + $0.backgroundColor = UIColor.appColor(.neutral0) + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + } + + messageTextView.do { + let font = UIFont.appFont(.pretendardRegular, size: 12) + let height: CGFloat = 32 + let topBottomInset = (height - font.lineHeight) / 2 + + $0.delegate = self + $0.isScrollEnabled = false + $0.font = font + $0.backgroundColor = UIColor.appColor(.neutral0) + $0.textContainerInset = UIEdgeInsets( + top: topBottomInset, + left: 16, + bottom: topBottomInset, + right: 16 + ) + $0.layer.cornerRadius = 12 + $0.text = textViewPlaceHolder + $0.textColor = UIColor.appColor(.neutral500) + } + + sendMessageButton.do { + $0.setImage(UIImage.appImage(asset: .callVanSendMessage), for: .normal) + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + } + } + + private func setUpLayouts() { + [sendImageButton, messageTextView, sendMessageButton].forEach { + addSubview($0) + } + } + + private func setUpConstraints() { + sendImageButton.snp.makeConstraints { + $0.size.equalTo(32) + $0.top.equalToSuperview().offset(8) + $0.leading.equalToSuperview().offset(24) + } + + sendMessageButton.snp.makeConstraints { + $0.size.equalTo(32) + $0.top.equalToSuperview().offset(8) + $0.trailing.equalToSuperview().offset(-24) + } + + messageTextView.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(8) + $0.leading.equalTo(sendImageButton.snp.trailing).offset(8) + $0.trailing.equalTo(sendMessageButton.snp.leading).offset(-8) + $0.bottom.greaterThanOrEqualTo(sendImageButton) + } + } +} diff --git a/Koin/Presentation/Shared/Chat/Views/ChatListView.swift b/Koin/Presentation/Shared/Chat/Views/ChatListView.swift new file mode 100644 index 00000000..e733a150 --- /dev/null +++ b/Koin/Presentation/Shared/Chat/Views/ChatListView.swift @@ -0,0 +1,117 @@ +// +// ChatListView.swift +// koin +// +// Created by 홍기정 on 8/20/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +final class ChatListView: UIView { + + // MARK: - Properties + let messageSendPublisher = PassthroughSubject() + let imageSendTappedPublisher = PassthroughSubject() + let imageTappedPublisher = PassthroughSubject() + private var subscriptions = Set() + + // MARK: - UI Components + private let tableView = ChatTableView() + private let chatInputView = ChatInputView() + + // MARK: - Initializer + override init(frame: CGRect) { + super.init(frame: frame) + configureView() + bind() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + func update(model: ChatListModel) { + tableView.configure(model: model) + } +} + +// MARK: - Bind + +private extension ChatListView { + private func bind() { + tableView.imageTappedPublisher + .sink { [weak self] imageUrl in + self?.imageTappedPublisher.send(imageUrl) + } + .store(in: &subscriptions) + + chatInputView.messageSendPublisher + .sink { [weak self] message in + self?.messageSendPublisher.send(message) + } + .store(in: &subscriptions) + + chatInputView.imageSendTappedPublisher + .sink { [weak self] in + self?.imageSendTappedPublisher.send() + } + .store(in: &subscriptions) + } +} + +// MARK: - Actions + +private extension ChatListView { + private func setGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAround)) + tapGesture.cancelsTouchesInView = false + tableView.addGestureRecognizer(tapGesture) + } + + @objc private func didTapAround() { + endEditing(true) + } +} + +// MARK: - Configure + +private extension ChatListView { + private func configureView() { + setGesture() + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + backgroundColor = UIColor.appColor(.neutral100) + + tableView.do { + $0.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi)) + $0.backgroundColor = .white + $0.separatorStyle = .none + $0.showsVerticalScrollIndicator = false + } + } + + private func setUpLayouts() { + [tableView, chatInputView].forEach { + addSubview($0) + } + } + + private func setUpConstraints() { + tableView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.bottom.equalTo(chatInputView.snp.top) + } + + chatInputView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(keyboardLayoutGuide.snp.top) + } + } +} diff --git a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatDateHeaderView.swift b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatDateHeaderView.swift similarity index 91% rename from Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatDateHeaderView.swift rename to Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatDateHeaderView.swift index b828c047..d4e449d8 100644 --- a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatDateHeaderView.swift +++ b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatDateHeaderView.swift @@ -1,5 +1,5 @@ // -// CallVanChatDateHeaderView.swift +// ChatDateHeaderView.swift // koin // // Created by 홍기정 on 3/9/26. @@ -9,7 +9,7 @@ import UIKit import SnapKit import Then -final class CallVanChatDateHeaderView: UITableViewHeaderFooterView { +final class ChatDateHeaderView: UITableViewHeaderFooterView { // MARK: - UI Components private let dateView = UIView() @@ -30,7 +30,7 @@ final class CallVanChatDateHeaderView: UITableViewHeaderFooterView { } } -extension CallVanChatDateHeaderView { +extension ChatDateHeaderView { private func configureView() { setUpStyles() diff --git a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatLeftCell.swift b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatLeftCell.swift similarity index 89% rename from Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatLeftCell.swift rename to Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatLeftCell.swift index 8b761ae1..5a42889c 100644 --- a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatLeftCell.swift +++ b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatLeftCell.swift @@ -1,5 +1,5 @@ // -// CallVanChatLeftCell.swift +// ChatLeftCell.swift // koin // // Created by 홍기정 on 3/9/26. @@ -10,7 +10,7 @@ import Combine import SnapKit import Then -final class CallVanChatLeftCell: UITableViewCell { +final class ChatLeftCell: UITableViewCell { // MARK: - Properties let imageTappedPublisher = PassthroughSubject() @@ -55,35 +55,36 @@ final class CallVanChatLeftCell: UITableViewCell { } // MARK: - Public - func configure(message: CallVanChatMessage) { + func configure(message: ChatMessageRowModel) { // MARK: 프로필 - profileWrapperView.isHidden = !message.showProfile + profileWrapperView.isHidden = !message.showsProfile profileImageView.image = message.profileImage nicknameLabel.text = message.senderNickname leftUserLabel.isHidden = !message.isLeftUser // MARK: 이미지 - messageImageWrapperView.isHidden = !message.isImage - if message.isImage { - imageUrl = message.content - messageImageView.loadImageWithSpinner(from: message.content) - messageImageTimeLabel.text = message.time - } - - // MARK: 텍스트 - messageTextWrapperView.isHidden = message.isImage - if !message.isImage { + switch message.content { + case .image(let imageUrl): + messageImageWrapperView.isHidden = false + messageTextWrapperView.isHidden = true + self.imageUrl = imageUrl + messageImageView.loadImageWithSpinner(from: imageUrl) + messageImageTimeLabel.text = message.timeText + case .text(let text): + messageImageWrapperView.isHidden = true + messageTextWrapperView.isHidden = false + imageUrl = nil messageTextLabel.attributedText = NSAttributedString( - string: message.content, + string: text, attributes: messageTextLabelAttributes ) - messageTextTimeLabel.text = message.time + messageTextTimeLabel.text = message.timeText } } } -extension CallVanChatLeftCell { +extension ChatLeftCell { override func prepareForReuse() { super.prepareForReuse() messageImageView.image = nil @@ -106,7 +107,7 @@ extension CallVanChatLeftCell { } } -extension CallVanChatLeftCell { +extension ChatLeftCell { private func configureView() { setUpStyles() diff --git a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatRightCell.swift b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatRightCell.swift similarity index 87% rename from Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatRightCell.swift rename to Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatRightCell.swift index 0d52874c..71a0bb4e 100644 --- a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatRightCell.swift +++ b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatRightCell.swift @@ -1,5 +1,5 @@ // -// CallVanChatRightCell.swift +// ChatRightCell.swift // koin // // Created by 홍기정 on 3/9/26. @@ -10,7 +10,7 @@ import Combine import SnapKit import Then -final class CallVanChatRightCell: UITableViewCell { +final class ChatRightCell: UITableViewCell { // MARK: - Properties let imageTappedPublisher = PassthroughSubject() @@ -52,32 +52,33 @@ final class CallVanChatRightCell: UITableViewCell { } // MARK: - Public - func configure(message: CallVanChatMessage) { + func configure(message: ChatMessageRowModel) { // MARK: 프로필 - profileWrapperView.isHidden = !message.showProfile + profileWrapperView.isHidden = !message.showsProfile // MARK: 이미지 - messageImageWrapperView.isHidden = !message.isImage - if message.isImage { - imageUrl = message.content - messageImageView.loadImageWithSpinner(from: message.content) - messageImageTimeLabel.text = message.time - } - - // MARK: 텍스트 - messageTextWrapperView.isHidden = message.isImage - if !message.isImage { + switch message.content { + case .image(let imageUrl): + messageImageWrapperView.isHidden = false + messageTextWrapperView.isHidden = true + self.imageUrl = imageUrl + messageImageView.loadImageWithSpinner(from: imageUrl) + messageImageTimeLabel.text = message.timeText + case .text(let text): + messageImageWrapperView.isHidden = true + messageTextWrapperView.isHidden = false + imageUrl = nil messageTextLabel.attributedText = NSAttributedString( - string: message.content, + string: text, attributes: messageTextLabelAttributes ) - messageTextTimeLabel.text = message.time + messageTextTimeLabel.text = message.timeText } } } -extension CallVanChatRightCell { +extension ChatRightCell { override func prepareForReuse() { super.prepareForReuse() messageImageView.image = nil @@ -100,7 +101,7 @@ extension CallVanChatRightCell { } } -extension CallVanChatRightCell { +extension ChatRightCell { private func configureView() { setUpStyles() diff --git a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatTableView.swift b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatTableView.swift similarity index 67% rename from Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatTableView.swift rename to Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatTableView.swift index e68f5c8d..82686d8d 100644 --- a/Koin/Presentation/CallVan/CallVanChat/Subviews/CallVanChatTableView/CallVanChatTableView.swift +++ b/Koin/Presentation/Shared/Chat/Views/ChatTableView/ChatTableView.swift @@ -1,5 +1,5 @@ // -// CallVanChatTableView.swift +// ChatTableView.swift // koin // // Created by 홍기정 on 3/9/26. @@ -9,12 +9,12 @@ import UIKit import Combine import Then -final class CallVanChatTableView: UITableView { +final class ChatTableView: UITableView { // MARK: - Properties let imageTappedPublisher = PassthroughSubject() private var dates: [String] = [] - private var messages: [[CallVanChatMessage]] = [] + private var messages: [[ChatMessageRowModel]] = [] // MARK: - Initializer init() { @@ -26,14 +26,14 @@ final class CallVanChatTableView: UITableView { } // MARK: - Public - func configure(callVanChat: CallVanChat) { - self.dates = callVanChat.dates - self.messages = callVanChat.messages + func configure(model: ChatListModel) { + self.dates = model.dates + self.messages = model.messages reloadData() } } -extension CallVanChatTableView { +extension ChatTableView { private func commonInit() { allowsSelection = false @@ -45,16 +45,16 @@ extension CallVanChatTableView { keyboardDismissMode = .interactiveWithAccessory delegate = self dataSource = self - register(CallVanChatLeftCell.self, forCellReuseIdentifier: CallVanChatLeftCell.identifier) - register(CallVanChatRightCell.self, forCellReuseIdentifier: CallVanChatRightCell.identifier) - register(CallVanChatDateHeaderView.self, forHeaderFooterViewReuseIdentifier: CallVanChatDateHeaderView.identifier) + register(ChatLeftCell.self, forCellReuseIdentifier: ChatLeftCell.identifier) + register(ChatRightCell.self, forCellReuseIdentifier: ChatRightCell.identifier) + register(ChatDateHeaderView.self, forHeaderFooterViewReuseIdentifier: ChatDateHeaderView.identifier) } } -extension CallVanChatTableView: UITableViewDelegate { +extension ChatTableView: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: CallVanChatDateHeaderView.identifier) as? CallVanChatDateHeaderView else { + guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: ChatDateHeaderView.identifier) as? ChatDateHeaderView else { return nil } headerView.configure(date: dates[section]) @@ -62,7 +62,7 @@ extension CallVanChatTableView: UITableViewDelegate { } } -extension CallVanChatTableView: UITableViewDataSource { +extension ChatTableView: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { return dates.count @@ -75,15 +75,16 @@ extension CallVanChatTableView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let message = messages[indexPath.section][indexPath.row] - if message.isMine { - guard let cell = tableView.dequeueReusableCell(withIdentifier: CallVanChatRightCell.identifier, for: indexPath) as? CallVanChatRightCell else { + switch message.alignment { + case .right: + guard let cell = tableView.dequeueReusableCell(withIdentifier: ChatRightCell.identifier, for: indexPath) as? ChatRightCell else { return UITableViewCell() } cell.configure(message: message) bind(cell) return cell - } else { - guard let cell = tableView.dequeueReusableCell(withIdentifier: CallVanChatLeftCell.identifier, for: indexPath) as? CallVanChatLeftCell else { + case .left: + guard let cell = tableView.dequeueReusableCell(withIdentifier: ChatLeftCell.identifier, for: indexPath) as? ChatLeftCell else { return UITableViewCell() } cell.configure(message: message) @@ -92,13 +93,13 @@ extension CallVanChatTableView: UITableViewDataSource { } } - private func bind(_ cell: CallVanChatLeftCell) { + private func bind(_ cell: ChatLeftCell) { cell.imageTappedPublisher.sink { [weak self] imageUrl in self?.imageTappedPublisher.send(imageUrl) }.store(in: &cell.subscriptions) } - private func bind(_ cell: CallVanChatRightCell) { + private func bind(_ cell: ChatRightCell) { cell.imageTappedPublisher.sink { [weak self] imageUrl in self?.imageTappedPublisher.send(imageUrl) }.store(in: &cell.subscriptions) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 6731f5bf..f2fa8753 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -405,10 +405,10 @@ 7CC42B892F5D85F700940CE1 /* CallVanChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B882F5D85F700940CE1 /* CallVanChat.swift */; }; 7CC42B8F2F5DCE7400940CE1 /* CallVanChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B8E2F5DCE7400940CE1 /* CallVanChatViewController.swift */; }; 7CC42B912F5DCE7D00940CE1 /* CallVanChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B902F5DCE7D00940CE1 /* CallVanChatViewModel.swift */; }; - 7CC42B942F5DCEA600940CE1 /* CallVanChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B932F5DCEA600940CE1 /* CallVanChatTableView.swift */; }; - 7CC42B962F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B952F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift */; }; - 7CC42B982F5E812F00940CE1 /* CallVanChatLeftCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B972F5E812F00940CE1 /* CallVanChatLeftCell.swift */; }; - 7CC42B9C2F5E834E00940CE1 /* CallVanChatRightCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B9B2F5E834E00940CE1 /* CallVanChatRightCell.swift */; }; + 7CC42B942F5DCEA600940CE1 /* ChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */; }; + 7CC42B962F5DCEB300940CE1 /* ChatDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */; }; + 7CC42B982F5E812F00940CE1 /* ChatLeftCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */; }; + 7CC42B9C2F5E834E00940CE1 /* ChatRightCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */; }; 7CCB1E4B2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4A2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift */; }; 7CCB1E4E2F29DB3200472669 /* LostItemPostChatDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB1E4D2F29DB3200472669 /* LostItemPostChatDetailRequest.swift */; }; 7CD411EA2FEB92280079467B /* HomeTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD411E92FEB92280079467B /* HomeTabBarItem.swift */; }; @@ -766,6 +766,11 @@ D19A00013000000000000002 /* NotificationRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000001 /* NotificationRowModel.swift */; }; D19A00013000000000000004 /* NotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000003 /* NotificationListView.swift */; }; D19A00013000000000000006 /* NotificationRowModel+NotificationHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */; }; + D19A10013000000000000002 /* ChatListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A10013000000000000001 /* ChatListModel.swift */; }; + D19A10013000000000000004 /* ChatMessageRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A10013000000000000003 /* ChatMessageRowModel.swift */; }; + D19A10013000000000000006 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A10013000000000000005 /* ChatListView.swift */; }; + D19A10013000000000000008 /* ChatInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A10013000000000000007 /* ChatInputView.swift */; }; + D19A1001300000000000000A /* ChatListModel+CallVanChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A10013000000000000009 /* ChatListModel+CallVanChat.swift */; }; D2009F762C17270F00211D1B /* CalendarDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2009F752C17270F00211D1B /* CalendarDate.swift */; }; D2009F7F2C1784DA00211D1B /* MockAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2009F7E2C1784DA00211D1B /* MockAnalyticsService.swift */; }; D2078BF62BC4CCEB00A39861 /* EventDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2078BF52BC4CCEB00A39861 /* EventDto.swift */; }; @@ -1410,10 +1415,10 @@ 7CC42B882F5D85F700940CE1 /* CallVanChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChat.swift; sourceTree = ""; }; 7CC42B8E2F5DCE7400940CE1 /* CallVanChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatViewController.swift; sourceTree = ""; }; 7CC42B902F5DCE7D00940CE1 /* CallVanChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatViewModel.swift; sourceTree = ""; }; - 7CC42B932F5DCEA600940CE1 /* CallVanChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatTableView.swift; sourceTree = ""; }; - 7CC42B952F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatDateHeaderView.swift; sourceTree = ""; }; - 7CC42B972F5E812F00940CE1 /* CallVanChatLeftCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatLeftCell.swift; sourceTree = ""; }; - 7CC42B9B2F5E834E00940CE1 /* CallVanChatRightCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanChatRightCell.swift; sourceTree = ""; }; + 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableView.swift; sourceTree = ""; }; + 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDateHeaderView.swift; sourceTree = ""; }; + 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLeftCell.swift; sourceTree = ""; }; + 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRightCell.swift; sourceTree = ""; }; 7CCB1E4A2F29D87C00472669 /* LostItemPostChatDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemPostChatDetailUseCase.swift; sourceTree = ""; }; 7CCB1E4D2F29DB3200472669 /* LostItemPostChatDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemPostChatDetailRequest.swift; sourceTree = ""; }; 7CD411E92FEB92280079467B /* HomeTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabBarItem.swift; sourceTree = ""; }; @@ -1774,6 +1779,11 @@ D19A00013000000000000001 /* NotificationRowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowModel.swift; sourceTree = ""; }; D19A00013000000000000003 /* NotificationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListView.swift; sourceTree = ""; }; D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationRowModel+NotificationHistoryItem.swift"; sourceTree = ""; }; + D19A10013000000000000001 /* ChatListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListModel.swift; sourceTree = ""; }; + D19A10013000000000000003 /* ChatMessageRowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageRowModel.swift; sourceTree = ""; }; + D19A10013000000000000005 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; + D19A10013000000000000007 /* ChatInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = ""; }; + D19A10013000000000000009 /* ChatListModel+CallVanChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatListModel+CallVanChat.swift"; sourceTree = ""; }; D2009F752C17270F00211D1B /* CalendarDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDate.swift; sourceTree = ""; }; D2009F7E2C1784DA00211D1B /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = ""; }; D2078BF52BC4CCEB00A39861 /* EventDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDto.swift; sourceTree = ""; }; @@ -3253,30 +3263,19 @@ 7CC42B8A2F5DCE4800940CE1 /* CallVanChat */ = { isa = PBXGroup; children = ( - 7CC42B8B2F5DCE5700940CE1 /* Subviews */, + 7CC42B8B2F5DCE5700940CE1 /* Support */, 7CC42B8E2F5DCE7400940CE1 /* CallVanChatViewController.swift */, 7CC42B902F5DCE7D00940CE1 /* CallVanChatViewModel.swift */, ); path = CallVanChat; sourceTree = ""; }; - 7CC42B8B2F5DCE5700940CE1 /* Subviews */ = { + 7CC42B8B2F5DCE5700940CE1 /* Support */ = { isa = PBXGroup; children = ( - 7CC42B922F5DCE9800940CE1 /* CallVanChatTableView */, + D19A10013000000000000009 /* ChatListModel+CallVanChat.swift */, ); - path = Subviews; - sourceTree = ""; - }; - 7CC42B922F5DCE9800940CE1 /* CallVanChatTableView */ = { - isa = PBXGroup; - children = ( - 7CC42B932F5DCEA600940CE1 /* CallVanChatTableView.swift */, - 7CC42B952F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift */, - 7CC42B972F5E812F00940CE1 /* CallVanChatLeftCell.swift */, - 7CC42B9B2F5E834E00940CE1 /* CallVanChatRightCell.swift */, - ); - path = CallVanChatTableView; + path = Support; sourceTree = ""; }; 7CCB1E4C2F29DB2500472669 /* LostItemChat */ = { @@ -4665,6 +4664,7 @@ D19A00013000000000000007 /* Shared */ = { isa = PBXGroup; children = ( + D19A1001300000000000000C /* Chat */, D19A00013000000000000008 /* Notification */, ); path = Shared; @@ -4695,6 +4695,45 @@ path = Support; sourceTree = ""; }; + D19A1001300000000000000C /* Chat */ = { + isa = PBXGroup; + children = ( + D19A1001300000000000000D /* Models */, + D19A1001300000000000000E /* Views */, + ); + path = Chat; + sourceTree = ""; + }; + D19A1001300000000000000D /* Models */ = { + isa = PBXGroup; + children = ( + D19A10013000000000000001 /* ChatListModel.swift */, + D19A10013000000000000003 /* ChatMessageRowModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + D19A1001300000000000000E /* Views */ = { + isa = PBXGroup; + children = ( + D19A10013000000000000005 /* ChatListView.swift */, + D19A10013000000000000007 /* ChatInputView.swift */, + 7CC42B922F5DCE9800940CE1 /* ChatTableView */, + ); + path = Views; + sourceTree = ""; + }; + 7CC42B922F5DCE9800940CE1 /* ChatTableView */ = { + isa = PBXGroup; + children = ( + 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */, + 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */, + 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */, + 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */, + ); + path = ChatTableView; + sourceTree = ""; + }; D2009F7D2C1784C000211D1B /* MockService */ = { isa = PBXGroup; children = ( @@ -5800,7 +5839,7 @@ D874DB4D2DBF1A6C0098EED0 /* SendVerificationCodeUsecase.swift in Sources */, 7CC42B622F59D0EF00940CE1 /* CallVanPostPlaceView.swift in Sources */, D27DD0E42BA609F30081FD36 /* ShopDataDto.swift in Sources */, - 7CC42B9C2F5E834E00940CE1 /* CallVanChatRightCell.swift in Sources */, + 7CC42B9C2F5E834E00940CE1 /* ChatRightCell.swift in Sources */, D20AD6EB2E028F950067760A /* FindIdSmsRequest.swift in Sources */, D27DD0CB2BA608310081FD36 /* ShopCategoryDto.swift in Sources */, 7C6790B32FCC5B2A0045E163 /* TimetableColorAsset.swift in Sources */, @@ -5836,7 +5875,7 @@ D27DD0CC2BA608310081FD36 /* ShopsDto.swift in Sources */, 7C61D3632F242D8D005FAC3A /* AddLostItemHeaderView.swift in Sources */, D874DB492DBE31530098EED0 /* CheckDuplicatedPhoneNumberUseCase.swift in Sources */, - 7CC42B982F5E812F00940CE1 /* CallVanChatLeftCell.swift in Sources */, + 7CC42B982F5E812F00940CE1 /* ChatLeftCell.swift in Sources */, 83DD0DE62C57406600278ABD /* GetBusFiltersUseCase.swift in Sources */, D22C54732C6A2A73000826DA /* FetchMyReviewUseCase.swift in Sources */, D29F21D52C4E1A8300994554 /* RegisterUseCase.swift in Sources */, @@ -6183,7 +6222,7 @@ 7C8A00012FD000000011D338 /* HomeViewModel.swift in Sources */, 7CED917A2EF2D1F900457128 /* ShopSummaryTableView.swift in Sources */, 7C61D34B2F220EB3005FAC3A /* EditLostItemFoundPlaceView.swift in Sources */, - 7CC42B942F5DCEA600940CE1 /* CallVanChatTableView.swift in Sources */, + 7CC42B942F5DCEA600940CE1 /* ChatTableView.swift in Sources */, 7CED917D2EF2D1F900457128 /* ReviewImageUploadCollectionView.swift in Sources */, 7CED917F2EF2D1F900457128 /* NoticeListViewController.swift in Sources */, 7CED91822EF2D1F900457128 /* DeliveryTipsCollectionView.swift in Sources */, @@ -6352,7 +6391,7 @@ 7C6403FE300F36F200A9589F /* DepartmentAPI.swift in Sources */, 7CA60D632F66197C007C08C1 /* CallVanRepository.swift in Sources */, 7C0C188F2F67F8BA001781B3 /* CallVanDataDto.swift in Sources */, - 7CC42B962F5DCEB300940CE1 /* CallVanChatDateHeaderView.swift in Sources */, + 7CC42B962F5DCEB300940CE1 /* ChatDateHeaderView.swift in Sources */, 7CED91DD2EF2D1F900457128 /* ShopDetailTableViewNameCell.swift in Sources */, 7C8A943B2FD424A600DEA6F5 /* WeatherDto.swift in Sources */, 7CED91DF2EF2D1F900457128 /* NotiViewController.swift in Sources */, @@ -6485,6 +6524,11 @@ D19A00013000000000000002 /* NotificationRowModel.swift in Sources */, D19A00013000000000000004 /* NotificationListView.swift in Sources */, D19A00013000000000000006 /* NotificationRowModel+NotificationHistoryItem.swift in Sources */, + D19A10013000000000000002 /* ChatListModel.swift in Sources */, + D19A10013000000000000004 /* ChatMessageRowModel.swift in Sources */, + D19A10013000000000000006 /* ChatListView.swift in Sources */, + D19A10013000000000000008 /* ChatInputView.swift in Sources */, + D19A1001300000000000000A /* ChatListModel+CallVanChat.swift in Sources */, 7C8BFCE52FD04B8000963679 /* CategoryModels.swift in Sources */, 7C8BFCF12FD04B800963679 /* HomeCounts.swift in Sources */, D2908D262BB8D11A0008F908 /* SemesterDto.swift in Sources */, From 0765bcd033e2be2b22d0916b7a7a1ca5a4f0e16c Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Thu, 20 Aug 2026 21:39:34 +0900 Subject: [PATCH 069/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwiftData Model의 이름 변경으로 인해 기존 데이터가 유실되지 않도록 수정했습니다. --- Common/NotificationHistory/NotificationHistoryRecord.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Common/NotificationHistory/NotificationHistoryRecord.swift b/Common/NotificationHistory/NotificationHistoryRecord.swift index 4a956787..4100327b 100644 --- a/Common/NotificationHistory/NotificationHistoryRecord.swift +++ b/Common/NotificationHistory/NotificationHistoryRecord.swift @@ -8,8 +8,10 @@ import SwiftData import Foundation +typealias NotificationHistoryRecord = NotificationRecord + @Model -final class NotificationHistoryRecord { +final class NotificationRecord { var body: String var title: String var category: AppPath From 1fc3c521be65fc067e8951f06a01ad0260c487b3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 12:03:38 +0900 Subject: [PATCH 070/101] =?UTF-8?q?design:=20=EC=A0=84=EA=B3=B5=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=AA=A8=EB=8B=AC=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinModalViewController.swift | 1 + .../SelectDeptModalViewController.swift | 276 +++++++++--------- .../Timetable/TimetableViewController.swift | 11 +- .../Timetable/TimetableViewModel.swift | 23 ++ koin.xcodeproj/project.pbxproj | 22 +- 5 files changed, 185 insertions(+), 148 deletions(-) diff --git a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift index 878c5c0c..56074646 100644 --- a/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift +++ b/Koin/Core/View/KoinModalViewController/KoinModalViewController.swift @@ -131,6 +131,7 @@ extension KoinModalViewController: UIViewControllerTransitioningDelegate { extension KoinModalViewController { private func setUpGestureRecognizer() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAround)) + tapGesture.cancelsTouchesInView = false view.addGestureRecognizer(tapGesture) } diff --git a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift index d7bf6713..f61dee74 100644 --- a/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/SubViews/SelectDeptModalViewController.swift @@ -2,151 +2,104 @@ // SelectDeptModalViewController.swift // koin // -// Created by 김나훈 on 12/6/24. +// Created by 홍기정 on 8/23/26. // import UIKit final class SelectDeptModalViewController: KoinModalViewController { - - private enum Layout { - static let buttonsPerRow: Int = 2 - static let gridSpacing: CGFloat = 4.8 - static let departmentButtonHeight: CGFloat = 32 - static let departmentButtonCornerRadius: CGFloat = 4 - } - + // MARK: - Properties - private let onDepartmentSelected: (String?) -> Void - private let departments = [ - "디자인ㆍ건축공학부", - "고용서비스정책학과", - "기계공학부", - "메카트로닉스공학부", - "산업경영학부", - "전기ㆍ전자ㆍ통신공학부", - "컴퓨터공학부", - "에너지신소재화학공학부", - "HRD학과", - "교양학부", - "안전공학과", - "융합학과" - ] - private var selectedDepartment: String? = nil - private var selectedButton: UIButton? = nil - + private let onCompleteButtonTapped: (String?)->Void + + // MARK: - Radio Button + private let departmentRadioButtonGroup = RadioButtonGroup() + + // MARK: - State + var selectedDepartment: String? { + departmentRadioButtonGroup.selectedRadioButton?.accessibilityLabel + } + // MARK: - UI Components - private let containerView = UIView() - private let messageLabel = UILabel() - private let gridStackView = UIStackView() - private var departmentButtons: [UIButton] = [] + private let customView = UIView() + + private let titleLabel = UILabel() + + private let departmentScrollView = UIScrollView() + private let departmentStackView = UIStackView() + private var departmentRadioButtons: [RadioButton] = [] + + private let cancelButton = UIButton() private let completeButton = UIButton() - + // MARK: - Initializer init( - onDepartmentSelected: @escaping (String?) -> Void + departments: [String], + selectedDapartment: String? = nil, + onCompleteButtonTapped: @escaping (String?)->Void ) { - self.onDepartmentSelected = onDepartmentSelected - + self.onCompleteButtonTapped = onCompleteButtonTapped + super.init(configuration: .init( appearance: .primary, - content: .custom(customView: containerView), + content: .custom(customView: customView), button: .none, layout: .init( width: 327, - contentTopPadding: 18, - contentHorizontalPadding: 12, - contentBottomPadding: 18 + contentTopPadding: 0, + contentHorizontalPadding: 0, + contentBottomPadding: 0 ) )) + + setUpRadioButtons( + departments: departments, + selectedDapartment: selectedDapartment + ) } @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() configureView() - setUpDepartments() - setAddTarget() + setAddTargets() } } - -extension SelectDeptModalViewController { - private func setAddTarget() { - completeButton.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside) - } - @objc private func completeButtonTapped() { - onDepartmentSelected(selectedDepartment) - dismiss(animated: true) - } -} - extension SelectDeptModalViewController { - private func setUpDepartments() { - var currentRow: UIStackView? = nil - - for (index, department) in departments.enumerated() { - let button = createDepartmentButton(title: department) - departmentButtons.append(button) - - if index % Layout.buttonsPerRow == 0 { - currentRow = makeRowStackView() - gridStackView.addArrangedSubview(currentRow!) + private func setUpRadioButtons( + departments: [String], + selectedDapartment: String? = nil + ) { + for department in departments { + let radioButton = RadioButton(title: department) + + departmentRadioButtons.append(radioButton) + departmentRadioButtonGroup.addRadioButton(radioButton) + + if department == selectedDapartment { + departmentRadioButtonGroup.selectRadioButton(radioButton) } - - currentRow?.addArrangedSubview(button) } } +} - private func makeRowStackView() -> UIStackView { - let stackView = UIStackView() - stackView.do { - $0.axis = .horizontal - $0.spacing = Layout.gridSpacing - $0.distribution = .fillEqually - } - return stackView - } - - private func createDepartmentButton(title: String) -> UIButton { - let button = UIButton(type: .system) - button.do { - $0.setTitle(title, for: .normal) - $0.titleLabel?.font = .appFont(.pretendardMedium, size: 14) - $0.layer.cornerRadius = Layout.departmentButtonCornerRadius - $0.layer.borderWidth = 1 - $0.layer.borderColor = UIColor.appColor(.neutral300).cgColor - $0.addTarget(self, action: #selector(departmentButtonTapped(_:)), for: .touchUpInside) - } - button.snp.makeConstraints { - $0.height.equalTo(Layout.departmentButtonHeight) - } - apply(isSelected: false, to: button) - return button +extension SelectDeptModalViewController { + private func setAddTargets() { + cancelButton.addTarget(self, action: #selector(cancelButtonButtonTapped), for: .touchUpInside) + completeButton.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside) } - - private func apply(isSelected: Bool, to button: UIButton) { - button.backgroundColor = .appColor(isSelected ? .primary500 : .neutral0) - button.setTitleColor(.appColor(isSelected ? .neutral0 : .neutral800), for: .normal) + + @objc private func cancelButtonButtonTapped() { + dismiss(animated: true) } - - // MARK: - Objc - @objc private func departmentButtonTapped(_ sender: UIButton) { - guard let department = sender.title(for: .normal) else { return } - - if selectedButton == sender { - apply(isSelected: false, to: sender) - selectedButton = nil - selectedDepartment = nil - } else { - departmentButtons.forEach { apply(isSelected: false, to: $0) } - apply(isSelected: true, to: sender) - selectedButton = sender - selectedDepartment = department - } + + @objc private func completeButtonTapped() { + onCompleteButtonTapped(selectedDepartment) + dismiss(animated: true) } } @@ -156,50 +109,105 @@ extension SelectDeptModalViewController { setUpLayouts() setUpConstraints() } - + private func setUpStyles() { - messageLabel.do { + titleLabel.do { $0.text = "전공선택" - $0.font = .appFont(.pretendardMedium, size: 18) $0.textColor = .appColor(.primary500) + $0.font = .appFont(.pretendardSemiBold, size: 18) } - - gridStackView.do { + + departmentScrollView.do { + $0.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) + $0.verticalScrollIndicatorInsets = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) + } + + departmentStackView.do { $0.axis = .vertical - $0.spacing = Layout.gridSpacing - $0.distribution = .fill + $0.spacing = 8 + $0.alignment = .fill } + cancelButton.do { + $0.setAttributedTitle( + NSAttributedString( + string: "취소", + attributes: [ + .font: UIFont.appFont(.pretendardMedium, size: 14), + .foregroundColor: UIColor.appColor(.neutral500) + ]), + for: .normal + ) + } completeButton.do { - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitle("완료", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) - $0.layer.cornerRadius = 4 - $0.layer.masksToBounds = true + $0.setAttributedTitle( + NSAttributedString( + string: "완료", + attributes: [ + .font: UIFont.appFont(.pretendardMedium, size: 14), + .foregroundColor: UIColor.appColor(.neutral0) + ]), + for: .normal + ) + $0.backgroundColor = .appColor(.primary500) + $0.layer.cornerRadius = 6 } } - + private func setUpLayouts() { - [messageLabel, gridStackView, completeButton].forEach { - containerView.addSubview($0) + departmentRadioButtons.forEach { + departmentStackView.addArrangedSubview($0) + } + + [departmentStackView].forEach { + departmentScrollView.addSubview($0) + } + + [titleLabel, + departmentScrollView, + cancelButton, + completeButton].forEach { + customView.addSubview($0) } } - + private func setUpConstraints() { - messageLabel.snp.makeConstraints { - $0.top.leading.equalToSuperview() + titleLabel.snp.makeConstraints { $0.height.equalTo(29) + $0.top.equalToSuperview().offset(12) + $0.leading.equalToSuperview().offset(24) } - gridStackView.snp.makeConstraints { - $0.top.equalTo(messageLabel.snp.bottom).offset(6) - $0.leading.trailing.equalToSuperview() + + departmentScrollView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(323) } + + departmentStackView.snp.makeConstraints { + $0.edges.equalTo(departmentScrollView.contentLayoutGuide) + $0.width.equalTo(departmentScrollView) + } + + departmentRadioButtons.forEach { + $0.snp.makeConstraints { + $0.height.equalTo(24) + } + } + completeButton.snp.makeConstraints { - $0.width.equalTo(60) + $0.width.equalTo(49) + $0.height.equalTo(30) + $0.top.equalTo(departmentScrollView.snp.bottom).offset(12) + $0.trailing.equalToSuperview().offset(-24) + $0.bottom.equalToSuperview().offset(-12) + } + + cancelButton.snp.makeConstraints { + $0.width.equalTo(49) $0.height.equalTo(30) - $0.top.equalTo(gridStackView.snp.bottom).offset(6) - $0.trailing.bottom.equalToSuperview() + $0.trailing.equalTo(completeButton.snp.leading).offset(-8) + $0.bottom.equalTo(completeButton) } } } diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 57e448fc..7cda2ec0 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -151,6 +151,7 @@ final class TimetableViewController: UIViewController { } self.view.endEditing(true) self.toggleCollectionView(collectionView: self.addClassCollectionView, animate: true) + self.inputSubject.send(.selectedDepartment(nil)) }.store(in: &subscriptions) @@ -271,10 +272,14 @@ extension TimetableViewController { } private func presentSelectDeptModal() { - let viewController = SelectDeptModalViewController { [weak self] department in + let modalViewController = SelectDeptModalViewController( + departments: viewModel.departments, + selectedDapartment: viewModel.selectedDepartment + ) { [weak self] department in self?.addClassCollectionView.setUpSelectedDept(dept: department) + self?.inputSubject.send(.selectedDepartment(department)) } - present(viewController, animated: true) + present(modalViewController, animated: true) } private func presentDeleteLectureModal(lecture: LectureData) { @@ -519,7 +524,7 @@ extension TimetableViewController { } } @objc private func modifyTimetableButtonTapped() { - + addClassCollectionView.setUpSelectedDept(dept: nil) if addClassCollectionView.isHidden && addDirectCollectionView.isHidden { toggleCollectionView(collectionView: addClassCollectionView, animate: true) diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift index cc1a6c97..22a2741c 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift @@ -22,6 +22,8 @@ final class TimetableViewModel: ViewModelProtocol { case modifyLecture(LectureData, Bool) case _deleteLecture(LectureData) case postCustomLecture(String, [Int]) + + case selectedDepartment(String?) } // MARK: - Output @@ -104,6 +106,10 @@ final class TimetableViewModel: ViewModelProtocol { } } + // 전체 전공 목록 + private(set) var departments: [String] = [] + private(set) var selectedDepartment: String? + // MARK: - Initialization @@ -119,6 +125,8 @@ final class TimetableViewModel: ViewModelProtocol { self?.deleteLectureById(lecture: lecture) case let .postCustomLecture(lectureName, lectureTime): self?.postCustomLecture(lectureName: lectureName, classTime: lectureTime) + case let .selectedDepartment(selectedDepartment): + self?.selectedDepartment = selectedDepartment } }.store(in: &subscriptions) return outputSubject.eraseToAnyPublisher() @@ -387,6 +395,7 @@ extension TimetableViewModel { receiveCompletion: { _ in }, receiveValue: { [weak self] response in self?.outputSubject.send(.updateLectureList(response)) + self?.updateDeparments(response) } ).store(in: &subscriptions) } @@ -462,3 +471,17 @@ extension TimetableViewModel { ).store(in: &subscriptions) } } + +extension TimetableViewModel { + private func updateDeparments(_ response: [SemesterLecture]) { + guard departments.isEmpty else { + return + } + self.departments = response + .map(\.department) + .reduce(into: Set(), { departments, department in + departments.insert(department) + }) + .sorted() + } +} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 034b162e..8d5d02d3 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -3286,6 +3286,17 @@ path = Support; sourceTree = ""; }; + 7CC42B922F5DCE9800940CE1 /* ChatTableView */ = { + isa = PBXGroup; + children = ( + 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */, + 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */, + 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */, + 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */, + ); + path = ChatTableView; + sourceTree = ""; + }; 7CCB1E4C2F29DB2500472669 /* LostItemChat */ = { isa = PBXGroup; children = ( @@ -4735,17 +4746,6 @@ path = Views; sourceTree = ""; }; - 7CC42B922F5DCE9800940CE1 /* ChatTableView */ = { - isa = PBXGroup; - children = ( - 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */, - 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */, - 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */, - 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */, - ); - path = ChatTableView; - sourceTree = ""; - }; D2009F7D2C1784C000211D1B /* MockService */ = { isa = PBXGroup; children = ( From a5f5e6b487d199109f2720484f47343b26bee40e Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 12:20:07 +0900 Subject: [PATCH 071/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimeTable/Timetable/TimetableViewController.swift | 1 + .../Presentation/TimeTable/Timetable/TimetableViewModel.swift | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift index 7cda2ec0..ac6e975f 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewController.swift @@ -525,6 +525,7 @@ extension TimetableViewController { } @objc private func modifyTimetableButtonTapped() { addClassCollectionView.setUpSelectedDept(dept: nil) + inputSubject.send(.selectedDepartment(nil)) if addClassCollectionView.isHidden && addDirectCollectionView.isHidden { toggleCollectionView(collectionView: addClassCollectionView, animate: true) diff --git a/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift b/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift index 22a2741c..134191de 100644 --- a/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift +++ b/Koin/Presentation/TimeTable/Timetable/TimetableViewModel.swift @@ -474,14 +474,12 @@ extension TimetableViewModel { extension TimetableViewModel { private func updateDeparments(_ response: [SemesterLecture]) { - guard departments.isEmpty else { - return - } self.departments = response .map(\.department) .reduce(into: Set(), { departments, department in departments.insert(department) }) .sorted() + self.selectedDepartment = nil } } From 961c7631f025635fcb5058bb751643d55a6a8988 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 21:46:38 +0900 Subject: [PATCH 072/101] =?UTF-8?q?feat:=20KoinDropdown=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helper/KoinDropdownAnimator.swift | 61 ++++++ .../Core/View/KoinDropdown/KoinDropdown.swift | 155 ++++++++++++++ .../KoinDropdownConfiguration.swift | 65 ++++++ .../View/KoinDropdown/KoinDropdownHost.swift | 196 ++++++++++++++++++ koin.xcodeproj/project.pbxproj | 54 ++++- 5 files changed, 520 insertions(+), 11 deletions(-) create mode 100644 Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift create mode 100644 Koin/Core/View/KoinDropdown/KoinDropdown.swift create mode 100644 Koin/Core/View/KoinDropdown/KoinDropdownConfiguration.swift create mode 100644 Koin/Core/View/KoinDropdown/KoinDropdownHost.swift diff --git a/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift b/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift new file mode 100644 index 00000000..d430704c --- /dev/null +++ b/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift @@ -0,0 +1,61 @@ +// +// KoinDropdownAnimator.swift +// koin +// +// Created by 홍기정 on 8/21/26. +// + +import UIKit + +@MainActor +final class KoinDropdownAnimator { + + enum Metric { + static let presentDuration: TimeInterval = 0.3 + static let presentBounce: CGFloat = 0.15 + static let dismissDuration: TimeInterval = 0.15 + } + + private func hiddenTransform(travel: CGFloat) -> CGAffineTransform { + CGAffineTransform(translationX: 0, y: -travel) + } + + func present( + view: UIView, + travel: CGFloat, + completion: (() -> Void)? = nil + ) { + view.isHidden = false + view.alpha = 0 + view.transform = hiddenTransform(travel: travel) + + UIView.animate( + springDuration: Metric.presentDuration, + bounce: Metric.presentBounce, + initialSpringVelocity: 0 + ) { + view.alpha = 1 + view.transform = .identity + } completion: { _ in + completion?() + } + } + + func dismiss( + view: UIView, + travel: CGFloat, + completion: (() -> Void)? = nil + ) { + UIView.animate( + springDuration: Metric.dismissDuration, + initialSpringVelocity: 0, + options: [.beginFromCurrentState, .allowUserInteraction] + ) { + view.alpha = 0 + view.transform = self.hiddenTransform(travel: travel) + } completion: { _ in + view.isHidden = true + completion?() + } + } +} diff --git a/Koin/Core/View/KoinDropdown/KoinDropdown.swift b/Koin/Core/View/KoinDropdown/KoinDropdown.swift new file mode 100644 index 00000000..82607a93 --- /dev/null +++ b/Koin/Core/View/KoinDropdown/KoinDropdown.swift @@ -0,0 +1,155 @@ +// +// KoinDropdown.swift +// koin +// +// Created by 홍기정 on 8/21/26. +// + +import UIKit +import Combine + +@MainActor +protocol KoinDropdownContentView: AnyObject { + var dismissTappedPublisher: AnyPublisher { get } + var height: CGFloat { get } +} + +@MainActor +final class KoinDropdown: UIView { + + enum State { + case dismissed + case presented + } + + // MARK: - State + private(set) var state: State = .dismissed + + // MARK: - Properties + private weak var host: KoinDropdownHost? + private(set) weak var trigger: UIView? + private let configuration: KoinDropdownConfiguration + private var subscriptions: Set = [] + + // MARK: - UI Components + private let contentView: UIView & KoinDropdownContentView + + // MARK: - Animation + let animator = KoinDropdownAnimator() + private var travel: CGFloat = 0 + + // MARK: - Initializer + init( + host: KoinDropdownHost, + trigger: UIView, + contentView: UIView & KoinDropdownContentView, + configuration: KoinDropdownConfiguration + ) { + self.host = host + self.trigger = trigger + self.contentView = contentView + self.configuration = configuration + super.init(frame: .zero) + + configureView() + bind() + } + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Bind + private func bind() { + contentView.dismissTappedPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.dismiss() + } + .store(in: &subscriptions) + } + + // MARK: - Public + func toggle() { + host?.toggle(self) + } + + func present() { + host?.present(self) + } + + func dismiss() { + host?.dismiss(self) + } +} + + +extension KoinDropdown { + func calculateTravel(in space: UIView) -> CGFloat? { + guard let trigger else { return nil } + contentView.transform = .identity + + let triggerFrame = trigger.convert(trigger.bounds, to: space) + let panelHeight = contentView.height + let padding = configuration.shadowPadding + + guard 0 < triggerFrame.width, + 0 < panelHeight else { + return nil + } + self.frame = CGRect( + x: triggerFrame.minX - padding.left, + y: triggerFrame.maxY, + width: triggerFrame.width + padding.left + padding.right, + height: configuration.topPadding + panelHeight + padding.bottom + ) + contentView.frame = CGRect( + x: padding.left, + y: configuration.topPadding, + width: triggerFrame.width, + height: panelHeight + ) + applyShadow() + + travel = panelHeight + configuration.topPadding + return travel + } + + func animatePresent() { + state = .presented + animator.present(view: contentView, travel: travel) + } + + func animateDismiss(completion: @escaping () -> Void) { + state = .dismissed + animator.dismiss( + view: contentView, + travel: travel, + completion: completion + ) + } +} + +extension KoinDropdown { + /// 프레임이 확정된 뒤에 부른다 — `shadowPath` 가 콘텐츠의 bounds 를 쓰기 때문이다. + private func applyShadow() { + let shadow = configuration.shadow + contentView.layer.applySketchShadow( + color: UIColor.appColor(shadow.color), + alpha: shadow.alpha, + x: shadow.offset.x, + y: shadow.offset.y, + blur: shadow.blur, + spread: shadow.spread + ) + } +} + + +extension KoinDropdown { + private func configureView() { + clipsToBounds = true + backgroundColor = .clear + addSubview(contentView) + } +} diff --git a/Koin/Core/View/KoinDropdown/KoinDropdownConfiguration.swift b/Koin/Core/View/KoinDropdown/KoinDropdownConfiguration.swift new file mode 100644 index 00000000..f8268de3 --- /dev/null +++ b/Koin/Core/View/KoinDropdown/KoinDropdownConfiguration.swift @@ -0,0 +1,65 @@ +// +// KoinDropdownConfiguration.swift +// koin +// +// Created by 홍기정 on 8/21/26. +// + +import UIKit + +struct KoinDropdownConfiguration { + let topPadding: CGFloat + let shadow: Shadow +} + +extension KoinDropdownConfiguration { + enum Shadow { + case shadow2 + case shadowSmall + + var color: ColorAsset { + switch self { + case .shadow2, .shadowSmall: + return .neutral800 + } + } + + var alpha: Float { + switch self { + case .shadow2: return 0.08 + case .shadowSmall: return 0.06 + } + } + + var offset: CGPoint { + switch self { + case .shadow2: return CGPoint(x: 0, y: 4) + case .shadowSmall: return CGPoint(x: 0, y: 1) + } + } + + var blur: CGFloat { + switch self { + case .shadow2: return 10 + case .shadowSmall: return 9 + } + } + + var spread: CGFloat { + switch self { + case .shadow2: return 0 + case .shadowSmall: return 1 + } + } + } + + var shadowPadding: UIEdgeInsets { + let base = max(0, shadow.blur + shadow.spread) + return UIEdgeInsets( + top: 0, + left: base + max(0, -shadow.offset.x), + bottom: base + max(0, shadow.offset.y), + right: base + max(0, shadow.offset.x) + ) + } +} diff --git a/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift b/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift new file mode 100644 index 00000000..439d2364 --- /dev/null +++ b/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift @@ -0,0 +1,196 @@ +// +// KoinDropdownHost.swift +// koin +// +// Created by 홍기정 on 8/22/26. +// + +import UIKit + +@MainActor +final class KoinDropdownHost { + + private enum Metric { + static let overlayZPosition: CGFloat = 10_000 + } + + // MARK: - Properties + private weak var scrollView: UIScrollView? + private var overlay: UIView? + private var dropdowns: [KoinDropdown] = [] + + private var presentedDropdown: KoinDropdown? + private var addedBottomInset: CGFloat = 0 + + var isPresenting: Bool { + presentedDropdown != nil + } + + // MARK: - Initializer + init(scrollView: UIScrollView) { + self.scrollView = scrollView + } + + // MARK: - Public + func makeDropdown( + trigger: UIView, + contentView: UIView & KoinDropdownContentView, + configuration: KoinDropdownConfiguration + ) -> KoinDropdown { + let dropdown = KoinDropdown( + host: self, + trigger: trigger, + contentView: contentView, + configuration: configuration + ) + dropdowns.append(dropdown) + return dropdown + } +} + +extension KoinDropdownHost { + + // MARK: - Toggle + func toggle(_ dropdown: KoinDropdown) { + if dropdown.state == .presented { + dismiss(dropdown) + } else { + present(dropdown) + } + } + + // MARK: - Present + func present(_ dropdown: KoinDropdown) { + guard let scrollView, + presentedDropdown == nil else { + return + } + + scrollView.layoutIfNeeded() + + // 바깥 영역 탭을 인식할 overlay + let overlay = makeOverlay(in: scrollView, dropdown: dropdown) + scrollView.addSubview(overlay) + self.overlay = overlay + + // overlay에 들어가는 dropdown + guard let travel = dropdown.calculateTravel(in: overlay) else { + overlay.removeFromSuperview() + self.overlay = nil + return + } + overlay.addSubview(dropdown) + self.presentedDropdown = dropdown + + // present 중에는 사용자에 의한 스크롤을 막는다. + scrollView.isScrollEnabled = false + + // present + addedBottomInset = addBottomInset( + travel: travel, + of: dropdown, + in: scrollView + ) + scrollView.scrollRectToVisible( + dropdown.convert(dropdown.bounds, to: scrollView), + animated: true + ) + dropdown.animatePresent() + } + + // MARK: - Dismiss + func dismissPresented() { + guard let presentedDropdown else { + return + } + dismiss(presentedDropdown) + } + + func dismiss(_ dropdown: KoinDropdown) { + guard let presentedDropdown, + presentedDropdown === dropdown, + presentedDropdown.state == .presented else { + return + } + + let dismissDuration = KoinDropdownAnimator.Metric.dismissDuration + UIView.animate(withDuration: dismissDuration) { [weak self] in + self?.scrollView?.contentInset.bottom -= self?.addedBottomInset ?? 0 + self?.addedBottomInset = 0 + } + + dropdown.animateDismiss { [weak self] in + self?.overlay?.removeFromSuperview() + self?.finishDismiss(of: dropdown) + } + } +} + +extension KoinDropdownHost { + private func makeOverlay( + in scrollView: UIScrollView, + dropdown: KoinDropdown + ) -> UIView { + let scrollableRect = CGRect(origin: .zero, size: scrollView.contentSize) + .union(CGRect(origin: scrollView.contentOffset, size: scrollView.bounds.size)) + + let overlay = UIView(frame: scrollableRect.insetBy( + dx: -scrollView.bounds.width, + dy: -scrollView.bounds.height + )) + overlay.backgroundColor = .clear + overlay.layer.zPosition = Metric.overlayZPosition + overlay.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(handleOutsideTap) + ) + ) + return overlay + } + + @objc private func handleOutsideTap() { + guard let presentedDropdown else { + return + } + dismiss(presentedDropdown) + } +} + +extension KoinDropdownHost { + + private func addBottomInset( + travel: CGFloat, + of dropdown: KoinDropdown, + in scrollView: UIScrollView + ) -> CGFloat { + guard let trigger = dropdown.trigger else { + return 0 + } + + let triggerMaxY = trigger.convert(trigger.bounds, to: scrollView).maxY + let dropdownMaxY = triggerMaxY + travel + let viewport = scrollView.bounds.height + let maxOffsetY = max( + -scrollView.adjustedContentInset.top, + scrollView.contentSize.height + scrollView.adjustedContentInset.bottom - viewport + ) + let reachableBottomY = maxOffsetY + viewport + let overflow = dropdownMaxY - reachableBottomY + + guard 0 < overflow else { + return 0 + } + + scrollView.contentInset.bottom += overflow + return overflow + } + + private func finishDismiss(of dropdown: KoinDropdown) { + guard presentedDropdown === dropdown else { return } + + presentedDropdown = nil + overlay = nil + scrollView?.isScrollEnabled = true + } +} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 034b162e..73b04528 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -767,6 +767,7 @@ B68D2CEB2EA50C1E00F3B479 /* UIViewController+Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68D2CEA2EA50C1E00F3B479 /* UIViewController+Toast.swift */; }; B6A0D62A2EEB01E600A19521 /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = B6A0D6292EEB01E600A19521 /* Debug.xcconfig */; }; B6A0D62C2EEB01F100A19521 /* Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = B6A0D62B2EEB01F100A19521 /* Release.xcconfig */; }; + D0A71C4F2F1B8340009E2D71 /* KoinDropdownHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A71C4E2F1B8340009E2D71 /* KoinDropdownHost.swift */; }; D19A00013000000000000002 /* NotificationRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000001 /* NotificationRowModel.swift */; }; D19A00013000000000000004 /* NotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000003 /* NotificationListView.swift */; }; D19A00013000000000000006 /* NotificationRowModel+NotificationHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */; }; @@ -1006,6 +1007,9 @@ D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; F1A000010000000000000003 /* ZoomingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */; }; + FE01000000000000000021 /* KoinDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01000000000000000011 /* KoinDropdown.swift */; }; + FE01000000000000000023 /* KoinDropdownAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01000000000000000013 /* KoinDropdownAnimator.swift */; }; + FE01000000000000000026 /* KoinDropdownConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE01000000000000000016 /* KoinDropdownConfiguration.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1784,6 +1788,7 @@ B68D2CEA2EA50C1E00F3B479 /* UIViewController+Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Toast.swift"; sourceTree = ""; }; B6A0D6292EEB01E600A19521 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B6A0D62B2EEB01F100A19521 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + D0A71C4E2F1B8340009E2D71 /* KoinDropdownHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinDropdownHost.swift; sourceTree = ""; }; D19A00013000000000000001 /* NotificationRowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowModel.swift; sourceTree = ""; }; D19A00013000000000000003 /* NotificationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListView.swift; sourceTree = ""; }; D19A00013000000000000005 /* NotificationRowModel+NotificationHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationRowModel+NotificationHistoryItem.swift"; sourceTree = ""; }; @@ -2027,6 +2032,9 @@ EC692285302700A400EE26ED /* koinUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F1A000010000000000000002 /* ZoomingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionView.swift; sourceTree = ""; }; F1A000010000000000000004 /* ZoomingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomingCollectionViewCell.swift; sourceTree = ""; }; + FE01000000000000000011 /* KoinDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinDropdown.swift; sourceTree = ""; }; + FE01000000000000000013 /* KoinDropdownAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinDropdownAnimator.swift; sourceTree = ""; }; + FE01000000000000000016 /* KoinDropdownConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KoinDropdownConfiguration.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2882,6 +2890,14 @@ path = LostItem; sourceTree = ""; }; + 7C8B2FF0303D928300A7EB5A /* Helper */ = { + isa = PBXGroup; + children = ( + FE01000000000000000013 /* KoinDropdownAnimator.swift */, + ); + path = Helper; + sourceTree = ""; + }; 7CA0D6172EC744E700E9A282 /* ZoomedImageViewControllerB */ = { isa = PBXGroup; children = ( @@ -3286,6 +3302,17 @@ path = Support; sourceTree = ""; }; + 7CC42B922F5DCE9800940CE1 /* ChatTableView */ = { + isa = PBXGroup; + children = ( + 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */, + 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */, + 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */, + 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */, + ); + path = ChatTableView; + sourceTree = ""; + }; 7CCB1E4C2F29DB2500472669 /* LostItemChat */ = { isa = PBXGroup; children = ( @@ -4735,17 +4762,6 @@ path = Views; sourceTree = ""; }; - 7CC42B922F5DCE9800940CE1 /* ChatTableView */ = { - isa = PBXGroup; - children = ( - 7CC42B932F5DCEA600940CE1 /* ChatTableView.swift */, - 7CC42B952F5DCEB300940CE1 /* ChatDateHeaderView.swift */, - 7CC42B972F5E812F00940CE1 /* ChatLeftCell.swift */, - 7CC42B9B2F5E834E00940CE1 /* ChatRightCell.swift */, - ); - path = ChatTableView; - sourceTree = ""; - }; D2009F7D2C1784C000211D1B /* MockService */ = { isa = PBXGroup; children = ( @@ -5248,6 +5264,7 @@ 83B7457C2C9B2EBB005868BD /* IndicatorView.swift */, 83EBB8792D0FDFD700346018 /* KoinPickerView.swift */, 7CC42B732F5AD39C00940CE1 /* KoinPickerDropDownView */, + FE01000000000000000031 /* KoinDropdown */, D289873D2D21B30D00AA7285 /* DebouncedButton.swift */, D89F8FD82DF0A8A4000265DC /* DefaultTextField.swift */, D89F8FDA2DF12847000265DC /* StatefulButton.swift */, @@ -5560,6 +5577,17 @@ path = koinUnitTests; sourceTree = ""; }; + FE01000000000000000031 /* KoinDropdown */ = { + isa = PBXGroup; + children = ( + 7C8B2FF0303D928300A7EB5A /* Helper */, + FE01000000000000000016 /* KoinDropdownConfiguration.swift */, + D0A71C4E2F1B8340009E2D71 /* KoinDropdownHost.swift */, + FE01000000000000000011 /* KoinDropdown.swift */, + ); + path = KoinDropdown; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -5754,6 +5782,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FE01000000000000000021 /* KoinDropdown.swift in Sources */, + D0A71C4F2F1B8340009E2D71 /* KoinDropdownHost.swift in Sources */, + FE01000000000000000023 /* KoinDropdownAnimator.swift in Sources */, + FE01000000000000000026 /* KoinDropdownConfiguration.swift in Sources */, D208A7CD2CABB7A3007040E7 /* CoreAPI.swift in Sources */, 7CA6867B2FFCB758007937E9 /* DefaultNotificationHistoryRepository.swift in Sources */, 833D9C052C68F8BA00982145 /* ScreenActionType.swift in Sources */, From 3e7ffa89d6e607e82213d410594cd8641ab11364 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 21:55:43 +0900 Subject: [PATCH 073/101] =?UTF-8?q?refactor:=20CallVanPost=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9D=84=20KoinDropdown=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KoinPickerDropDownView.swift | 11 +++++ .../CallVanPostViewController.swift | 39 +++++++++++++-- .../Subviews/CallVanPostDateView.swift | 47 +++--------------- .../Subviews/CallVanPostTimeView.swift | 49 +++---------------- 4 files changed, 59 insertions(+), 87 deletions(-) diff --git a/Koin/Core/View/KoinPickerDropDownView/KoinPickerDropDownView.swift b/Koin/Core/View/KoinPickerDropDownView/KoinPickerDropDownView.swift index 0e7907a5..8efa0534 100644 --- a/Koin/Core/View/KoinPickerDropDownView/KoinPickerDropDownView.swift +++ b/Koin/Core/View/KoinPickerDropDownView/KoinPickerDropDownView.swift @@ -63,6 +63,16 @@ final class KoinPickerDropDownView: UIView { } } +// MARK: - KoinDropdownContentView +extension KoinPickerDropDownView: KoinDropdownContentView { + var dismissTappedPublisher: AnyPublisher { + applyButtonTappedPublisher.eraseToAnyPublisher() + } + var height: CGFloat { + return 153 + } +} + extension KoinPickerDropDownView { private func bind() { @@ -134,6 +144,7 @@ extension KoinPickerDropDownView { private func setUpConstraints() { pickerView.snp.makeConstraints { + $0.height.equalTo(90) $0.top.equalToSuperview().offset(12) $0.centerX.equalToSuperview() } diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index 6d11807e..5604e687 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -23,6 +23,8 @@ final class CallVanPostViewController: UIViewController { private var subscriptions: Set = [] // MARK: - UI Components + private let scrollView = UIScrollView() + private let scrollContentView = UIView() private let placeView = CallVanPostPlaceView() private let dateView = CallVanPostDateView() private let timeView = CallVanPostTimeView() @@ -34,6 +36,19 @@ final class CallVanPostViewController: UIViewController { private let bottomSheetContentView = CallVanPostPlaceBottomSheetView() + // MARK: - Dropdown + private lazy var dropdownHost = KoinDropdownHost(scrollView: scrollView) + private lazy var dateDropdown = dropdownHost.makeDropdown( + trigger: dateView.dropdownTrigger, + contentView: dateView.dropdownContentView, + configuration: .init(topPadding: 12, shadow: .shadow2) + ) + private lazy var timeDropdown = dropdownHost.makeDropdown( + trigger: timeView.dropdownTrigger, + contentView: timeView.dropdownContentView, + configuration: .init(topPadding: 12, shadow: .shadow2) + ) + // MARK: - Initializer init(viewModel: CallVanPostViewModel) { self.viewModel = viewModel @@ -97,7 +112,7 @@ final class CallVanPostViewController: UIViewController { }.store(in: &subscriptions) dateView.dateButtonTappedPublisher.receive(on: DispatchQueue.main).sink { [weak self] in - self?.timeView.dismissTimeDropDownView() + self?.dateDropdown.toggle() }.store(in: &subscriptions) dateView.dateChangedPublisher.sink { [weak self] date in @@ -105,7 +120,7 @@ final class CallVanPostViewController: UIViewController { }.store(in: &subscriptions) timeView.timeButtonTappedPublisher.receive(on: DispatchQueue.main).sink { [weak self] in - self?.dateView.dismissDateDropDownView() + self?.timeDropdown.toggle() }.store(in: &subscriptions) timeView.timeChangedPublisher.sink { [weak self] time in @@ -137,6 +152,8 @@ extension CallVanPostViewController { } @objc private func postButtonTapped() { + guard !dropdownHost.isPresenting else { return } + postButton.isUserInteractionEnabled = false inputSubject.send(.logEvent(label: EventParameter.EventLabel.Campus.callvanWriteDone, category: .click, value: "")) inputSubject.send(.postData) @@ -299,15 +316,26 @@ extension CallVanPostViewController { } } private func setUpLayouts() { - [placeView, participantsView, separatorView, descriptionLabel, postButton, - timeView, dateView].forEach { + [placeView, dateView, timeView, participantsView].forEach { + scrollContentView.addSubview($0) + } + scrollView.addSubview(scrollContentView) + [scrollView, separatorView, descriptionLabel, postButton].forEach { view.addSubview($0) } } private func setUpConstraints() { - placeView.snp.makeConstraints { + scrollView.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(separatorView.snp.top) + } + scrollContentView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalToSuperview() + } + placeView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() } dateView.snp.makeConstraints { $0.top.equalTo(placeView.snp.bottom) @@ -320,6 +348,7 @@ extension CallVanPostViewController { participantsView.snp.makeConstraints { $0.top.equalTo(timeView.snp.bottom) $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() } postButton.snp.makeConstraints { diff --git a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostDateView.swift b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostDateView.swift index 9104a621..14d2851b 100644 --- a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostDateView.swift +++ b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostDateView.swift @@ -10,7 +10,7 @@ import Combine import SnapKit import Then -final class CallVanPostDateView: ExtendedTouchAreaView { +final class CallVanPostDateView: UIView { // MARK: - Properties let dateButtonTappedPublisher = PassthroughSubject() @@ -21,6 +21,10 @@ final class CallVanPostDateView: ExtendedTouchAreaView { $0.dateFormat = "yyyy년 M월 d일" } + // MARK: - Dropdown + var dropdownTrigger: UIView { dateButton } + var dropdownContentView: UIView & KoinDropdownContentView { dateDropDownView } + // MARK: - UI Components private let titleLabel = UILabel() private let descriptionLabel = UILabel() @@ -58,9 +62,6 @@ extension CallVanPostDateView { }.store(in: &subscriptions) - dateDropDownView.applyButtonTappedPublisher.sink { [weak self] in - self?.dismissDateDropDownView() - }.store(in: &subscriptions) } } @@ -72,32 +73,6 @@ extension CallVanPostDateView { @objc private func dateButtonTapped() { dateButtonTappedPublisher.send() - - if dateDropDownView.isHidden { - presentDateDropDownView() - } else { - dismissDateDropDownView() - } - } - - private func presentDateDropDownView() { - dateDropDownView.isHidden = false - UIView.animate(springDuration: 0.3, bounce: 0.3, initialSpringVelocity: 0) { [weak self] in - guard let self else { return } - dateDropDownView.alpha = 1 - dateDropDownView.transform = CGAffineTransform.identity - } - } - - func dismissDateDropDownView() { - UIView.animate(springDuration: 0.2, bounce: 0, initialSpringVelocity: 0) { [weak self] in - guard let self else { return } - dateDropDownView.alpha = 0 - dateDropDownView.transform = CGAffineTransform(translationX: 0, y: -20) - } - DispatchQueue.main.asyncAfter(deadline: .now()+0.1 ) { [weak self] in - self?.dateDropDownView.isHidden = true - } } } @@ -135,16 +110,11 @@ extension CallVanPostDateView { dateDropDownView.do { $0.backgroundColor = UIColor.appColor(.neutral100) $0.layer.cornerRadius = 8 - $0.clipsToBounds = true - $0.layer.applySketchShadow(color: UIColor.appColor(.neutral800), alpha: 0.08, x: 0, y: 4, blur: 10, spread: 0) - $0.isHidden = true - $0.transform = CGAffineTransform(translationX: 0, y: -20) - $0.alpha = 0 } } private func setUpLayouts() { - [titleLabel, descriptionLabel, dateButton, dateLabel, downArrowImageView, dateDropDownView].forEach { + [titleLabel, descriptionLabel, dateButton, dateLabel, downArrowImageView].forEach { addSubview($0) } } @@ -174,10 +144,5 @@ extension CallVanPostDateView { $0.centerY.equalTo(dateButton) $0.trailing.equalTo(dateButton).offset(-12) } - dateDropDownView.snp.makeConstraints { - $0.height.equalTo(153) - $0.top.equalTo(dateButton.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(24) - } } } diff --git a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostTimeView.swift b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostTimeView.swift index 112b69a8..07e993b7 100644 --- a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostTimeView.swift +++ b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostTimeView.swift @@ -10,16 +10,21 @@ import Combine import SnapKit import Then -final class CallVanPostTimeView: ExtendedTouchAreaView { +final class CallVanPostTimeView: UIView { // MARK: - Properteis let timeButtonTappedPublisher = PassthroughSubject() let timeChangedPublisher = PassthroughSubject() + private var subscriptions: Set = [] private let formatter = DateFormatter().then { $0.locale = Locale(identifier: "ko_KR") } + // MARK: - Dropdown + var dropdownTrigger: UIView { timeButton } + var dropdownContentView: UIView & KoinDropdownContentView { timeDropDownView } + // MARK: - UI Components private let titleLabel = UILabel() private let descriptionLabel = UILabel() @@ -40,6 +45,7 @@ final class CallVanPostTimeView: ExtendedTouchAreaView { fatalError("init(coder:) has not been implemented") } + // MARK: - Public func update(_ date: Date) { timeDropDownView.reset(initialDate: date) @@ -66,9 +72,6 @@ extension CallVanPostTimeView { } }.store(in: &subscriptions) - timeDropDownView.applyButtonTappedPublisher.sink { [weak self] in - self?.dismissTimeDropDownView() - }.store(in: &subscriptions) } } @@ -80,32 +83,6 @@ extension CallVanPostTimeView { @objc private func timeButtonTapped() { timeButtonTappedPublisher.send() - - if timeDropDownView.isHidden { - presentTimeDropDownView() - } else { - dismissTimeDropDownView() - } - } - - private func presentTimeDropDownView() { - timeDropDownView.isHidden = false - UIView.animate(springDuration: 0.3, bounce: 0.3, initialSpringVelocity: 0) { [weak self] in - guard let self else { return } - timeDropDownView.alpha = 1 - timeDropDownView.transform = CGAffineTransform.identity - } - } - - func dismissTimeDropDownView() { - UIView.animate(springDuration: 0.2, bounce: 0, initialSpringVelocity: 0) { [weak self] in - guard let self else { return } - timeDropDownView.alpha = 0 - timeDropDownView.transform = CGAffineTransform(translationX: 0, y: -20) - } - DispatchQueue.main.asyncAfter(deadline: .now()+0.1 ) { [weak self] in - self?.timeDropDownView.isHidden = true - } } } @@ -121,11 +98,6 @@ extension CallVanPostTimeView { timeDropDownView.do { $0.backgroundColor = UIColor.appColor(.neutral100) $0.layer.cornerRadius = 8 - $0.clipsToBounds = true - $0.layer.applySketchShadow(color: UIColor.appColor(.neutral800), alpha: 0.08, x: 0, y: 4, blur: 10, spread: 0) - $0.isHidden = true - $0.transform = CGAffineTransform(translationX: 0, y: -20) - $0.alpha = 0 } titleLabel.do { $0.text = "출발 시각" @@ -159,7 +131,7 @@ extension CallVanPostTimeView { } private func setUpLayouts() { - [titleLabel, descriptionLabel, timeButton, amPmLabel, separatorView, timeLabel, timeDropDownView].forEach { + [titleLabel, descriptionLabel, timeButton, amPmLabel, separatorView, timeLabel].forEach { addSubview($0) } } @@ -192,10 +164,5 @@ extension CallVanPostTimeView { $0.centerY.equalTo(timeButton) $0.leading.equalTo(separatorView.snp.trailing).offset(16) } - timeDropDownView.snp.makeConstraints { - $0.height.equalTo(153) - $0.top.equalTo(timeButton.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(24) - } } } From dd4b1792e04d14ace34aa07a2fbd98736e7b55b8 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 22:55:56 +0900 Subject: [PATCH 074/101] =?UTF-8?q?refacotor:=20PostLostItem=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9D=84=20Koi?= =?UTF-8?q?nDropdown=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubViews/EditLostItemImagesView.swift | 6 -- .../PostLostItemViewController.swift | 25 +---- .../AddLostItemCollectionView.swift | 40 +++----- .../AddLostItemCollectionViewCell.swift | 99 +++++-------------- .../AddLostItemFooterView.swift | 2 - .../SubViews/DatePickerDropdownView.swift | 11 +++ .../LostArticleImageCollectionView.swift | 4 - .../LostArticleImageCollectionViewCell.swift | 2 - 8 files changed, 51 insertions(+), 138 deletions(-) diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift index 7a7eafce..73e8c6b3 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift @@ -79,12 +79,6 @@ final class EditLostItemImagesView: UIView { self?.addPictureButton.isEnabled = urls.count < 10 self?.pictureCountLabel.text = "\(urls.count)/10" }.store(in: &subscriptions) - - imageUploadCollectionView.shouldDismissDropDownKeyBoardPublisher.sink { [weak self] in - self?.dismissDropDownPublisher.send() - self?.endEditing(true) - }.store(in: &subscriptions) - } private func setAddTargets() { diff --git a/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift b/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift index d89f1ade..545ef282 100644 --- a/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift +++ b/Koin/Presentation/LostItem/PostLostItem/PostLostItemViewController.swift @@ -75,7 +75,7 @@ final class PostLostItemViewController: UIViewController { title = "\(viewModel.type.description)물 신고" addLostItemCollectionView.setType(type: viewModel.type) - configureTapGestureToDismissKeyboardDropdown() + hideKeyboardWhenTappedAround() } override func viewWillAppear(_ animated: Bool) { @@ -108,10 +108,6 @@ final class PostLostItemViewController: UIViewController { addLostItemCollectionView.logPublisher.sink { [weak self] value in self?.inputSubject.send(.logEvent(value.0, value.1, value.2)) }.store(in: &subscriptions) - - addLostItemCollectionView.shouldDismissKeyBoardPublisher.sink { [weak self] in - self?.dismissKeyboard() - }.store(in: &subscriptions) } } @@ -138,6 +134,8 @@ extension PostLostItemViewController { return } + guard !addLostItemCollectionView.isDropdownPresenting else { return } + let contentInset = UIEdgeInsets( top: 0, left: 0, @@ -160,6 +158,8 @@ extension PostLostItemViewController { } @objc private func keyBoardWillHide(_ notification: NSNotification) { + guard !addLostItemCollectionView.isDropdownPresenting else { return } + let contentInset = UIEdgeInsets.zero addLostItemCollectionView.contentInset = contentInset @@ -187,8 +187,6 @@ extension PostLostItemViewController: UITextViewDelegate, PHPickerViewController return allCellData } private func writeButtonTapped() { - dismissKeyboardDropdown() - var isAllValid = true for index in 0..() let uploadImageButtonPublisher = PassthroughSubject() - let shouldDismissKeyBoardPublisher = PassthroughSubject() let logPublisher = PassthroughSubject<(EventLabelType, EventParameter.EventCategory, Any), Never>() private var type: LostItemType = .lost private var articles: [PostLostItemRequest] = [] + var isDropdownPresenting: Bool { + dropdownHost.isPresenting + } + // MARK: - Initializer override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { let flowLayout = UICollectionViewFlowLayout() @@ -52,6 +58,10 @@ final class AddLostItemCollectionView: UICollectionView { extension AddLostItemCollectionView { + func dismissDropdown() { + dropdownHost.dismissPresented() + } + func setType(type: LostItemType) { self.type = type reloadData() @@ -62,15 +72,6 @@ extension AddLostItemCollectionView { collectionViewLayout.invalidateLayout() } - func dismissDatePicker(_ currentIndexPath: IndexPath?) { - for row in 0.. UIView? { var addLostItemCollectionViewCells: [AddLostItemCollectionViewCell] = [] @@ -126,19 +127,7 @@ extension AddLostItemCollectionView: UICollectionViewDataSource { cell.imageUrlsPublisher.sink { [weak self] urls in self?.articles[indexPath.row].images = urls }.store(in: &cell.cancellables) - cell.shouldDismissDropDownPublisher.sink { [weak self] indexPath in - self?.dismissDatePicker(indexPath) - }.store(in: &cell.cancellables) - cell.shouldDismissKeyBoardPublisher.sink { [weak self] in - self?.shouldDismissKeyBoardPublisher.send() - }.store(in: &cell.cancellables) - cell.focusDropdownPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] targetView in - var rect = targetView.convert(targetView.bounds, to: self) - rect.size.height += 15 - self?.scrollRectToVisible(rect, animated: true) - }.store(in: &cell.cancellables) + cell.prepareDropdown(host: dropdownHost) return cell } @@ -162,7 +151,7 @@ extension AddLostItemCollectionView: UICollectionViewDataSource { formatter.dateFormat = "yyyy년 M월 d일" return formatter.string(from: Date()) }() - self?.dismissDatePicker(nil) + self?.dismissDropdown() DispatchQueue.main.asyncAfter(deadline: .now()+0.1) { self?.articles.append(PostLostItemRequest(type: .found, category: "", location: "", foundDate: formattedDate, content: "", images: [], registeredAt: "", updatedAt: "")) self?.reloadData() @@ -172,9 +161,6 @@ extension AddLostItemCollectionView: UICollectionViewDataSource { case .lost: self?.logPublisher.send((EventParameter.EventLabel.Campus.lostItemAddItem, .click, "물품 추가")) } }.store(in: &footerCancellables) - footerView.shouldDismissDropDownPublisher.sink { [weak self] in - self?.dismissDatePicker(nil) - }.store(in: &footerCancellables) return footerView } return UICollectionReusableView() diff --git a/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemCollectionViewCell.swift b/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemCollectionViewCell.swift index 34a50b47..ea57d727 100644 --- a/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemCollectionViewCell.swift +++ b/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemCollectionViewCell.swift @@ -20,9 +20,6 @@ final class AddLostItemCollectionViewCell: UICollectionViewCell { let locationPublisher = PassthroughSubject() let contentPublisher = PassthroughSubject() let imageUrlsPublisher = PassthroughSubject<[String], Never>() - let shouldDismissDropDownPublisher = PassthroughSubject() - let shouldDismissKeyBoardPublisher = PassthroughSubject() - let focusDropdownPublisher = PassthroughSubject() private var type: LostItemType = .lost private var textViewPlaceHolder = "" @@ -141,17 +138,7 @@ final class AddLostItemCollectionViewCell: UICollectionViewCell { $0.contentHorizontalAlignment = .left $0.titleEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) } - - private lazy var dropdownView = DatePickerDropdownView().then { - $0.backgroundColor = UIColor.appColor(.neutral100) - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - $0.layer.applySketchShadow(color: UIColor.appColor(.neutral800), alpha: 0.08, x: 0, y: 4, blur: 10, spread: 0) - $0.isHidden = true - $0.transform = CGAffineTransform(translationX: 0, y: -20) - $0.alpha = 0 - } - + private let locationLabel = UILabel().then { _ in } private let locationEssentialLabel = UILabel().then { @@ -196,6 +183,14 @@ final class AddLostItemCollectionViewCell: UICollectionViewCell { $0.text = textViewPlaceHolder } + // MARK: - Dropdown + private lazy var dropdownView = DatePickerDropdownView().then { + $0.backgroundColor = UIColor.appColor(.neutral100) + $0.layer.cornerRadius = 12 + } + + private var dropdown: KoinDropdown? + // MARK: - Initializer override init(frame: CGRect) { super.init(frame: frame) @@ -217,22 +212,25 @@ final class AddLostItemCollectionViewCell: UICollectionViewCell { self?.pictureCountLabel.text = "\(urls.count)/10" self?.imageUrlsPublisher.send(urls) }.store(in: &cancellable) - imageUploadCollectionView.shouldDismissDropDownKeyBoardPublisher.sink { [weak self] in - self?.shouldDismissDropDownPublisher.send(nil) - self?.shouldDismissKeyBoardPublisher.send() - }.store(in: &cancellable) dropdownView.valueChangedPublisher.sink { [weak self] in self?.dropdownValueChanged() }.store(in: &cancellable) - dropdownView.dismissDropdownPublisher.sink { [weak self] in - self?.shouldDismissDropDownPublisher.send(nil) - }.store(in: &cancellable) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - PrepareForReuse + /// ScrollView 를 아는 CollectionView 가 Host 를 넘겨준다. 셀당 한 번만 만든다. + func prepareDropdown(host: KoinDropdownHost) { + guard dropdown == nil else { return } + dropdown = host.makeDropdown( + trigger: dateButton, + contentView: dropdownView, + configuration: .init(topPadding: 4, shadow: .shadow2) + ) + } + override func prepareForReuse() { super.prepareForReuse() cancellables.forEach { $0.cancel() } @@ -339,9 +337,6 @@ extension AddLostItemCollectionViewCell { extension AddLostItemCollectionViewCell{ @objc private func addImageButtonTapped() { - shouldDismissDropDownPublisher.send(nil) - shouldDismissKeyBoardPublisher.send() - addImageButtonPublisher.send() // TODO: 높이 해결 @@ -356,15 +351,10 @@ extension AddLostItemCollectionViewCell{ } @objc private func deleteCellButtonTapped() { - shouldDismissDropDownPublisher.send(nil) - shouldDismissKeyBoardPublisher.send() deleteButtonPublisher.send() } @objc private func stackButtonTapped(_ sender: UIButton) { - shouldDismissDropDownPublisher.send(nil) - shouldDismissKeyBoardPublisher.send() - categoryWarningLabel.isHidden = true categoryPublisher.send(sender.titleLabel?.text ?? "") categoryStackView.arrangedSubviews.forEach { view in @@ -475,43 +465,8 @@ extension AddLostItemCollectionViewCell { // MARK: - dropdown 열기/닫기 @objc private func dateButtonTapped(button: UIButton) { - if dropdownView.isHidden { - presentDropdown() - shouldDismissKeyBoardPublisher.send() - focusDropdownPublisher.send(dropdownView) - } else { - dismissDropdown() - } - } - - private func presentDropdown() { - - guard let text = itemCountLabel.text, - let lastCharacter = text.last, - let row = Int(String(lastCharacter)) else { - return - } - let indexPath = IndexPath(row: row - 1, section: 0) - - shouldDismissDropDownPublisher.send(indexPath) - - dropdownView.isHidden = false - UIView.animate(withDuration: 0.2) { [weak self] in - guard let self else { return } - dropdownView.alpha = 1 - dropdownView.transform = CGAffineTransform(translationX: 0, y: 0) - } - } - - @objc func dismissDropdown() { - UIView.animate(withDuration: 0.1) { [weak self] in - guard let self else { return } - dropdownView.alpha = 0 - dropdownView.transform = CGAffineTransform(translationX: 0, y: -20) - } - DispatchQueue.main.asyncAfter(deadline: .now()+0.1 ) { [weak self] in - self?.dropdownView.isHidden = true - } + // KoinDropdown 은 표시 중 키보드가 없다고 전제한다. 먼저 내린다. + dropdown?.toggle() } private func dropdownValueChanged() { @@ -531,9 +486,6 @@ extension AddLostItemCollectionViewCell: UITextViewDelegate { // MARK: 내용 수정 시작 func textViewDidBeginEditing(_ textView: UITextView) { - // 열려있는 드롭다운 닫기 - shouldDismissDropDownPublisher.send(nil) - // placeholder 비우기 if textView.text == textViewPlaceHolder && textView.textColor == UIColor.appColor(.neutral500) { textView.text = "" @@ -574,9 +526,6 @@ extension AddLostItemCollectionViewCell: UITextFieldDelegate { // MARK: 장소 수정 시작 func textFieldDidBeginEditing(_ textField: UITextField) { - // 열려있는 dropdown 닫기 - shouldDismissDropDownPublisher.send(nil) - // placeholder 비우기 if textField.textColor == UIColor.appColor(.neutral500) { textField.text = "" @@ -612,7 +561,7 @@ extension AddLostItemCollectionViewCell: UITextFieldDelegate { extension AddLostItemCollectionViewCell { private func setUpLayouts() { - [separateView, itemCountLabel, pictureLabel, pictureMessageLabel, pictureCountLabel, addPictureButton, categoryLabel, categoryMessageLabel, categoryStackView, dateLabel, locationLabel, locationTextField, contentLabel, contentTextCountLabel, contentTextView, deleteCellButton, categoryWarningLabel, dateWarningLabel, locationWarningLabel, imageUploadCollectionView, categoryEssentialLabel, dateEssentialLabel, locationEssentialLabel, dropdownView, dateButton].forEach { + [separateView, itemCountLabel, pictureLabel, pictureMessageLabel, pictureCountLabel, addPictureButton, categoryLabel, categoryMessageLabel, categoryStackView, dateLabel, locationLabel, locationTextField, contentLabel, contentTextCountLabel, contentTextView, deleteCellButton, categoryWarningLabel, dateWarningLabel, locationWarningLabel, imageUploadCollectionView, categoryEssentialLabel, dateEssentialLabel, locationEssentialLabel, dateButton].forEach { contentView.addSubview($0) } dateButton.addSubview(chevronImage) @@ -700,10 +649,6 @@ extension AddLostItemCollectionViewCell { $0.trailing.equalTo(contentView.snp.trailing).offset(-24) $0.height.equalTo(40) } - dropdownView.snp.makeConstraints { - $0.top.equalTo(dateButton.snp.bottom).offset(4) - $0.leading.trailing.equalTo(dateButton) - } chevronImage.snp.makeConstraints { make in make.centerY.equalToSuperview() diff --git a/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemFooterView.swift b/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemFooterView.swift index 6f0fc52b..697aec1f 100644 --- a/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemFooterView.swift +++ b/Koin/Presentation/LostItem/PostLostItem/SubViews/AddLostItemCollectionView/AddLostItemFooterView.swift @@ -12,7 +12,6 @@ final class AddLostItemFooterView: UICollectionReusableView { static let identifier = "AddLostItemFooterView" let addItemButtonPublisher = PassthroughSubject() - let shouldDismissDropDownPublisher = PassthroughSubject() private let addItemButton = UIButton().then { var configuration = UIButton.Configuration.plain() @@ -43,7 +42,6 @@ final class AddLostItemFooterView: UICollectionReusableView { extension AddLostItemFooterView { @objc private func addItemButtonTapped() { addItemButtonPublisher.send() - shouldDismissDropDownPublisher.send() self.endEditing(true) } diff --git a/Koin/Presentation/LostItem/PostLostItem/SubViews/DatePickerDropdownView.swift b/Koin/Presentation/LostItem/PostLostItem/SubViews/DatePickerDropdownView.swift index 79413830..b9f1423c 100644 --- a/Koin/Presentation/LostItem/PostLostItem/SubViews/DatePickerDropdownView.swift +++ b/Koin/Presentation/LostItem/PostLostItem/SubViews/DatePickerDropdownView.swift @@ -61,6 +61,17 @@ final class DatePickerDropdownView: UIView { } } +// MARK: - KoinDropdownContentView + +extension DatePickerDropdownView: KoinDropdownContentView { + var dismissTappedPublisher: AnyPublisher { + dismissDropdownPublisher.eraseToAnyPublisher() + } + var height: CGFloat { + return 161 + } +} + extension DatePickerDropdownView { private func setAddTargets() { diff --git a/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionView.swift b/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionView.swift index 59c968fb..559471d8 100644 --- a/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionView.swift +++ b/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionView.swift @@ -10,7 +10,6 @@ import UIKit final class LostItemImageCollectionView: UICollectionView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - let shouldDismissDropDownKeyBoardPublisher = PassthroughSubject() let imageCountPublisher = PassthroughSubject<[String], Never>() private(set) var imageUrls: [String] = [] { didSet { @@ -59,9 +58,6 @@ extension LostItemImageCollectionView { self?.imageUrls.remove(at: indexPath.row) self?.reloadData() }.store(in: &cell.cancellables) - cell.shouldDismissDropDownKeyBoardPublisher.sink { [weak self] in - self?.shouldDismissDropDownKeyBoardPublisher.send() - }.store(in: &cell.cancellables) return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { diff --git a/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionViewCell.swift b/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionViewCell.swift index db00f6cb..dbe5d703 100644 --- a/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionViewCell.swift +++ b/Koin/Presentation/LostItem/PostLostItem/SubViews/LostItemImageCollectionView/LostArticleImageCollectionViewCell.swift @@ -11,7 +11,6 @@ import UIKit final class LostItemImageCollectionViewCell: UICollectionViewCell { // MARK: - Properties - let shouldDismissDropDownKeyBoardPublisher = PassthroughSubject() let cancelButtonPublisher = PassthroughSubject() var cancellables = Set() @@ -47,7 +46,6 @@ final class LostItemImageCollectionViewCell: UICollectionViewCell { } @objc private func cancelButtonTapped() { - shouldDismissDropDownKeyBoardPublisher.send() cancelButtonPublisher.send(()) } From 65221c5580b10a1fb9364f4212456c999d5bf3bd Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 23:02:20 +0900 Subject: [PATCH 075/101] =?UTF-8?q?refactor:=20EditLostItem=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9D=84=20Koi?= =?UTF-8?q?nDropdown=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditLostItemViewController.swift | 40 ++++-------- .../SubViews/EditLostItemCategoryView.swift | 2 - .../SubViews/EditLostItemContentView.swift | 3 - .../SubViews/EditLostItemFoundDateView.swift | 62 ++++++------------- .../SubViews/EditLostItemFoundPlaceView.swift | 3 - .../SubViews/EditLostItemImagesView.swift | 2 - 6 files changed, 30 insertions(+), 82 deletions(-) diff --git a/Koin/Presentation/LostItem/EditLostItem/EditLostItemViewController.swift b/Koin/Presentation/LostItem/EditLostItem/EditLostItemViewController.swift index 5bb73423..87ab2e0e 100644 --- a/Koin/Presentation/LostItem/EditLostItem/EditLostItemViewController.swift +++ b/Koin/Presentation/LostItem/EditLostItem/EditLostItemViewController.swift @@ -58,6 +58,9 @@ final class EditLostItemViewController: UIViewController { $0.layer.masksToBounds = true } + // MARK: - Dropdown + private lazy var dropdownHost = KoinDropdownHost(scrollView: scrollView) + // MARK: - Initializer init(viewModel: EditLostItemViewModel) { self.viewModel = viewModel @@ -87,11 +90,7 @@ final class EditLostItemViewController: UIViewController { configureNavigationBar(style: .empty) } - override func hideKeyboardWhenTappedAround() { - super.hideKeyboardWhenTappedAround() - foundDateView.dismissDropdown() - } - + // MARK: - Bind private func bind() { viewModel.transform(with: inputSubject.eraseToAnyPublisher()).sink { [weak self] output in guard let self else { return } @@ -110,28 +109,7 @@ final class EditLostItemViewController: UIViewController { self?.addImageButtonTapped() }.store(in: &subscriptions) - imagesView.dismissDropDownPublisher.sink { [weak self] in - self?.foundDateView.dismissDropdown() - }.store(in: &subscriptions) - - categoryView.dismissDropDownPublisher.sink { [weak self] in - self?.foundDateView.dismissDropdown() - }.store(in: &subscriptions) - - foundPlaceView.shouldDismissDropDownPublisher.sink { [weak self] in - self?.foundDateView.dismissDropdown() - }.store(in: &subscriptions) - - contentView.shouldDismissDropDownPublisher.sink { [weak self] in - self?.foundDateView.dismissDropdown() - }.store(in: &subscriptions) - - foundDateView.focusDropdownPublisher.sink { [weak self] targetView in - guard let self else { return } - var rect = targetView.convert(targetView.bounds, to: scrollView) - rect.size.height += 15 - scrollView.scrollRectToVisible(rect, animated: true) - }.store(in: &subscriptions) + foundDateView.prepareDropdown(host: dropdownHost) } } @@ -142,8 +120,10 @@ extension EditLostItemViewController { } @objc private func editButtonTapped() { + guard !dropdownHost.isPresenting else { + return + } dismissKeyboard() - foundDateView.dismissDropdown() if foundDateView.isValid && foundPlaceView.isValid { let imageUrls = imagesView.imageUploadCollectionView.imageUrls @@ -194,6 +174,8 @@ extension EditLostItemViewController { return } + guard !dropdownHost.isPresenting else { return } + let contentInset = UIEdgeInsets( top: 0, left: 0, @@ -218,6 +200,8 @@ extension EditLostItemViewController { } @objc private func keyBoardWillHide(_ notification: NSNotification) { + guard !dropdownHost.isPresenting else { return } + let contentInset = UIEdgeInsets.zero scrollView.contentInset = contentInset scrollView.scrollIndicatorInsets = contentInset diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemCategoryView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemCategoryView.swift index 93089615..bbecf0b3 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemCategoryView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemCategoryView.swift @@ -13,7 +13,6 @@ final class EditLostItemCategoryView: UIView { // MARK: - Properties @Published private(set) var selectedCategory: String = "" private var subscriptions: Set = [] - let dismissDropDownPublisher = PassthroughSubject() // MARK: - UI Components private let categoryLabel = UILabel().then { @@ -84,7 +83,6 @@ extension EditLostItemCategoryView { @objc private func buttonTapped(_ sender: EditLostItemButton) { selectedCategory = sender.title - dismissDropDownPublisher.send() endEditing(true) } } diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemContentView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemContentView.swift index d5646876..6cefdfd5 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemContentView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemContentView.swift @@ -15,7 +15,6 @@ final class EditLostItemContentView: UIView { private var content: String? private let maxCharacters = 1000 private lazy var textViewPlaceHolder = "물품이나 \(type.description) 장소에 대한 추가 설명이 있다면 작성해주세요." - let shouldDismissDropDownPublisher = PassthroughSubject() // MARK: - UI Components private let contentLabel = UILabel().then { @@ -72,8 +71,6 @@ extension EditLostItemContentView: UITextViewDelegate { // MARK: 내용 수정 시작 func textViewDidBeginEditing(_ textView: UITextView) { - shouldDismissDropDownPublisher.send() - // placeholder 비우기 if textView.text == textViewPlaceHolder && textView.textColor == UIColor.appColor(.neutral500) { textView.text = "" diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift index 72afd3c3..4940f022 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift @@ -8,7 +8,7 @@ import UIKit import Combine -final class EditLostItemFoundDateView: ExtendedTouchAreaView { +final class EditLostItemFoundDateView: UIView { // MARK: - Properties private var type: LostItemType @@ -18,7 +18,6 @@ final class EditLostItemFoundDateView: ExtendedTouchAreaView { dateWarningLabel.isHidden } private(set) var foundDate: String - let focusDropdownPublisher = PassthroughSubject() // MARK: - UI Components private lazy var dateLabel = UILabel().then { @@ -68,12 +67,12 @@ final class EditLostItemFoundDateView: ExtendedTouchAreaView { private lazy var dropdownView = DatePickerDropdownView().then { $0.backgroundColor = UIColor.appColor(.neutral100) $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - $0.layer.applySketchShadow(color: UIColor.appColor(.neutral800), alpha: 0.08, x: 0, y: 4, blur: 10, spread: 0) - $0.isHidden = true - $0.transform = CGAffineTransform(translationX: 0, y: -20) - $0.alpha = 0 } + + // MARK: - Dropdown + var dropdownTrigger: UIView { dateButton } + var dropdownContentView: UIView & KoinDropdownContentView { dropdownView } + private var dropdown: KoinDropdown? // MARK: - Initializer init(type: LostItemType, foundDate: String) { @@ -107,9 +106,6 @@ final class EditLostItemFoundDateView: ExtendedTouchAreaView { dropdownView.valueChangedPublisher.sink { [weak self] in self?.dropdownValueChanged() }.store(in: &subscriptions) - dropdownView.dismissDropdownPublisher.sink { [weak self] in - self?.dismissDropdown() - }.store(in: &subscriptions) } private func setAddTargets() { @@ -117,36 +113,18 @@ final class EditLostItemFoundDateView: ExtendedTouchAreaView { } @objc private func dateButtonTapped(button: UIButton) { - if dropdownView.isHidden { - presentDropdown() - endEditing(true) - focusDropdownPublisher.send(dropdownView) - } else { - dismissDropdown() - } - } - - private func presentDropdown() { - // 열려있는 키보드 닫기 - self.endEditing(true) - - dropdownView.isHidden = false - UIView.animate(withDuration: 0.2) { [weak self] in - guard let self else { return } - dropdownView.alpha = 1 - dropdownView.transform = CGAffineTransform(translationX: 0, y: 0) - } + endEditing(true) + dropdown?.toggle() } - - @objc func dismissDropdown() { - UIView.animate(withDuration: 0.2) { [weak self] in - guard let self else { return } - dropdownView.alpha = 0 - dropdownView.transform = CGAffineTransform(translationX: 0, y: -20) - } - DispatchQueue.main.asyncAfter(deadline: .now()+0.1 ) { [weak self] in - self?.dropdownView.isHidden = true - } + + /// ScrollView 를 아는 호출부가 Host 를 넘겨준다. + func prepareDropdown(host: KoinDropdownHost) { + guard dropdown == nil else { return } + dropdown = host.makeDropdown( + trigger: dateButton, + contentView: dropdownView, + configuration: .init(topPadding: 4, shadow: .shadow2) + ) } private func dropdownValueChanged() { @@ -167,7 +145,7 @@ final class EditLostItemFoundDateView: ExtendedTouchAreaView { extension EditLostItemFoundDateView { private func setUpLayouts() { - [dateLabel, dateWarningLabel, dateButton, chevronImage, dropdownView, essentialLabel].forEach { + [dateLabel, dateWarningLabel, dateButton, chevronImage, essentialLabel].forEach { addSubview($0) } } @@ -195,10 +173,6 @@ extension EditLostItemFoundDateView { $0.centerY.equalTo(dateButton) $0.trailing.equalTo(dateButton.snp.trailing).offset(-16) } - dropdownView.snp.makeConstraints { - $0.top.equalTo(dateButton.snp.bottom).offset(4) - $0.leading.trailing.equalTo(dateButton) - } } private func configureView() { diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundPlaceView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundPlaceView.swift index 0c138e9e..5ac3d7a3 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundPlaceView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundPlaceView.swift @@ -13,7 +13,6 @@ final class EditLostItemFoundPlaceView: UIView { // MARK: - Properties private var type: LostItemType private lazy var textFieldPlaceHolder = "\(type.description) 장소를 입력해주세요." - let shouldDismissDropDownPublisher = PassthroughSubject() var isValid: Bool { locationWarningLabel.isHidden @@ -107,8 +106,6 @@ extension EditLostItemFoundPlaceView: UITextFieldDelegate { // MARK: 장소 수정 시작 func textFieldDidBeginEditing(_ textField: UITextField) { - shouldDismissDropDownPublisher.send() - // placeholder 비우기 if textField.textColor == UIColor.appColor(.neutral500) { textField.text = "" diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift index 73e8c6b3..51cb1f3f 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemImagesView.swift @@ -13,7 +13,6 @@ final class EditLostItemImagesView: UIView { // MARK: - Properties private var type: LostItemType private var images: [AppImage] - let dismissDropDownPublisher = PassthroughSubject() let addImageButtonPublisher = PassthroughSubject() private var subscriptions: Set = [] @@ -86,7 +85,6 @@ final class EditLostItemImagesView: UIView { } @objc private func addImageButtonTapped() { - dismissDropDownPublisher.send() addImageButtonPublisher.send() endEditing(true) } From 7136291a842090986338a61daa750761361bfeb1 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 25 Aug 2026 23:02:35 +0900 Subject: [PATCH 076/101] =?UTF-8?q?delete:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20ImageDropdownCell=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageDropDownCell.swift | 52 ------------------- koin.xcodeproj/project.pbxproj | 4 -- 2 files changed, 56 deletions(-) delete mode 100644 Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewListCollectionView/ImageDropDownCell.swift diff --git a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewListCollectionView/ImageDropDownCell.swift b/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewListCollectionView/ImageDropDownCell.swift deleted file mode 100644 index c7879cad..00000000 --- a/Koin/Presentation/Shop/ShopReviewList/SubViews/ReviewListCollectionView/ImageDropDownCell.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ImageDropDownCell.swift -// koin -// -// Created by 김나훈 on 8/12/24. -// - -import Then -import UIKit - -final class ImageDropDownCell: DropDownCell { - - - private let dropDownLabel = UILabel().then { - $0.textColor = UIColor.appColor(.neutral800) - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - } - - private let dropDownImageView = UIImageView().then { _ in - } - - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(text: String, image: UIImage?) { - dropDownLabel.text = text - dropDownImageView.image = image - } - - private func setupView() { - [dropDownLabel, dropDownImageView].forEach { - contentView.addSubview($0) - } - dropDownLabel.snp.makeConstraints { make in - make.centerY.equalTo(contentView.snp.centerY) - make.leading.equalTo(contentView.snp.leading).offset(10) - } - dropDownImageView.snp.makeConstraints { make in - make.centerY.equalTo(contentView.snp.centerY) - make.leading.equalTo(dropDownLabel.snp.trailing).offset(5) - make.width.equalTo(16) - make.height.equalTo(16) - } - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 73b04528..84fdbd5d 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -582,7 +582,6 @@ 7CED91CE2EF2D1F900457128 /* NotiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F142EF2D1F900457128 /* NotiViewModel.swift */; }; 7CED91CF2EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8F622EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift */; }; 7CED91D22EF2D1F900457128 /* LandImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90B32EF2D1F900457128 /* LandImageCollectionViewCell.swift */; }; - 7CED91D52EF2D1F900457128 /* ImageDropDownCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED90082EF2D1F900457128 /* ImageDropDownCell.swift */; }; 7CED91D72EF2D1F900457128 /* ShopDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FE82EF2D1F900457128 /* ShopDetailViewModel.swift */; }; 7CED91D82EF2D1F900457128 /* ClassComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED908A2EF2D1F900457128 /* ClassComponentView.swift */; }; 7CED91DD2EF2D1F900457128 /* ShopDetailTableViewNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CED8FE02EF2D1F900457128 /* ShopDetailTableViewNameCell.swift */; }; @@ -1566,7 +1565,6 @@ 7CED90052EF2D1F900457128 /* ReviewListCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListCollectionView.swift; sourceTree = ""; }; 7CED90062EF2D1F900457128 /* ReviewListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListCollectionViewCell.swift; sourceTree = ""; }; 7CED90072EF2D1F900457128 /* ReviewListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListHeaderView.swift; sourceTree = ""; }; - 7CED90082EF2D1F900457128 /* ImageDropDownCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDropDownCell.swift; sourceTree = ""; }; 7CED900A2EF2D1F900457128 /* ScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreView.swift; sourceTree = ""; }; 7CED900B2EF2D1F900457128 /* NonReviewListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonReviewListView.swift; sourceTree = ""; }; 7CED900F2EF2D1F900457128 /* ReviewListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewListViewController.swift; sourceTree = ""; }; @@ -4001,7 +3999,6 @@ 7CED90052EF2D1F900457128 /* ReviewListCollectionView.swift */, 7CED90062EF2D1F900457128 /* ReviewListCollectionViewCell.swift */, 7CED90072EF2D1F900457128 /* ReviewListHeaderView.swift */, - 7CED90082EF2D1F900457128 /* ImageDropDownCell.swift */, ); path = ReviewListCollectionView; sourceTree = ""; @@ -6426,7 +6423,6 @@ 7CED91CE2EF2D1F900457128 /* NotiViewModel.swift in Sources */, 7CED91CF2EF2D1F900457128 /* ManageNoticeKeywordViewModel.swift in Sources */, 7CED91D22EF2D1F900457128 /* LandImageCollectionViewCell.swift in Sources */, - 7CED91D52EF2D1F900457128 /* ImageDropDownCell.swift in Sources */, 7CED91D72EF2D1F900457128 /* ShopDetailViewModel.swift in Sources */, 7C01656F300B4A2D0013DD7B /* DepartmentView.swift in Sources */, 7C7F429B2F629B8800CC5860 /* UploadDomain.swift in Sources */, From 8aa78db856e6b1ee177e8d82fa9c9ded5ff962e6 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Wed, 26 Aug 2026 00:04:17 +0900 Subject: [PATCH 077/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helper/KoinDropdownAnimator.swift | 11 +++------ .../Core/View/KoinDropdown/KoinDropdown.swift | 23 ++++++------------- .../View/KoinDropdown/KoinDropdownHost.swift | 15 ++++-------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift b/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift index d430704c..9b38f73f 100644 --- a/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift +++ b/Koin/Core/View/KoinDropdown/Helper/KoinDropdownAnimator.swift @@ -13,7 +13,7 @@ final class KoinDropdownAnimator { enum Metric { static let presentDuration: TimeInterval = 0.3 static let presentBounce: CGFloat = 0.15 - static let dismissDuration: TimeInterval = 0.15 + static let dismissDuration: TimeInterval = 0.2 } private func hiddenTransform(travel: CGFloat) -> CGAffineTransform { @@ -31,8 +31,7 @@ final class KoinDropdownAnimator { UIView.animate( springDuration: Metric.presentDuration, - bounce: Metric.presentBounce, - initialSpringVelocity: 0 + bounce: Metric.presentBounce ) { view.alpha = 1 view.transform = .identity @@ -46,11 +45,7 @@ final class KoinDropdownAnimator { travel: CGFloat, completion: (() -> Void)? = nil ) { - UIView.animate( - springDuration: Metric.dismissDuration, - initialSpringVelocity: 0, - options: [.beginFromCurrentState, .allowUserInteraction] - ) { + UIView.animate(springDuration: Metric.dismissDuration) { view.alpha = 0 view.transform = self.hiddenTransform(travel: travel) } completion: { _ in diff --git a/Koin/Core/View/KoinDropdown/KoinDropdown.swift b/Koin/Core/View/KoinDropdown/KoinDropdown.swift index 82607a93..400beca9 100644 --- a/Koin/Core/View/KoinDropdown/KoinDropdown.swift +++ b/Koin/Core/View/KoinDropdown/KoinDropdown.swift @@ -16,15 +16,7 @@ protocol KoinDropdownContentView: AnyObject { @MainActor final class KoinDropdown: UIView { - - enum State { - case dismissed - case presented - } - - // MARK: - State - private(set) var state: State = .dismissed - + // MARK: - Properties private weak var host: KoinDropdownHost? private(set) weak var trigger: UIView? @@ -36,7 +28,7 @@ final class KoinDropdown: UIView { // MARK: - Animation let animator = KoinDropdownAnimator() - private var travel: CGFloat = 0 + private(set) var travel: CGFloat = 0 // MARK: - Initializer init( @@ -85,8 +77,8 @@ final class KoinDropdown: UIView { extension KoinDropdown { - func calculateTravel(in space: UIView) -> CGFloat? { - guard let trigger else { return nil } + func layout(in space: UIView) -> Bool { + guard let trigger else { return false } contentView.transform = .identity let triggerFrame = trigger.convert(trigger.bounds, to: space) @@ -95,7 +87,7 @@ extension KoinDropdown { guard 0 < triggerFrame.width, 0 < panelHeight else { - return nil + return false } self.frame = CGRect( x: triggerFrame.minX - padding.left, @@ -112,16 +104,15 @@ extension KoinDropdown { applyShadow() travel = panelHeight + configuration.topPadding - return travel + return true } + func animatePresent() { - state = .presented animator.present(view: contentView, travel: travel) } func animateDismiss(completion: @escaping () -> Void) { - state = .dismissed animator.dismiss( view: contentView, travel: travel, diff --git a/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift b/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift index 439d2364..44179ffa 100644 --- a/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift +++ b/Koin/Core/View/KoinDropdown/KoinDropdownHost.swift @@ -17,7 +17,6 @@ final class KoinDropdownHost { // MARK: - Properties private weak var scrollView: UIScrollView? private var overlay: UIView? - private var dropdowns: [KoinDropdown] = [] private var presentedDropdown: KoinDropdown? private var addedBottomInset: CGFloat = 0 @@ -37,14 +36,12 @@ final class KoinDropdownHost { contentView: UIView & KoinDropdownContentView, configuration: KoinDropdownConfiguration ) -> KoinDropdown { - let dropdown = KoinDropdown( + return KoinDropdown( host: self, trigger: trigger, contentView: contentView, configuration: configuration ) - dropdowns.append(dropdown) - return dropdown } } @@ -52,7 +49,7 @@ extension KoinDropdownHost { // MARK: - Toggle func toggle(_ dropdown: KoinDropdown) { - if dropdown.state == .presented { + if presentedDropdown === dropdown { dismiss(dropdown) } else { present(dropdown) @@ -74,7 +71,7 @@ extension KoinDropdownHost { self.overlay = overlay // overlay에 들어가는 dropdown - guard let travel = dropdown.calculateTravel(in: overlay) else { + guard dropdown.layout(in: overlay) else { overlay.removeFromSuperview() self.overlay = nil return @@ -87,7 +84,7 @@ extension KoinDropdownHost { // present addedBottomInset = addBottomInset( - travel: travel, + travel: dropdown.travel, of: dropdown, in: scrollView ) @@ -107,9 +104,7 @@ extension KoinDropdownHost { } func dismiss(_ dropdown: KoinDropdown) { - guard let presentedDropdown, - presentedDropdown === dropdown, - presentedDropdown.state == .presented else { + guard presentedDropdown === dropdown else { return } From 14fa9b79022be663ecbf083b81118aeab52e576b Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Wed, 26 Aug 2026 00:08:02 +0900 Subject: [PATCH 078/101] =?UTF-8?q?fix:=20=EB=B6=84=EC=8B=A4=EB=AC=BC=20?= =?UTF-8?q?=EC=8A=B5=EB=93=9D/=EB=B6=84=EC=8B=A4=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API에서 요구하는 yyyy-MM-dd 형식이 아닌, UI 포맷인 yyyy년 M월 d일 형태로 API를 호출하며 실패하는 문제 해결 --- .../SubViews/EditLostItemFoundDateView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift index 4940f022..1cde73e7 100644 --- a/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift +++ b/Koin/Presentation/LostItem/EditLostItem/SubViews/EditLostItemFoundDateView.swift @@ -130,14 +130,20 @@ final class EditLostItemFoundDateView: UIView { private func dropdownValueChanged() { // shouldScrollTo(dropdownView) - let formattedDate = { + let displayDate = { let formatter = DateFormatter() formatter.dateFormat = "yyyy년 M월 d일" return formatter.string(from: dropdownView.dateValue) }() - dateButton.setTitle(formattedDate, for: .normal) + dateButton.setTitle(displayDate, for: .normal) dateButton.setTitleColor(UIColor.appColor(.neutral800), for: .normal) dateWarningLabel.isHidden = true + + let formattedDate = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: dropdownView.dateValue) + }() self.foundDate = formattedDate } } From ec7571f9aaf93bcd2272e3bc02afd044dea4c4d4 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 28 Aug 2026 17:13:12 +0900 Subject: [PATCH 079/101] =?UTF-8?q?feat:=20Shared=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20FilterBottomSheetView=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterBottomSheetView.swift | 243 ++++++++++++++++++ .../Model/FilterGroupModel.swift | 112 ++++++++ .../Model/FilterItemModel.swift | 22 ++ .../FilterGroupCollectionView.swift | 122 +++++++++ .../FilterGroupCollectionViewCell.swift | 65 +++++ .../Subviews/FilterGroupView.swift | 97 +++++++ koin.xcodeproj/project.pbxproj | 65 ++++- 7 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/Model/FilterItemModel.swift create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionView.swift create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionViewCell.swift create mode 100644 Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupView.swift diff --git a/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift b/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift new file mode 100644 index 00000000..e21f6a8b --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift @@ -0,0 +1,243 @@ +// +// FilterBottomSheetView.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +class FilterBottomSheetView: UIView { + + // MARK: - Properties + weak var delegate: BottomSheetViewControllerBDelegate? + private var groupModels: [FilterGroupModel] + private let onApplyTapped: ([FilterGroupModel])->Void + private var subscriptions: Set = [] + + // MARK: - UI Components + private let titleLabel = UILabel() + private let closeButton = UIButton() + private let topSeparatorView = UIView() + + private let filterGroupScrollView = UIScrollView() + private let filterGroupStackView = UIStackView() + private let filterGroupViews: [FilterGroupView] + + private let resetButton = UIButton() + private let applyButton = UIButton() + private let bottomSeparatorView = UIView() + + + // MARK: - Initializer + init( + groupModels: [FilterGroupModel], + onApplyTapped: @escaping ([FilterGroupModel])->Void + ) { + self.groupModels = groupModels + self.onApplyTapped = onApplyTapped + self.filterGroupViews = groupModels.map { group in + FilterGroupView(filterGroup: group) + } + + super.init(frame: .zero) + + configureView() + setAddTargets() + bind() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Bind + private func bind() { + for groupIndex in filterGroupViews.indices { + filterGroupViews[groupIndex].itemTappedPublisher + .sink { [weak self] itemIndex in + self?.didTapItem(groupIndex: groupIndex, itemIndex: itemIndex) + } + .store(in: &subscriptions) + } + } + + private func didTapItem(groupIndex: Int, itemIndex: Int) { + let before = groupModels[groupIndex].items.map(\.isSelected) + groupModels[groupIndex].didTap(itemAt: itemIndex) + let after = groupModels[groupIndex].items.map(\.isSelected) + + filterGroupViews[groupIndex].update( + filterGroup: groupModels[groupIndex], + changed: Self.changedIndexPaths(before: before, after: after) + ) + } + + private static func changedIndexPaths(before: [Bool], after: [Bool]) -> [IndexPath] { + zip(before, after).enumerated().compactMap { index, pair in + pair.0 != pair.1 ? IndexPath(row: index, section: 0) : nil + } + } +} + +extension FilterBottomSheetView { + private func setAddTargets() { + closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) + applyButton.addTarget(self, action: #selector(applyButtonTapped), for: .touchUpInside) + } + + @objc private func closeButtonTapped() { + delegate?.dismiss() + } + + @objc private func resetButtonTapped() { + for groupIndex in groupModels.indices { + let before = groupModels[groupIndex].items.map(\.isSelected) + groupModels[groupIndex].reset() + let after = groupModels[groupIndex].items.map(\.isSelected) + + filterGroupViews[groupIndex].update( + filterGroup: groupModels[groupIndex], + changed: Self.changedIndexPaths(before: before, after: after) + ) + } + } + + @objc private func applyButtonTapped() { + onApplyTapped(groupModels) + delegate?.dismiss() + } +} + +extension FilterBottomSheetView { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + self.do { + $0.backgroundColor = .appColor(.neutral0) + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.layer.cornerRadius = 32 + } + + titleLabel.do { + $0.text = "필터" + $0.font = UIFont.appFont(.pretendardSemiBold, size: 18) + $0.textColor = UIColor.appColor(.new500) + } + + closeButton.do { + $0.setImage(.appImage(asset: .newCancel), for: .normal) + $0.tintColor = UIColor.appColor(.neutral800) + } + + [topSeparatorView, bottomSeparatorView].forEach { + $0.do { + $0.backgroundColor = UIColor.appColor(.neutral200) + } + } + + filterGroupScrollView.do { +// $0.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) + $0.showsVerticalScrollIndicator = false + } + + filterGroupStackView.do { + $0.axis = .vertical + $0.alignment = .fill + $0.distribution = .fill + $0.spacing = 12 + } + + resetButton.do { + var configuration = UIButton.Configuration.plain() + configuration.attributedTitle = AttributedString("초기화", attributes: AttributeContainer([ + .font : UIFont.appFont(.pretendardSemiBold, size: 16), + .foregroundColor : UIColor.appColor(.neutral600) + ])) + configuration.image = UIImage.appImage(asset: .refresh) + configuration.imagePadding = 8 + configuration.imagePlacement = .trailing + configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) + $0.configuration = configuration + $0.layer.borderColor = UIColor.appColor(.neutral400).cgColor + $0.layer.borderWidth = 1 + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + } + resetButton.setContentHuggingPriority(.required, for: .horizontal) + resetButton.setContentCompressionResistancePriority(.required, for: .horizontal) + + applyButton.do { + $0.setAttributedTitle(NSAttributedString( + string: "적용하기", + attributes: [ + .font : UIFont.appFont(.pretendardSemiBold, size: 16), + .foregroundColor : UIColor.appColor(.neutral0) + ]), for: .normal) + $0.backgroundColor = UIColor.appColor(.new500) + $0.layer.cornerRadius = 12 + } + } + + private func setUpLayouts() { + filterGroupViews.forEach { + filterGroupStackView.addArrangedSubview($0) + } + [filterGroupStackView].forEach { + filterGroupScrollView.addSubview($0) + } + [titleLabel, closeButton, topSeparatorView, filterGroupScrollView, resetButton, applyButton, bottomSeparatorView].forEach { + addSubview($0) + } + } + + private func setUpConstraints() { + titleLabel.snp.makeConstraints { + $0.height.equalTo(29) + $0.top.equalToSuperview().offset(12) + $0.leading.equalToSuperview().offset(32) + } + closeButton.snp.makeConstraints { + $0.centerY.equalTo(titleLabel) + $0.trailing.equalToSuperview().offset(-24) + } + topSeparatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview() + } + filterGroupScrollView.snp.makeConstraints { + $0.top.equalTo(topSeparatorView.snp.bottom) + $0.leading.trailing.equalToSuperview().inset(32) + $0.height.equalTo(filterGroupScrollView.contentLayoutGuide.snp.height).priority(.medium) + } + filterGroupStackView.snp.makeConstraints { +// $0.edges.equalTo(filterGroupScrollView.contentLayoutGuide) + $0.leading.trailing.equalTo(filterGroupScrollView.contentLayoutGuide) + $0.top.bottom.equalTo(filterGroupScrollView.contentLayoutGuide).inset(12) + $0.width.equalTo(filterGroupScrollView) + } + resetButton.snp.makeConstraints { + $0.height.equalTo(48) + $0.top.equalTo(filterGroupScrollView.snp.bottom).offset(12) + $0.leading.equalToSuperview().offset(32) + } + applyButton.snp.makeConstraints { + $0.top.bottom.equalTo(resetButton) + $0.leading.equalTo(resetButton.snp.trailing).offset(12) + $0.trailing.equalToSuperview().offset(-32) + } + bottomSeparatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.equalTo(resetButton.snp.bottom).offset(12) + $0.leading.trailing.bottom.equalToSuperview() + } + } +} diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift new file mode 100644 index 00000000..5d015f9c --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift @@ -0,0 +1,112 @@ +// +// FilterItemGroup.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import Foundation + +struct FilterGroupModel { + + enum Behavior { + case single + case multiple + } + + let title: String + let description: String? + let hasAllButton: Bool + let behavior: Behavior + private(set) var items: [FilterItemModel] = [] + + init( + title: String, + description: String? = nil, + hasAllButton: Bool, + items: [String], + behavior: Behavior, + ) { + self.title = title + self.description = description + self.hasAllButton = hasAllButton + self.behavior = behavior + + if hasAllButton { + self.items.append(FilterItemModel(title: "전체")) + } + for item in items { + self.items.append(FilterItemModel(title: item)) + } + + if !self.items.contains(where: { $0.isSelected }) { + reset() + } + } +} + +extension FilterGroupModel { + var selectedItems: [FilterItemModel] { + items.filter { $0.isSelected } + } +} + +extension FilterGroupModel { + mutating func reset() { + deselectAll(except: 0) + } + + mutating func didTap(itemAt index: Int) { + switch items[index].isSelected { + case true: + deselect(itemAt: index) + case false: + select(itemAt: index) + } + } +} + +extension FilterGroupModel { + mutating private func deselectAll(except selectedIndex: Int) { + for index in items.indices { + items[index].isSelected = index == selectedIndex + } + } + + mutating private func select(itemAt selectedIndex: Int) { + let selectedItem = items[selectedIndex].title + let didTapAll = selectedItem == "전체" + + if didTapAll { + deselectAll(except: 0) + return + } + + switch behavior { + case .single: + deselectAll(except: selectedIndex) + case .multiple: + if items[0].title == "전체" { + items[0].isSelected = false + } + items[selectedIndex].isSelected = true + } + } + + mutating private func deselect(itemAt deselectedIndex: Int) { + let deselectedItem = items[deselectedIndex].title + let didTapAll = deselectedItem == "전체" + + if didTapAll { + return + } + switch behavior { + case .single: + return + case .multiple: + if 1 < selectedItems.count { + items[deselectedIndex].isSelected = false + } + } + } +} diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterItemModel.swift b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterItemModel.swift new file mode 100644 index 00000000..76af6265 --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterItemModel.swift @@ -0,0 +1,22 @@ +// +// FilterItemModel.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import Foundation + +struct FilterItemModel { + + let title: String + var isSelected: Bool + + init( + title: String, + isSelected: Bool = false + ) { + self.title = title + self.isSelected = isSelected + } +} diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionView.swift b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionView.swift new file mode 100644 index 00000000..a3ce07f6 --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionView.swift @@ -0,0 +1,122 @@ +// +// FilterGroupCollectionView.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import UIKit +import Combine + +final class FilterGroupCollectionView: UICollectionView { + + // MARK: - Publisher + let itemTappedPublisher = PassthroughSubject() + + // MARK: - Properties + private(set) var filterGroup: FilterGroupModel + + // MARK: - Initializer + init(filterGroup: FilterGroupModel) { + self.filterGroup = filterGroup + super.init( + frame: .zero, + collectionViewLayout: LeftAlignedFlowLayout() + ) + commonInit() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + func update(filterGroup: FilterGroupModel, changed indexPaths: [IndexPath]) { + self.filterGroup = filterGroup + reconfigureItems(at: indexPaths) + } + + // MARK: - Override + override var intrinsicContentSize: CGSize { + CGSize( + width: UIView.noIntrinsicMetric, + height: collectionViewLayout.collectionViewContentSize.height + ) + } + override func layoutSubviews() { + super.layoutSubviews() + if bounds.height != collectionViewLayout.collectionViewContentSize.height { + invalidateIntrinsicContentSize() + } + } +} + +extension FilterGroupCollectionView { + private func commonInit() { + isScrollEnabled = false + delegate = self + dataSource = self + register( + FilterGroupCollectionViewCell.self, + forCellWithReuseIdentifier: FilterGroupCollectionViewCell.identifier + ) + } +} + +extension FilterGroupCollectionView: UICollectionViewDelegateFlowLayout { + func collectionView( + _ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath + ) { + collectionView.deselectItem(at: indexPath, animated: false) + itemTappedPublisher.send(indexPath.row) + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { + let title = filterGroup.items[indexPath.row].title + let titleWidth = (title as NSString).size( + withAttributes: [.font: UIFont.appFont(.pretendardSemiBold, size: 14)] + ).width + return CGSize( + width: ceil(titleWidth) + 12 * 2, + height: 34 + ) + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + minimumLineSpacingForSectionAt section: Int + ) -> CGFloat { + 8 + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + minimumInteritemSpacingForSectionAt section: Int + ) -> CGFloat { + 12 + } +} + +extension FilterGroupCollectionView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return filterGroup.items.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: FilterGroupCollectionViewCell.identifier, + for: indexPath + ) as? FilterGroupCollectionViewCell else { + return UICollectionViewCell() + } + let index = indexPath.row + cell.configure(item: filterGroup.items[index]) + return cell + } +} diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionViewCell.swift b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionViewCell.swift new file mode 100644 index 00000000..40a567a6 --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupCollectionView/FilterGroupCollectionViewCell.swift @@ -0,0 +1,65 @@ +// +// FilterGroupCollectionViewCell.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import UIKit +import SnapKit +import Then + +final class FilterGroupCollectionViewCell: UICollectionViewCell { + + // MARK: - UI Components + private let titleLabel = UILabel() + + // MARK: - Initializer + override init(frame: CGRect) { + super.init(frame: frame) + configureView() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Configure + func configure(item model: FilterItemModel) { + titleLabel.text = model.title + titleLabel.textColor = model.isSelected ? .appColor(.new500) : .appColor(.neutral500) + contentView.layer.borderColor = model.isSelected + ? UIColor.appColor(.new500).cgColor + : UIColor.appColor(.neutral300).cgColor + } +} + +extension FilterGroupCollectionViewCell { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + titleLabel.do { + $0.font = .appFont(.pretendardSemiBold, size: 14) + $0.textAlignment = .center + } + contentView.do { + $0.layer.borderWidth = 1 + $0.layer.cornerRadius = 17 + $0.clipsToBounds = true + } + } + + private func setUpLayouts() { + contentView.addSubview(titleLabel) + } + + private func setUpConstraints() { + titleLabel.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(12) + } + } +} diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupView.swift b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupView.swift new file mode 100644 index 00000000..2db944cb --- /dev/null +++ b/Koin/Presentation/Shared/FilterBottomSheet/Subviews/FilterGroupView.swift @@ -0,0 +1,97 @@ +// +// FilterGroupView.swift +// koin +// +// Created by 홍기정 on 8/27/26. +// + +import UIKit +import Combine +import SnapKit +import Then + +final class FilterGroupView: UIView { + + // MARK: - Publisher + var itemTappedPublisher: AnyPublisher { + filterGroupCollectionView.itemTappedPublisher.eraseToAnyPublisher() + } + + // MARK: - UI Components + private let titleLabel = UILabel() + private let descriptionLabel = UILabel() + private let filterGroupCollectionView: FilterGroupCollectionView + private let separatorView = UIView() + + // MARK: - Initializer + init(filterGroup: FilterGroupModel) { + self.filterGroupCollectionView = FilterGroupCollectionView(filterGroup: filterGroup) + super.init(frame: .zero) + + titleLabel.text = filterGroup.title + descriptionLabel.text = filterGroup.description + configureView() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + func update(filterGroup: FilterGroupModel, changed: [IndexPath]) { + filterGroupCollectionView.update(filterGroup: filterGroup, changed: changed) + } +} + +extension FilterGroupView { + private func configureView() { + setUpStyles() + setUpLayouts() + setUpConstraints() + } + + private func setUpStyles() { + titleLabel.do { + $0.font = UIFont.appFont(.pretendardSemiBold, size: 16) + $0.textColor = UIColor.appColor(.neutral800) + } + descriptionLabel.do { + $0.font = UIFont.appFont(.pretendardRegular, size: 12) + $0.textColor = UIColor.appColor(.neutral500) + } + separatorView.do { + $0.backgroundColor = UIColor.appColor(.neutral200) + } + } + + private func setUpLayouts() { + [titleLabel, filterGroupCollectionView, separatorView].forEach { + addSubview($0) + } + if descriptionLabel.text != nil { + addSubview(descriptionLabel) + } + } + + private func setUpConstraints() { + titleLabel.snp.makeConstraints { + $0.height.equalTo(26) + $0.leading.top.equalToSuperview() + } + if descriptionLabel.text != nil { + descriptionLabel.snp.makeConstraints { + $0.height.equalTo(19) + $0.centerY.equalTo(titleLabel) + $0.leading.equalTo(titleLabel.snp.trailing).offset(8) + } + } + filterGroupCollectionView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview() + } + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.equalTo(filterGroupCollectionView.snp.bottom).offset(12) + $0.leading.trailing.bottom.equalToSuperview() + } + } +} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 84fdbd5d..2161bca7 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -242,6 +242,9 @@ 7C7F56B92FFBA0A700847151 /* AppPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7CCE6F2F866E9400E3A54B /* AppPath.swift */; }; 7C7F56BB2FFBA18100847151 /* NotificationHistoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */; }; 7C7F56BC2FFBA18100847151 /* NotificationHistoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */; }; + 7C8261973040344700C20F64 /* FilterGroupCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8261963040344700C20F64 /* FilterGroupCollectionView.swift */; }; + 7C82619930403C5E00C20F64 /* FilterGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82619830403C5E00C20F64 /* FilterGroupCollectionViewCell.swift */; }; + 7C82619C30407FF300C20F64 /* FilterGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82619B30407FF300C20F64 /* FilterGroupView.swift */; }; 7C82FF782E9D18C7006335A7 /* OrderShopDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82FF772E9D18C7006335A7 /* OrderShopDetail.swift */; }; 7C85D2752EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C85D2742EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift */; }; 7C86750E30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C86750D30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift */; }; @@ -277,6 +280,9 @@ 7C8ADD3E2F20C2C400F85BDE /* LostItemRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8ADD3D2F20C2C400F85BDE /* LostItemRepository.swift */; }; 7C8ADD402F20C30700F85BDE /* DefaultLostItemRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8ADD3F2F20C30700F85BDE /* DefaultLostItemRepository.swift */; }; 7C8ADD452F20C3FB00F85BDE /* FetchLostItemListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8ADD442F20C3FB00F85BDE /* FetchLostItemListUseCase.swift */; }; + 7C8B37A93040287D00A7EB5A /* FilterBottomSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8B37A83040287D00A7EB5A /* FilterBottomSheetView.swift */; }; + 7C8B37AD3040289500A7EB5A /* FilterItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8B37AC3040289500A7EB5A /* FilterItemModel.swift */; }; + 7C8B37B0304028B400A7EB5A /* FilterGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8B37AF304028B400A7EB5A /* FilterGroupModel.swift */; }; 7C8BFCC32FCFF47600963679 /* ActionBindableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCC22FCFF47600963679 /* ActionBindableView.swift */; }; 7C8BFCD32FD0272900963679 /* View+border.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCD22FD0272900963679 /* View+border.swift */; }; 7C8BFCD62FD032A000963679 /* View+linespacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8BFCD52FD032A000963679 /* View+linespacing.swift */; }; @@ -1275,6 +1281,9 @@ 7C7F56B32FFB6A6400847151 /* NotificationPopUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPopUpViewController.swift; sourceTree = ""; }; 7C7F56B62FFB9FE000847151 /* NotificationHistoryRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHistoryRecord.swift; sourceTree = ""; }; 7C7F56BA2FFBA18100847151 /* NotificationHistoryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHistoryService.swift; sourceTree = ""; }; + 7C8261963040344700C20F64 /* FilterGroupCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupCollectionView.swift; sourceTree = ""; }; + 7C82619830403C5E00C20F64 /* FilterGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupCollectionViewCell.swift; sourceTree = ""; }; + 7C82619B30407FF300C20F64 /* FilterGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupView.swift; sourceTree = ""; }; 7C82FF772E9D18C7006335A7 /* OrderShopDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopDetail.swift; sourceTree = ""; }; 7C85D2742EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopMenusAndGroupsFromShopUseCase.swift; sourceTree = ""; }; 7C86750D30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckHasUnreadNotificationHistoryUseCase.swift; sourceTree = ""; }; @@ -1310,6 +1319,9 @@ 7C8ADD3D2F20C2C400F85BDE /* LostItemRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LostItemRepository.swift; sourceTree = ""; }; 7C8ADD3F2F20C30700F85BDE /* DefaultLostItemRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLostItemRepository.swift; sourceTree = ""; }; 7C8ADD442F20C3FB00F85BDE /* FetchLostItemListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchLostItemListUseCase.swift; sourceTree = ""; }; + 7C8B37A83040287D00A7EB5A /* FilterBottomSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterBottomSheetView.swift; sourceTree = ""; }; + 7C8B37AC3040289500A7EB5A /* FilterItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterItemModel.swift; sourceTree = ""; }; + 7C8B37AF304028B400A7EB5A /* FilterGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupModel.swift; sourceTree = ""; }; 7C8BFCC22FCFF47600963679 /* ActionBindableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBindableView.swift; sourceTree = ""; }; 7C8BFCD22FD0272900963679 /* View+border.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+border.swift"; sourceTree = ""; }; 7C8BFCD52FD032A000963679 /* View+linespacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+linespacing.swift"; sourceTree = ""; }; @@ -2786,6 +2798,23 @@ path = Common; sourceTree = ""; }; + 7C82619A30407FD800C20F64 /* FilterGroupCollectionView */ = { + isa = PBXGroup; + children = ( + 7C8261963040344700C20F64 /* FilterGroupCollectionView.swift */, + 7C82619830403C5E00C20F64 /* FilterGroupCollectionViewCell.swift */, + ); + path = FilterGroupCollectionView; + sourceTree = ""; + }; + 7C82619E3040C7C100C20F64 /* Mapper */ = { + isa = PBXGroup; + children = ( + 7C82619F3040C7E700C20F64 /* CallVanListRequest+.swift */, + ); + path = Mapper; + sourceTree = ""; + }; 7C86751030062FF3003CB942 /* Core */ = { isa = PBXGroup; children = ( @@ -2896,6 +2925,34 @@ path = Helper; sourceTree = ""; }; + 7C8B37A73040286C00A7EB5A /* FilterBottomSheet */ = { + isa = PBXGroup; + children = ( + 7C8B37AB3040288C00A7EB5A /* Model */, + 7C8B37AA3040288200A7EB5A /* Subviews */, + 7C8B37A83040287D00A7EB5A /* FilterBottomSheetView.swift */, + ); + path = FilterBottomSheet; + sourceTree = ""; + }; + 7C8B37AA3040288200A7EB5A /* Subviews */ = { + isa = PBXGroup; + children = ( + 7C82619B30407FF300C20F64 /* FilterGroupView.swift */, + 7C82619A30407FD800C20F64 /* FilterGroupCollectionView */, + ); + path = Subviews; + sourceTree = ""; + }; + 7C8B37AB3040288C00A7EB5A /* Model */ = { + isa = PBXGroup; + children = ( + 7C8B37AF304028B400A7EB5A /* FilterGroupModel.swift */, + 7C8B37AC3040289500A7EB5A /* FilterItemModel.swift */, + ); + path = Model; + sourceTree = ""; + }; 7CA0D6172EC744E700E9A282 /* ZoomedImageViewControllerB */ = { isa = PBXGroup; children = ( @@ -4700,6 +4757,7 @@ D19A00013000000000000007 /* Shared */ = { isa = PBXGroup; children = ( + 7C8B37A73040286C00A7EB5A /* FilterBottomSheet */, D19A1001300000000000000C /* Chat */, D19A00013000000000000008 /* Notification */, ); @@ -5942,6 +6000,7 @@ 835DC1F92C849ECD00488506 /* FetchRecommendedKeywordUseCase.swift in Sources */, 7CF77B4930027539003642C1 /* TimeTableHourView.swift in Sources */, D23944822C3C24A200048F45 /* ReviewsDto.swift in Sources */, + 7C8261973040344700C20F64 /* FilterGroupCollectionView.swift in Sources */, D20AD6FD2E02A49A0067760A /* FindIdSmsUseCase.swift in Sources */, B60D25462ED72017000E57B7 /* RadioButtonState.swift in Sources */, 7C4D5A5D2F0163BA00B40128 /* DIContainer+Bus.swift in Sources */, @@ -6060,7 +6119,7 @@ D21572602CEDD2C60061E725 /* ModifyLectureUseCase.swift in Sources */, 7CC42B532F59C5FA00940CE1 /* CallVanPostViewModel.swift in Sources */, D20AB9522C5534F7006BC684 /* FetchNotiListUseCase.swift in Sources */, - 7CC42B282F575E8100940CE1 /* CallVanListFilterViewController.swift in Sources */, + 7C8B37B0304028B400A7EB5A /* FilterGroupModel.swift in Sources */, D29F21CA2C4E151000994554 /* TimetableRepository.swift in Sources */, 7CED90BC2EF2D1F900457128 /* ShopSummaryTableViewHeaderView.swift in Sources */, 7CED90BD2EF2D1F900457128 /* ManageNoticeKeywordViewController.swift in Sources */, @@ -6363,6 +6422,7 @@ 7C4D5A5A2F01638800B40128 /* BusAreaSelectdViewController.swift in Sources */, 7C4D5A5B2F01638800B40128 /* ManyBusTimetableCollectionViewCell.swift in Sources */, 7CED91AA2EF2D1F900457128 /* DeleteLectureView.swift in Sources */, + 7C82619C30407FF300C20F64 /* FilterGroupView.swift in Sources */, 7CED91AB2EF2D1F900457128 /* NoticeListViewModel.swift in Sources */, 7CB11C152FB1BDA6004E0C80 /* FetchLostItemKeywordSuggestionUseCase.swift in Sources */, 7C7F43282F65F31500CC5860 /* CallVanReportRequest.swift in Sources */, @@ -6463,6 +6523,7 @@ 7CED92002EF2D1F900457128 /* MyKeywordCollectionView.swift in Sources */, 7CED92022EF2D1F900457128 /* ShopSummaryMenuGroupCollectionViewCell.swift in Sources */, 7CED92042EF2D1F900457128 /* DeleteSemesterModalViewController.swift in Sources */, + 7C8B37AD3040289500A7EB5A /* FilterItemModel.swift in Sources */, 7C04A47F2FB3380700214992 /* NotiSubscribeDetailRequest.swift in Sources */, 7C04A4802FB3380700214992 /* NotiSubscribeRequest.swift in Sources */, 7C04A4812FB3380700214992 /* SendDeviceTokenRequest.swift in Sources */, @@ -6489,6 +6550,7 @@ 7C01656D300B49F80013DD7B /* DepartmentHostingController.swift in Sources */, B47839072E70FF2B00D002E3 /* OrderShopMenusGroups.swift in Sources */, 833D9C342C70418100982145 /* NoticeListPages.swift in Sources */, + 7C82619930403C5E00C20F64 /* FilterGroupCollectionViewCell.swift in Sources */, 7C7F43262F65F2F900CC5860 /* CallVanPostRequest.swift in Sources */, D20AB9582C553621006BC684 /* NotiService.swift in Sources */, 7CF77B6530056EB9003642C1 /* NoticeKeywordSearchButton.swift in Sources */, @@ -6699,6 +6761,7 @@ 7C333E7E302E28AA009D2B89 /* KoinModalPresentationController.swift in Sources */, D874DB4F2DBF1FEE0098EED0 /* SendVerificationCodeRequest.swift in Sources */, 7CA60D772F663F7B007C08C1 /* PostNotificationReadUseCase.swift in Sources */, + 7C8B37A93040287D00A7EB5A /* FilterBottomSheetView.swift in Sources */, 7C8A941E2FD0A94F00DEA6F5 /* NotificationViewModel.swift in Sources */, 7C8A941F2FD0A94F00DEA6F5 /* NotificationViewController.swift in Sources */, 7CC42B402F597B5B00940CE1 /* CallVanNotificationViewModel.swift in Sources */, From 02996e79f0bdb4d45c56e869ada59a500b57f7b5 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 28 Aug 2026 17:16:37 +0900 Subject: [PATCH 080/101] =?UTF-8?q?refactor:=20BottomSheetViewControllerB?= =?UTF-8?q?=EA=B0=80=20=EB=86=92=EC=9D=B4=EB=A5=BC=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=EB=B0=9B=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIScrollView를 포함하며 높이가 스스로 결정되지 않는 ContentView를 주입받을 수 있도록 수정합니다. --- .../View/BottomSheetViewControllerB.swift | 58 ++++++++++++------- .../CallVanPostViewController.swift | 7 +-- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/Koin/Core/View/BottomSheetViewControllerB.swift b/Koin/Core/View/BottomSheetViewControllerB.swift index 524038ef..dee6efb6 100644 --- a/Koin/Core/View/BottomSheetViewControllerB.swift +++ b/Koin/Core/View/BottomSheetViewControllerB.swift @@ -17,40 +17,41 @@ protocol BottomSheetViewControllerBDelegate: AnyObject { final class BottomSheetViewControllerB: UIViewController { // MARK: - Properties - private var contentViewBottomConstraint: Constraint? - private var safeAreaHeightConstraint: Constraint? - private var alpha: CGFloat + private var dismissContentViewConstraint: Constraint? + private var presentContentViewConstraint: Constraint? + private let dimAlpha: CGFloat // MARK: - UI Components private let dimView = UIView().then { $0.alpha = 0 } private let contentView: UIView private let safeAreaView = UIView() + // MARK: - LayoutGuide + private let safeAreaLayoutGuide = UILayoutGuide() + // MARK: - Initializer - init(contentView: UIView, dimColor: UIColor, dimAlpha: CGFloat, backgroundColor: UIColor) { + init( + contentView: UIView, + dimAlpha: CGFloat = 0.7 + ) { self.contentView = contentView - self.alpha = dimAlpha + self.dimAlpha = dimAlpha super.init(nibName: nil, bundle: nil) - modalTransitionStyle = .crossDissolve modalPresentationStyle = .overFullScreen dimView.do { - $0.backgroundColor = dimColor + $0.backgroundColor = .appColor(.neutral800) } safeAreaView.do { - $0.backgroundColor = backgroundColor + $0.backgroundColor = contentView.backgroundColor } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - NotificationCenter.default.removeObserver(self) - } - // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -61,7 +62,6 @@ final class BottomSheetViewControllerB: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - safeAreaHeightConstraint?.update(offset: view.safeAreaInsets.bottom) present() } } @@ -69,17 +69,22 @@ final class BottomSheetViewControllerB: UIViewController { extension BottomSheetViewControllerB: BottomSheetViewControllerBDelegate { func present() { -// view.layoutIfNeeded() - contentViewBottomConstraint?.update(offset: 0) + view.layoutIfNeeded() + + dismissContentViewConstraint?.deactivate() + presentContentViewConstraint?.activate() + UIView.animate(withDuration: 0.25) { [weak self] in guard let self else { return } - dimView.alpha = alpha + dimView.alpha = dimAlpha view.layoutIfNeeded() } } func dismiss() { - contentViewBottomConstraint?.update(offset: contentView.bounds.height) + dismissContentViewConstraint?.activate() + presentContentViewConstraint?.deactivate() + UIView.animate( withDuration: 0.25, animations: { [weak self] in @@ -94,7 +99,6 @@ extension BottomSheetViewControllerB: BottomSheetViewControllerBDelegate { } extension BottomSheetViewControllerB { - private func setGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimViewTapped)) dimView.addGestureRecognizer(tapGesture) @@ -107,7 +111,6 @@ extension BottomSheetViewControllerB { } extension BottomSheetViewControllerB { - private func configureView() { setUpLayouts() setUpConstraints() @@ -117,6 +120,9 @@ extension BottomSheetViewControllerB { [dimView, contentView, safeAreaView].forEach { view.addSubview($0) } + [safeAreaLayoutGuide].forEach { + view.addLayoutGuide($0) + } } private func setUpConstraints() { @@ -124,14 +130,22 @@ extension BottomSheetViewControllerB { $0.edges.equalToSuperview() } contentView.snp.makeConstraints { - contentView.layoutIfNeeded() - contentViewBottomConstraint = $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top).offset(contentView.bounds.height).constraint + $0.height.lessThanOrEqualTo(view.safeAreaLayoutGuide.snp.height) + $0.leading.trailing.equalToSuperview() + dismissContentViewConstraint = $0.top.equalTo(view.snp.bottom).constraint + presentContentViewConstraint = $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top).constraint + } + presentContentViewConstraint?.deactivate() + + safeAreaLayoutGuide.snp.makeConstraints { $0.leading.trailing.equalToSuperview() + $0.top.equalTo(view.keyboardLayoutGuide.snp.top) + $0.bottom.equalToSuperview() } safeAreaView.snp.makeConstraints { $0.leading.trailing.equalToSuperview() $0.top.equalTo(contentView.snp.bottom) - safeAreaHeightConstraint = $0.height.equalTo(0).constraint + $0.height.equalTo(safeAreaLayoutGuide.snp.height) } } } diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index 5604e687..ac648c14 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -227,12 +227,7 @@ extension CallVanPostViewController { } private func presentPlaceBottomSheet() { - let bottomSheetViewController = BottomSheetViewControllerB( - contentView: bottomSheetContentView, - dimColor: .black, - dimAlpha: 0.7, - backgroundColor: UIColor.appColor(.neutral0) - ) + let bottomSheetViewController = BottomSheetViewControllerB(contentView: bottomSheetContentView) bottomSheetContentView.delegate = bottomSheetViewController present(bottomSheetViewController, animated: false) } From 731c2c85508192c46ceee6bfe5f01e21ea2ba46d Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 28 Aug 2026 17:53:11 +0900 Subject: [PATCH 081/101] =?UTF-8?q?refactor:=20CallVanListFilterViewContro?= =?UTF-8?q?ller=20=EB=A5=BC=20FilterBottomSheetView=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decodable/CallVan/CallVanPlaceDto.swift | 4 - .../Model/CallVan/CallVanListRequest.swift | 16 + Koin/Domain/Model/CallVan/CallVanPlace.swift | 26 +- .../CallVan/CallVanRecruitmentState.swift | 8 + .../CallVanListViewController.swift | 43 +- .../Mapper/CallVanListRequest+.swift | 156 ++++++ .../CallVanListFilterViewController.swift | 523 ------------------ .../Subviews/CallVanFilterButton.swift | 0 .../FilterBottomSheetView.swift | 8 + koin.xcodeproj/project.pbxproj | 9 +- 10 files changed, 245 insertions(+), 548 deletions(-) create mode 100644 Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift delete mode 100644 Koin/Presentation/CallVan/CallVanList/Subviews/CallVanListFilterViewController.swift rename Koin/Presentation/CallVan/{CallVanList => CallVanPost}/Subviews/CallVanFilterButton.swift (100%) diff --git a/Koin/Data/DTOs/Decodable/CallVan/CallVanPlaceDto.swift b/Koin/Data/DTOs/Decodable/CallVan/CallVanPlaceDto.swift index b26ee455..f0807da3 100644 --- a/Koin/Data/DTOs/Decodable/CallVan/CallVanPlaceDto.swift +++ b/Koin/Data/DTOs/Decodable/CallVan/CallVanPlaceDto.swift @@ -10,7 +10,6 @@ import Foundation enum CallVanPlaceDto: String, Codable { case frontGate = "FRONT_GATE" case backGate = "BACK_GATE" - case tennisCourt = "TENNIS_COURT" case dormitoryMain = "DORMITORY_MAIN" case dormitorySub = "DORMITORY_SUB" case terminal = "TERMINAL" @@ -29,7 +28,6 @@ extension CallVanPlaceDto { case .dormitorySub: self = .dormitorySub case .frontGate: self = .frontGate case .station: self = .station - case .tennisCourt: self = .tennisCourt case .terminal: self = .terminal case .all: return nil } @@ -51,8 +49,6 @@ extension CallVanPlaceDto { return .frontGate case .station: return .station - case .tennisCourt: - return .tennisCourt case .terminal: return .terminal } diff --git a/Koin/Domain/Model/CallVan/CallVanListRequest.swift b/Koin/Domain/Model/CallVan/CallVanListRequest.swift index c1571438..e865583d 100644 --- a/Koin/Domain/Model/CallVan/CallVanListRequest.swift +++ b/Koin/Domain/Model/CallVan/CallVanListRequest.swift @@ -26,10 +26,26 @@ enum CallVanListSort: String, CallVanFilterState { case departureDesc = "출발시각순" case latestAsc = "과거순" case latestDesc = "최신순" + + var index: Int? { + switch self { + case .latestDesc: 0 + case .departureDesc: 1 + default: nil + } + } } enum CallVanMineOrJoined: String, CallVanFilterState { case all = "전체" case mine = "내 게시물" case joined = "참여중인 게시물" + + var index: Int { + switch self { + case .all: 0 + case .mine: 1 + case .joined: 2 + } + } } diff --git a/Koin/Domain/Model/CallVan/CallVanPlace.swift b/Koin/Domain/Model/CallVan/CallVanPlace.swift index 2a4d7f77..c6c100d7 100644 --- a/Koin/Domain/Model/CallVan/CallVanPlace.swift +++ b/Koin/Domain/Model/CallVan/CallVanPlace.swift @@ -7,16 +7,36 @@ import Foundation -enum CallVanPlace: String, CallVanFilterState { +enum CallVanPlace: String, CallVanFilterState, CaseIterable { + case all = "전체" + case frontGate = "정문" case backGate = "후문" - case tennisCourt = "테니스장" + case terminal = "천안터미널" + case dormitoryMain = "본관동" case dormitorySub = "별관동" - case terminal = "천안터미널" case station = "천안역" case asanStation = "천안아산역" + case custom = "기타" + + var index: Int { + switch self { + case .all: 0 + + case .frontGate: 1 + case .backGate: 2 + case .terminal: 3 + + case .dormitoryMain: 4 + case .dormitorySub: 5 + case .station: 6 + case .asanStation: 7 + + case .custom: 8 + } + } } diff --git a/Koin/Domain/Model/CallVan/CallVanRecruitmentState.swift b/Koin/Domain/Model/CallVan/CallVanRecruitmentState.swift index 049bd701..c8e1f7f3 100644 --- a/Koin/Domain/Model/CallVan/CallVanRecruitmentState.swift +++ b/Koin/Domain/Model/CallVan/CallVanRecruitmentState.swift @@ -11,4 +11,12 @@ enum CallVanRecruitmentState: String, CallVanFilterState { case all = "전체" case recruiting = "모집중" case closed = "모집마감" + + var index: Int { + switch self { + case .all: 0 + case .recruiting: 1 + case .closed: 2 + } + } } diff --git a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift index 8ec4d0cb..bf8d9fc4 100644 --- a/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift +++ b/Koin/Presentation/CallVan/CallVanList/CallVanListViewController.swift @@ -196,21 +196,36 @@ extension CallVanListViewController { } @objc private func filterButtonTapped() { - let height = min(view.frame.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom, 707) - let contentViewController = CallVanListFilterViewController( - filter: viewModel.filterState, - onApplyButtonTapped: { [weak self] filterState in - guard let self else { return } - inputSubject.send(.updateFilterState(filterState)) - inputSubject.send(.logEvent(label: EventParameter.EventLabel.Campus.callvanFilterApply, category: .click, value: "")) - }, - isLoggedIn: viewModel.isLoggedIn, - height: height + let onFilterItemTapped: (FilterItemModel)->Bool = { [weak self] filterItem in + guard let self else { return false } + if filterItem.title == CallVanMineOrJoined.mine.rawValue + || filterItem.title == CallVanMineOrJoined.joined.rawValue { + if viewModel.isLoggedIn { + return true + } else { + showToastMessage(message: "로그인이 필요한 기능입니다.") + return false + } + } + return true + } + let onApplyTapped: ([FilterGroupModel])->Void = { [weak self] groupModels in + guard let request = CallVanListRequest(from: groupModels) else { + return + } + self?.inputSubject.send(.updateFilterState(request)) + self?.inputSubject.send(.logEvent(label: EventParameter.EventLabel.Campus.callvanFilterApply, category: .click, value: "")) + } + let contentView = FilterBottomSheetView( + groupModels: viewModel.filterState.toFilterGroupModels(), + onFilterItemTapped: onFilterItemTapped, + onApplyTapped: onApplyTapped ) - let bottomSheetViewController = BottomSheetViewController(contentViewController: contentViewController, defaultHeight: height + view.safeAreaInsets.bottom) - bottomSheetViewController.modalTransitionStyle = .crossDissolve - bottomSheetViewController.modalPresentationStyle = .overFullScreen - present(bottomSheetViewController, animated: false) + let bottomSheetVC = BottomSheetViewControllerB(contentView: contentView) + contentView.delegate = bottomSheetVC + + present(bottomSheetVC, animated: false) + inputSubject.send(.logEvent(label: EventParameter.EventLabel.Campus.callvanFilter, category: .click, value: "")) } diff --git a/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift b/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift new file mode 100644 index 00000000..b3e0723c --- /dev/null +++ b/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift @@ -0,0 +1,156 @@ +// +// CallVanListRequest+.swift +// koin +// +// Created by 홍기정 on 8/28/26. +// + +import Foundation + +extension CallVanListRequest { + func toFilterGroupModels() -> [FilterGroupModel] { + var filterGroupModels = [ + FilterGroupModel( + title: "목록", + hasAllButton: true, + items: [ + CallVanMineOrJoined.mine.rawValue, + CallVanMineOrJoined.joined.rawValue + ], + behavior: .single + ), + FilterGroupModel( + title: "정렬", + hasAllButton: false, + items: [ + CallVanListSort.latestDesc.rawValue, + CallVanListSort.departureDesc.rawValue + ], + behavior: .single + ), + FilterGroupModel( + title: "모집 상태", + hasAllButton: true, + items: [ + CallVanRecruitmentState.recruiting.rawValue, + CallVanRecruitmentState.closed.rawValue + ], + behavior: .single + ), + FilterGroupModel( + title: "출발지", + description: "기타 장소는 검색창을 이용해주세요.", + hasAllButton: true, + items: [ + CallVanPlace.frontGate.rawValue, + CallVanPlace.backGate.rawValue, + CallVanPlace.terminal.rawValue, + CallVanPlace.dormitoryMain.rawValue, + CallVanPlace.dormitorySub.rawValue, + CallVanPlace.station.rawValue, + CallVanPlace.asanStation.rawValue + ], + behavior: .multiple + ), + FilterGroupModel( + title: "도착지", + description: "기타 장소는 검색창을 이용해주세요.", + hasAllButton: true, + items: [ + CallVanPlace.frontGate.rawValue, + CallVanPlace.backGate.rawValue, + CallVanPlace.terminal.rawValue, + CallVanPlace.dormitoryMain.rawValue, + CallVanPlace.dormitorySub.rawValue, + CallVanPlace.station.rawValue, + CallVanPlace.asanStation.rawValue + ], + behavior: .multiple + ) + ] + + filterGroupModels[0].didTap(itemAt: mineOrJoined.index) + + if let index = sort.index { + filterGroupModels[1].didTap(itemAt: index) + } + + filterGroupModels[2].didTap(itemAt: state.index) + + departure.forEach { place in + filterGroupModels[3].didTap(itemAt: place.index) + } + + arrival.forEach { place in + filterGroupModels[3].didTap(itemAt: place.index) + } + + return filterGroupModels + } +} + +extension CallVanListRequest { + init?(from filterGroupModels: [FilterGroupModel]) { + guard let mineOrJoined = filterGroupModels[0].selectedItems.first?.title, + let sort = filterGroupModels[1].selectedItems.first?.title, + let state = filterGroupModels[2].selectedItems.first?.title + else { + return nil + } + + switch mineOrJoined { + case CallVanMineOrJoined.mine.rawValue: + self.mineOrJoined = .mine + case CallVanMineOrJoined.joined.rawValue: + self.mineOrJoined = .joined + default: + self.mineOrJoined = .all + } + + switch sort { + case CallVanListSort.departureDesc.rawValue: + self.sort = .departureDesc + default: + self.sort = .latestDesc + } + + switch state { + case CallVanRecruitmentState.recruiting.rawValue: + self.state = .recruiting + case CallVanRecruitmentState.closed.rawValue: + self.state = .closed + default: + self.state = .all + } + + var departure: Set = [] + for selectedItem in filterGroupModels[3].selectedItems { + guard let selectedItem = CallVanPlace(rawValue: selectedItem.title) else { + return + } + if selectedItem == .all { + departure = Set.init(arrayLiteral: .all) + break + } else { + departure.insert(selectedItem) + continue + } + } + self.departure = departure + + var arrival: Set = [] + for selectedItem in filterGroupModels[4].selectedItems { + guard let selectedItem = CallVanPlace(rawValue: selectedItem.title) else { + return + } + if selectedItem == .all { + arrival = Set.init(arrayLiteral: .all) + break + } else { + arrival.insert(selectedItem) + continue + } + } + self.arrival = arrival + } +} diff --git a/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanListFilterViewController.swift b/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanListFilterViewController.swift deleted file mode 100644 index 2037ada4..00000000 --- a/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanListFilterViewController.swift +++ /dev/null @@ -1,523 +0,0 @@ -// -// CallVanListFilterViewController.swift -// koin -// -// Created by 홍기정 on 3/4/26. -// - -import UIKit -import Combine -import Then -import SnapKit - -final class CallVanListFilterViewController: UIViewController { - - // MARK: - Properties - @Published private var filter: CallVanListRequest - private let onApplyButtonTapped: (CallVanListRequest)->Void - private var subscriptions: Set = [] - private let isLoggedIn: Bool - private let height: CGFloat - - // MARK: - UI Components - private let titleLabel = UILabel() - private let closeButton = UIButton() - private let topSeparatorView = UIView() - - private let scrollView = UIScrollView() - private let contentView = UIView() - - private let listLabel = UILabel() - private let listButtonsStackView = UIStackView() - private let listButtons = [ - CallVanFilterButton(filterState: CallVanMineOrJoined.all), - CallVanFilterButton(filterState: CallVanMineOrJoined.mine), - CallVanFilterButton(filterState: CallVanMineOrJoined.joined) - ] - private let listSeparatorView = UIView() - private let sortLabel = UILabel() - private let sortButtonsStackView = UIStackView() - private let sortButtons = [ - CallVanFilterButton(filterState: CallVanListSort.latestDesc), - CallVanFilterButton(filterState: CallVanListSort.departureDesc) - ] - private let sortSeparatorView = UIView() - - private let stateLabel = UILabel() - private let stateButtonsStackView = UIStackView() - private let stateButtons = [ - CallVanFilterButton(filterState: CallVanRecruitmentState.all), - CallVanFilterButton(filterState: CallVanRecruitmentState.recruiting), - CallVanFilterButton(filterState: CallVanRecruitmentState.closed) - ] - private let stateSeparatorView = UIView() - - private let departureLabel = UILabel() - private let departureDescriptionLabel = UILabel() - private let departureButtonsStackView1 = UIStackView() - private let departureButtons1 = [ - CallVanFilterButton(filterState: CallVanPlace.all), - CallVanFilterButton(filterState: CallVanPlace.frontGate), - CallVanFilterButton(filterState: CallVanPlace.backGate), - CallVanFilterButton(filterState: CallVanPlace.terminal) - ] - private let departureButtonsStackView2 = UIStackView() - private let departureButtons2: [CallVanFilterButton] = [ - CallVanFilterButton(filterState: CallVanPlace.dormitoryMain), - CallVanFilterButton(filterState: CallVanPlace.dormitorySub), - CallVanFilterButton(filterState: CallVanPlace.station), - CallVanFilterButton(filterState: CallVanPlace.asanStation) - ] - private let departureSeparatorView = UIView() - - private let arrivalLabel = UILabel() - private let arrivalDescriptionLabel = UILabel() - private let arrivalButtonsStackView1 = UIStackView() - private let arrivalButtons1: [CallVanFilterButton] = [ - CallVanFilterButton(filterState: CallVanPlace.all), - CallVanFilterButton(filterState: CallVanPlace.frontGate), - CallVanFilterButton(filterState: CallVanPlace.backGate), - CallVanFilterButton(filterState: CallVanPlace.terminal) - ] - private let arrivalButtonsStackView2 = UIStackView() - private var arrivalButtons2: [CallVanFilterButton] = [ - CallVanFilterButton(filterState: CallVanPlace.dormitoryMain), - CallVanFilterButton(filterState: CallVanPlace.dormitorySub), - CallVanFilterButton(filterState: CallVanPlace.station), - CallVanFilterButton(filterState: CallVanPlace.asanStation) - ] - private let arrivalSeparatorView = UIView() - - private let resetButton = UIButton() - private let applyButton = UIButton() - private let bottomSeparatorView = UIView() - - // MARK: - Initializer - init( - filter: CallVanListRequest, - onApplyButtonTapped: @escaping (CallVanListRequest)->Void, - isLoggedIn: Bool, - height: CGFloat - ) { - self.filter = filter - self.onApplyButtonTapped = onApplyButtonTapped - self.isLoggedIn = isLoggedIn - self.height = height - super.init(nibName: nil, bundle: nil) - configureView() - bind() - } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func bind() { - $filter.sink { [weak self] filter in - guard let self else { return } - - // MARK: - List - listButtons.forEach { button in - button.isSelected = button.filterState as! CallVanMineOrJoined == filter.mineOrJoined - } - - // MARK: - Sort - sortButtons.forEach { button in - button.isSelected = button.filterState as! CallVanListSort == filter.sort - } - - // MARK: - State - stateButtons.forEach { button in - button.isSelected = button.filterState as! CallVanRecruitmentState == filter.state - } - - // MARK: - Departure - if filter.departure == [.all] { - (departureButtons1 + departureButtons2).forEach { button in - button.isSelected = false - } - departureButtons1.first?.isSelected = true - } else { - (departureButtons1 + departureButtons2).forEach { button in - button.isSelected = filter.departure.contains(button.filterState as! CallVanPlace) - } - } - - // MARK: - Arrival - if filter.arrival == [.all] { - (arrivalButtons1 + arrivalButtons2).forEach { button in - button.isSelected = false - } - arrivalButtons1.first?.isSelected = true - } else { - (arrivalButtons1 + arrivalButtons2).forEach { button in - button.isSelected = filter.arrival.contains(button.filterState as! CallVanPlace) - } - } - }.store(in: &subscriptions) - } -} - -extension CallVanListFilterViewController { - - private func configureView() { - view.backgroundColor = .white - setUpLayouts() - setUpStyles() - setUpConstraints() - setAddTargets() - } - - private func setUpStyles() { - // MARK: - Labels - titleLabel.do { - $0.text = "필터" - $0.font = UIFont.appFont(.pretendardSemiBold, size: 18) - $0.textColor = UIColor.appColor(.new500) - } - - [listLabel, sortLabel, stateLabel, departureLabel, arrivalLabel].forEach { - $0.font = UIFont.appFont(.pretendardSemiBold, size: 16) - $0.textColor = UIColor.appColor(.neutral800) - } - listLabel.text = "목록" - sortLabel.text = "정렬" - stateLabel.text = "모집 상태" - departureLabel.text = "출발지" - arrivalLabel.text = "도착지" - - [departureDescriptionLabel, arrivalDescriptionLabel].forEach { - $0.font = UIFont.appFont(.pretendardRegular, size: 12) - $0.textColor = UIColor.appColor(.neutral500) - $0.text = "기타 장소는 검색창을 이용해주세요." - } - - // MARK: - StackView - [listButtonsStackView, sortButtonsStackView, stateButtonsStackView, departureButtonsStackView1, departureButtonsStackView2, arrivalButtonsStackView1, arrivalButtonsStackView2].forEach { - $0.axis = .horizontal - $0.distribution = .fillProportionally - $0.spacing = 12 - } - - // MARK: - Separator View - [topSeparatorView, listSeparatorView, sortSeparatorView, stateSeparatorView, departureSeparatorView, arrivalSeparatorView, bottomSeparatorView].forEach { - $0.backgroundColor = UIColor.appColor(.neutral200) - } - - // MARK: - Buttons - closeButton.do { - $0.setImage(.appImage(asset: .newCancel), for: .normal) - $0.tintColor = UIColor.appColor(.neutral800) - } - resetButton.do { - var configuration = UIButton.Configuration.plain() - configuration.attributedTitle = AttributedString("초기화", attributes: AttributeContainer([ - .font : UIFont.appFont(.pretendardSemiBold, size: 16), - .foregroundColor : UIColor.appColor(.neutral600) - ])) - configuration.image = UIImage.appImage(asset: .refresh) - configuration.imagePadding = 8 - configuration.imagePlacement = .trailing - configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) - $0.configuration = configuration - $0.layer.borderColor = UIColor.appColor(.neutral400).cgColor - $0.layer.borderWidth = 1 - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - } - applyButton.do { - $0.setAttributedTitle(NSAttributedString( - string: "적용하기", - attributes: [ - .font : UIFont.appFont(.pretendardSemiBold, size: 16), - .foregroundColor : UIColor.appColor(.neutral0) - ]), for: .normal) - $0.backgroundColor = UIColor.appColor(.new500) - $0.layer.cornerRadius = 12 - } - } - - private func setUpLayouts() { - listButtons.forEach { - listButtonsStackView.addArrangedSubview($0) - } - sortButtons.forEach { - sortButtonsStackView.addArrangedSubview($0) - } - stateButtons.forEach { - stateButtonsStackView.addArrangedSubview($0) - } - departureButtons1.forEach { - departureButtonsStackView1.addArrangedSubview($0) - } - departureButtons2.forEach { - departureButtonsStackView2.addArrangedSubview($0) - } - arrivalButtons1.forEach { - arrivalButtonsStackView1.addArrangedSubview($0) - } - arrivalButtons2.forEach { - arrivalButtonsStackView2.addArrangedSubview($0) - } - - [listLabel, listButtonsStackView, listSeparatorView, - sortLabel, sortButtonsStackView, sortSeparatorView, - stateLabel, stateButtonsStackView, stateSeparatorView, - departureLabel, departureDescriptionLabel, departureButtonsStackView1, departureButtonsStackView2, departureSeparatorView, - arrivalLabel, arrivalDescriptionLabel, arrivalButtonsStackView1, arrivalButtonsStackView2, arrivalSeparatorView].forEach { - contentView.addSubview($0) - } - [contentView].forEach { - scrollView.addSubview($0) - } - [titleLabel, closeButton, topSeparatorView, - scrollView, - resetButton, applyButton, bottomSeparatorView].forEach { - view.addSubview($0) - } - } - - private func setUpConstraints() { - - // MARK: - 상단 - titleLabel.snp.makeConstraints { - $0.height.equalTo(29) - $0.top.equalToSuperview().offset(12) - $0.leading.equalToSuperview().offset(32) - } - closeButton.snp.makeConstraints { - $0.centerY.equalTo(titleLabel) - $0.trailing.equalToSuperview().offset(-24) - } - topSeparatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.top.equalTo(titleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview() - } - - // MARK: - ScrollView - let height = height - 54 - 72 - scrollView.snp.makeConstraints { - $0.top.equalTo(topSeparatorView.snp.bottom) - $0.leading.trailing.equalToSuperview() - $0.height.equalTo(height) - } - contentView.snp.makeConstraints { - $0.edges.equalTo(scrollView) - $0.width.equalTo(scrollView) - } - - // MARK: - List - listLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(12) - } - listButtonsStackView.snp.makeConstraints { - $0.top.equalTo(listLabel.snp.bottom).offset(12) - } - listSeparatorView.snp.makeConstraints { - $0.top.equalTo(listButtonsStackView.snp.bottom).offset(12) - } - - // MARK: - Sort - sortLabel.snp.makeConstraints { - $0.top.equalTo(listSeparatorView.snp.bottom).offset(12) - } - sortButtonsStackView.snp.makeConstraints { - $0.top.equalTo(sortLabel.snp.bottom).offset(12) - } - sortSeparatorView.snp.makeConstraints { - $0.top.equalTo(sortButtonsStackView.snp.bottom).offset(12) - } - - // MARK: - State - stateLabel.snp.makeConstraints { - $0.top.equalTo(sortSeparatorView.snp.bottom).offset(12) - } - stateButtonsStackView.snp.makeConstraints { - $0.top.equalTo(stateLabel.snp.bottom).offset(12) - } - stateSeparatorView.snp.makeConstraints { - $0.top.equalTo(stateButtonsStackView.snp.bottom).offset(12) - } - - // MARK: - Departure - departureLabel.snp.makeConstraints { - $0.top.equalTo(stateSeparatorView.snp.bottom).offset(12) - } - departureDescriptionLabel.snp.makeConstraints { - $0.centerY.equalTo(departureLabel) - $0.leading.equalTo(departureLabel.snp.trailing).offset(8) - } - departureButtonsStackView1.snp.makeConstraints { - $0.top.equalTo(departureLabel.snp.bottom).offset(12) - } - departureButtonsStackView2.snp.makeConstraints { - $0.top.equalTo(departureButtonsStackView1.snp.bottom).offset(8) - } - departureSeparatorView.snp.makeConstraints { - $0.top.equalTo(departureButtonsStackView2.snp.bottom).offset(12) - } - - // MARK: - Arrival - arrivalLabel.snp.makeConstraints { - $0.top.equalTo(departureSeparatorView.snp.bottom).offset(12) - } - arrivalDescriptionLabel.snp.makeConstraints { - $0.centerY.equalTo(arrivalLabel) - $0.leading.equalTo(arrivalLabel.snp.trailing).offset(8) - } - arrivalButtonsStackView1.snp.makeConstraints { - $0.top.equalTo(arrivalLabel.snp.bottom).offset(12) - } - arrivalButtonsStackView2.snp.makeConstraints { - $0.top.equalTo(arrivalButtonsStackView1.snp.bottom).offset(8) - } - arrivalSeparatorView.snp.makeConstraints { - $0.top.equalTo(arrivalButtonsStackView2.snp.bottom).offset(12) - $0.bottom.equalTo(contentView).offset(-12) - } - - // MARK: - 하단 - resetButton.snp.makeConstraints { - $0.height.equalTo(48) - $0.width.equalTo(resetButton.intrinsicContentSize.width) - $0.top.equalTo(scrollView.snp.bottom).offset(12) - $0.leading.equalToSuperview().offset(32) - } - applyButton.snp.makeConstraints { - $0.top.bottom.equalTo(resetButton) - $0.leading.equalTo(resetButton.snp.trailing).offset(12) - $0.trailing.equalToSuperview().offset(-32) - } - bottomSeparatorView.snp.makeConstraints { - $0.height.equalTo(1) - $0.top.equalTo(resetButton.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview() - } - - // MARK: - Common - [listLabel, sortLabel, stateLabel, departureLabel, arrivalLabel].forEach { - $0.snp.makeConstraints { - $0.height.equalTo(26) - $0.leading.equalTo(titleLabel) - } - } - [departureDescriptionLabel, arrivalDescriptionLabel].forEach { - $0.snp.makeConstraints { - $0.height.equalTo(19) - } - } - [listButtonsStackView, sortButtonsStackView, stateButtonsStackView, departureButtonsStackView1, departureButtonsStackView2, arrivalButtonsStackView1, arrivalButtonsStackView2].forEach { - $0.snp.makeConstraints { - $0.height.equalTo(34) - $0.leading.equalTo(titleLabel) - } - } - [listSeparatorView, sortSeparatorView, stateSeparatorView, departureSeparatorView, arrivalSeparatorView].forEach { - $0.snp.makeConstraints { - $0.height.equalTo(1) - $0.leading.trailing.equalToSuperview().inset(32) - } - } - } -} - -extension CallVanListFilterViewController { - - private func setAddTargets() { - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - listButtons.forEach { - $0.addTarget(self, action: #selector(listButtonTapped(_:)), for: .touchUpInside) - } - sortButtons.forEach { - $0.addTarget(self, action: #selector(sortButtonTapped(_:)), for: .touchUpInside) - } - stateButtons.forEach { - $0.addTarget(self, action: #selector(stateButtonTapped(_:)), for: .touchUpInside) - } - (departureButtons1 + departureButtons2).forEach { - $0.addTarget(self, action: #selector(departureButtonTapped(_:)), for: .touchUpInside) - } - (arrivalButtons1 + arrivalButtons2).forEach { - $0.addTarget(self, action: #selector(arrivalButtonTapped(_:)), for: .touchUpInside) - } - - resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside) - applyButton.addTarget(self, action: #selector(applyButtonTapped), for: .touchUpInside) - } - - @objc private func closeButtonTapped() { - dismissView() - } - - @objc private func listButtonTapped(_ sender: UIButton) { - guard let listButton = sender as? CallVanFilterButton, - let state = listButton.filterState as? CallVanMineOrJoined else { - return - } - if state != .all && isLoggedIn != true { - showToastMessage(message: "로그인이 필요한 기능입니다.") - } else { - filter.mineOrJoined = state - } - } - - @objc private func sortButtonTapped(_ sender: UIButton) { - if let sortButton = sender as? CallVanFilterButton, - let sort = sortButton.filterState as? CallVanListSort { - filter.sort = sort - } - } - - @objc private func stateButtonTapped(_ sender: UIButton) { - if let stateButton = sender as? CallVanFilterButton, - let state = stateButton.filterState as? CallVanRecruitmentState { - filter.state = state - } - } - - // MARK: - Departure - @objc private func departureButtonTapped(_ sender: UIButton) { - guard let departureButton = sender as? CallVanFilterButton, - let departure = departureButton.filterState as? CallVanPlace else { - return - } - if departure == .all { - filter.departure = [.all] - } else { - filter.departure.remove(.all) - if filter.departure == [departure] { - return - } else { - filter.departure.formSymmetricDifference([departure]) - } - } - } - - // MARK: - Arrival - @objc private func arrivalButtonTapped(_ sender: UIButton) { - guard let arrivalButton = sender as? CallVanFilterButton, - let arrival = arrivalButton.filterState as? CallVanPlace else { - return - } - if arrival == .all { - filter.arrival = [.all] - } else { - filter.arrival.remove(.all) - if filter.arrival == [arrival] { - return - } else { - filter.arrival.formSymmetricDifference([arrival]) - } - } - } - - // MARK: - - @objc private func resetButtonTapped() { - self.filter = CallVanListRequest() - } - @objc private func applyButtonTapped() { - onApplyButtonTapped(filter) - dismissView() - } -} diff --git a/Koin/Presentation/CallVan/CallVanList/Subviews/CallVanFilterButton.swift b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanFilterButton.swift similarity index 100% rename from Koin/Presentation/CallVan/CallVanList/Subviews/CallVanFilterButton.swift rename to Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanFilterButton.swift diff --git a/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift b/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift index e21f6a8b..3f45112f 100644 --- a/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift +++ b/Koin/Presentation/Shared/FilterBottomSheet/FilterBottomSheetView.swift @@ -15,6 +15,7 @@ class FilterBottomSheetView: UIView { // MARK: - Properties weak var delegate: BottomSheetViewControllerBDelegate? private var groupModels: [FilterGroupModel] + private let onFilterItemTapped: (FilterItemModel)->Bool private let onApplyTapped: ([FilterGroupModel])->Void private var subscriptions: Set = [] @@ -35,9 +36,11 @@ class FilterBottomSheetView: UIView { // MARK: - Initializer init( groupModels: [FilterGroupModel], + onFilterItemTapped: @escaping (FilterItemModel)->Bool, onApplyTapped: @escaping ([FilterGroupModel])->Void ) { self.groupModels = groupModels + self.onFilterItemTapped = onFilterItemTapped self.onApplyTapped = onApplyTapped self.filterGroupViews = groupModels.map { group in FilterGroupView(filterGroup: group) @@ -65,6 +68,11 @@ class FilterBottomSheetView: UIView { } private func didTapItem(groupIndex: Int, itemIndex: Int) { + let tappedItem = groupModels[groupIndex].items[itemIndex] + guard onFilterItemTapped(tappedItem) else { + return + } + let before = groupModels[groupIndex].items.map(\.isSelected) groupModels[groupIndex].didTap(itemAt: itemIndex) let after = groupModels[groupIndex].items.map(\.isSelected) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 2161bca7..c01a4cdd 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -245,6 +245,7 @@ 7C8261973040344700C20F64 /* FilterGroupCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8261963040344700C20F64 /* FilterGroupCollectionView.swift */; }; 7C82619930403C5E00C20F64 /* FilterGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82619830403C5E00C20F64 /* FilterGroupCollectionViewCell.swift */; }; 7C82619C30407FF300C20F64 /* FilterGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82619B30407FF300C20F64 /* FilterGroupView.swift */; }; + 7C8261A03040C7E700C20F64 /* CallVanListRequest+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82619F3040C7E700C20F64 /* CallVanListRequest+.swift */; }; 7C82FF782E9D18C7006335A7 /* OrderShopDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C82FF772E9D18C7006335A7 /* OrderShopDetail.swift */; }; 7C85D2752EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C85D2742EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift */; }; 7C86750E30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C86750D30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift */; }; @@ -383,7 +384,6 @@ 7CC42B1E2F56F3B200940CE1 /* CallVanButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B1D2F56F3B200940CE1 /* CallVanButton.swift */; }; 7CC42B202F56F3C400940CE1 /* CallVanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B1F2F56F3C400940CE1 /* CallVanState.swift */; }; 7CC42B222F56F9C100940CE1 /* CallVanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B212F56F9C100940CE1 /* CallVanList.swift */; }; - 7CC42B282F575E8100940CE1 /* CallVanListFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B272F575E8100940CE1 /* CallVanListFilterViewController.swift */; }; 7CC42B2A2F575F4D00940CE1 /* CallVanFilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B292F575F4D00940CE1 /* CallVanFilterButton.swift */; }; 7CC42B2F2F58105B00940CE1 /* CallVanBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B2E2F58105B00940CE1 /* CallVanBottomSheetViewController.swift */; }; 7CC42B332F58247D00940CE1 /* CallVanNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC42B322F58247D00940CE1 /* CallVanNotification.swift */; }; @@ -1284,6 +1284,7 @@ 7C8261963040344700C20F64 /* FilterGroupCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupCollectionView.swift; sourceTree = ""; }; 7C82619830403C5E00C20F64 /* FilterGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupCollectionViewCell.swift; sourceTree = ""; }; 7C82619B30407FF300C20F64 /* FilterGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterGroupView.swift; sourceTree = ""; }; + 7C82619F3040C7E700C20F64 /* CallVanListRequest+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallVanListRequest+.swift"; sourceTree = ""; }; 7C82FF772E9D18C7006335A7 /* OrderShopDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopDetail.swift; sourceTree = ""; }; 7C85D2742EBDE374005E63FF /* FetchOrderShopMenusAndGroupsFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopMenusAndGroupsFromShopUseCase.swift; sourceTree = ""; }; 7C86750D30061AA0003CB942 /* CheckHasUnreadNotificationHistoryUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckHasUnreadNotificationHistoryUseCase.swift; sourceTree = ""; }; @@ -1406,7 +1407,6 @@ 7CC42B1D2F56F3B200940CE1 /* CallVanButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanButton.swift; sourceTree = ""; }; 7CC42B1F2F56F3C400940CE1 /* CallVanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanState.swift; sourceTree = ""; }; 7CC42B212F56F9C100940CE1 /* CallVanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanList.swift; sourceTree = ""; }; - 7CC42B272F575E8100940CE1 /* CallVanListFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanListFilterViewController.swift; sourceTree = ""; }; 7CC42B292F575F4D00940CE1 /* CallVanFilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanFilterButton.swift; sourceTree = ""; }; 7CC42B2E2F58105B00940CE1 /* CallVanBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanBottomSheetViewController.swift; sourceTree = ""; }; 7CC42B322F58247D00940CE1 /* CallVanNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanNotification.swift; sourceTree = ""; }; @@ -3188,6 +3188,7 @@ 7CC42B092F56AB0100940CE1 /* CallVanList */ = { isa = PBXGroup; children = ( + 7C82619E3040C7C100C20F64 /* Mapper */, 7CC42B0F2F56AB7E00940CE1 /* Subviews */, 7CC42B0D2F56AB7900940CE1 /* CallVanListViewController.swift */, 7CC42B432F5991AE00940CE1 /* CallVanListViewModel.swift */, @@ -3199,8 +3200,6 @@ isa = PBXGroup; children = ( 7CC42B262F575E6900940CE1 /* CallVanListCollectionView */, - 7CC42B292F575F4D00940CE1 /* CallVanFilterButton.swift */, - 7CC42B272F575E8100940CE1 /* CallVanListFilterViewController.swift */, 7CC42B2E2F58105B00940CE1 /* CallVanBottomSheetViewController.swift */, ); path = Subviews; @@ -3308,6 +3307,7 @@ 7CC42B5B2F59CFD600940CE1 /* CallVanPostTimeView.swift */, 7CC42B5F2F59D06C00940CE1 /* CallVanPostParticipantsView.swift */, 7CC42B7A2F5CBD3B00940CE1 /* CallVanPostPlaceBottomSheetView.swift */, + 7CC42B292F575F4D00940CE1 /* CallVanFilterButton.swift */, ); path = Subviews; sourceTree = ""; @@ -6392,6 +6392,7 @@ 7C4D5A482F01638800B40128 /* BusTimetableDataViewModel.swift in Sources */, 7C4D5A492F01638800B40128 /* BusSearchDatePickerViewController.swift in Sources */, 7C640412300F435E00A9589F /* DepartmentCategoryDto.swift in Sources */, + 7C8261A03040C7E700C20F64 /* CallVanListRequest+.swift in Sources */, 7C4D5A4A2F01638800B40128 /* BusTimetableRouteCollectionView.swift in Sources */, 7CC42B442F5991AE00940CE1 /* CallVanListViewModel.swift in Sources */, 7C61D34D2F220EBA005FAC3A /* EditLostItemContentView.swift in Sources */, From c5b8e846affb7b572ce960b38615ccda8bc7c3d3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 28 Aug 2026 19:31:51 +0900 Subject: [PATCH 082/101] =?UTF-8?q?fix:=20CallVanPost=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIStackView에 버튼을 넣어서 구현한 UI를 삭제, FilterGroupCollectionView를 사용하도록 변경했습니다. --- .../CallVanPostViewController.swift | 4 +- .../CallVanPostPlaceBottomSheetView.swift | 328 +++++++++--------- .../Model/FilterGroupModel.swift | 10 +- 3 files changed, 164 insertions(+), 178 deletions(-) diff --git a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift index ac648c14..d4d97e86 100644 --- a/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift +++ b/Koin/Presentation/CallVan/CallVanPost/CallVanPostViewController.swift @@ -201,7 +201,7 @@ extension CallVanPostViewController { } bottomSheetContentView.configure( title: .departure, - place: viewModel.request.departureType, + selectedPlace: viewModel.request.departureType, customPlace: viewModel.request.departureCustomName, onApplyButtonTapped: onApplyButtonTapped ) @@ -219,7 +219,7 @@ extension CallVanPostViewController { } bottomSheetContentView.configure( title: .arrival, - place: viewModel.request.arrivalType, + selectedPlace: viewModel.request.arrivalType, customPlace: viewModel.request.arrivalCustomName, onApplyButtonTapped: onApplyButtonTapped ) diff --git a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift index 01272067..68abcf02 100644 --- a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift +++ b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift @@ -17,35 +17,58 @@ final class CallVanPostPlaceBottomSheetView: UIView { case arrival = "도착지가 어디인가요?" } + // MARK: - State + private var selectedPlace: CallVanPlace? { + didSet { + guard let selectedPlace else { + applyButton.isEnabled = false + return + } + updateSelection(selectedPlace) + updateTextField(isEditing: selectedPlace == .custom) + validate() + } + } + private var customPlace: String? { + didSet { + validate() + } + } + // MARK: - Properties weak var delegate: BottomSheetViewControllerBDelegate? private var onApplyButtonTapped: ((CallVanPlace, String?)->Void)? + private var filterGroup = FilterGroupModel( + title: "", + hasAllButton: false, + items: [ + CallVanPlace.frontGate.rawValue, + CallVanPlace.backGate.rawValue, + CallVanPlace.dormitoryMain.rawValue, + CallVanPlace.dormitorySub.rawValue, + CallVanPlace.terminal.rawValue, + CallVanPlace.station.rawValue, + CallVanPlace.asanStation.rawValue, + CallVanPlace.custom.rawValue + ], + behavior: .single, + allowEmptySelection: true + ) + private var subscriptions: Set = [] // MARK: - UI Components - private let containerView = UIView() - private let titleLabel = UILabel() private let closeButton = UIButton() private let topSeparatorView = UIView() - private let buttonsStackView1 = UIStackView() - private let buttons1 = [ - CallVanFilterButton(filterState: CallVanPlace.frontGate), - CallVanFilterButton(filterState: CallVanPlace.backGate), - CallVanFilterButton(filterState: CallVanPlace.dormitoryMain), - CallVanFilterButton(filterState: CallVanPlace.dormitorySub) - ] - - private let buttonsStackView2 = UIStackView() - private let buttons2 = [ - CallVanFilterButton(filterState: CallVanPlace.terminal), - CallVanFilterButton(filterState: CallVanPlace.station), - CallVanFilterButton(filterState: CallVanPlace.asanStation) - ] - private let customButton = CallVanFilterButton(filterState: CallVanPlace.custom) + private lazy var filterGroupCollectionView = FilterGroupCollectionView(filterGroup: filterGroup) private let separatorView = UIView() - private let customPlaceTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral800), font: UIFont.appFont(.pretendardMedium, size: 15)) + private let customPlaceTextField = DefaultTextField( + placeholder: "", + placeholderColor: UIColor.appColor(.neutral800), + font: UIFont.appFont(.pretendardMedium, size: 15) + ) private let applyButton = UIButton() private let bottomSeparatorView = UIView() @@ -55,177 +78,121 @@ final class CallVanPostPlaceBottomSheetView: UIView { configureView() setAddTargets() setDelegate() + bind() } // MARK: - Public func configure( title: Title, - place: CallVanPlace?, + selectedPlace: CallVanPlace?, customPlace: String?, onApplyButtonTapped: @escaping (CallVanPlace, String?)->Void ) { + self.selectedPlace = selectedPlace + self.customPlace = customPlace self.onApplyButtonTapped = onApplyButtonTapped - titleLabel.text = title.rawValue - - resetState() - - if place == .custom { - (buttons1 + buttons2).forEach { - $0.isSelected = false - } - customButton.isSelected = true - customPlaceTextField.text = customPlace - updateCustomPlaceTextField(isVisible: true) - updateApplyButtonTitle(isCustomSelected: true) - valiate(customPlaceTextField) - } else { - let place = place ?? CallVanPlace.frontGate - (buttons1 + buttons2).forEach { - $0.isSelected = $0.filterState as? CallVanPlace == place - } - customButton.isSelected = false - updateCustomPlaceTextField(isVisible: false) - updateApplyButtonTitle(isCustomSelected: false) - applyButton.backgroundColor = UIColor.appColor(.new500) - applyButton.isEnabled = true - } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: - Bind + private func bind() { + filterGroupCollectionView.itemTappedPublisher + .sink { [weak self] selectedIndex in + guard let self, + let selectedPlace = CallVanPlace(rawValue: filterGroup.items[selectedIndex].title) else { + return + } + self.selectedPlace = selectedPlace + } + .store(in: &subscriptions) + } } extension CallVanPostPlaceBottomSheetView { + // MARK: - Set Delegate + private func setDelegate() { + customPlaceTextField.delegate = self + } + // MARK: - Set AddTargets private func setAddTargets() { closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - (buttons1 + buttons2).forEach { - $0.addTarget(self, action: #selector(placeButtonTapped(_:)), for: .touchUpInside) - } - customButton.addTarget(self, action: #selector(customButtonTapped(_:)), for: .touchUpInside) - + customPlaceTextField.addTarget(self, action: #selector(editingChanged), for: .editingChanged) applyButton.addTarget(self, action: #selector(applyButtonTapped), for: .touchUpInside) - - customPlaceTextField.addTarget(self, action: #selector(valiate(_:)), for: .editingChanged) } + // MARK: - Objc @objc private func closeButtonTapped() { customPlaceTextField.resignFirstResponder() delegate?.dismiss() } - @objc private func placeButtonTapped(_ sender: UIButton) { - if let placeButton = sender as? CallVanFilterButton { - (buttons1 + buttons2).forEach { - $0.isSelected = $0.filterState.rawValue == placeButton.filterState.rawValue - } - } - customButton.isSelected = false - - updateCustomPlaceTextField(isVisible: false) - UIView.animate( - withDuration: 0.2, - animations: { [weak self] in - self?.superview?.layoutIfNeeded() - self?.layoutIfNeeded() - self?.applyButton.isEnabled = true - self?.applyButton.backgroundColor = UIColor.appColor(.new500) - self?.updateApplyButtonTitle(isCustomSelected: false) - }, - completion: { [weak self] _ in - self?.customPlaceTextField.resignFirstResponder() - } - ) - } - - @objc private func customButtonTapped(_ sender: UIButton) { - (buttons1 + buttons2).forEach { - $0.isSelected = false - } - sender.isSelected = true - - updateCustomPlaceTextField(isVisible: true) - UIView.animate( - withDuration: 0.2, - animations: { [weak self] in - self?.superview?.layoutIfNeeded() - self?.layoutIfNeeded() - self?.updateApplyButtonTitle(isCustomSelected: true) - }, - completion: { [weak self] _ in - self?.customPlaceTextField.becomeFirstResponder() - } - ) - } - @objc private func applyButtonTapped() { - if let selectedButton = (buttons1 + buttons2).first(where: { $0.isSelected }), - let selectedPlace = selectedButton.filterState as? CallVanPlace { - onApplyButtonTapped?(selectedPlace, nil) - } - else if customButton.isSelected, - let customPlace = customPlaceTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), - !customPlace.isEmpty { + guard let selectedPlace else { return } + customPlaceTextField.resignFirstResponder() + + switch selectedPlace { + case .custom: onApplyButtonTapped?(.custom, customPlace) + default: + onApplyButtonTapped?(selectedPlace, nil) } - customPlaceTextField.resignFirstResponder() + delegate?.dismiss() } + + @objc private func editingChanged() { + customPlace = customPlaceTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) + } } extension CallVanPostPlaceBottomSheetView: UITextFieldDelegate { - - private func setDelegate() { - customPlaceTextField.delegate = self - } - + // MARK: - Handle Textfield func textFieldShouldReturn(_ textField: UITextField) -> Bool { + customPlace = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) self.endEditing(true) return true } func textFieldDidBeginEditing(_ textField: UITextField) { - valiate(textField) + customPlace = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) } - @objc private func valiate(_ textField: UITextField) { - if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines), - !text.isEmpty { - applyButton.backgroundColor = UIColor.appColor(.new500) - applyButton.isEnabled = true - } else { - applyButton.backgroundColor = UIColor.appColor(.neutral400) - applyButton.isEnabled = false - } + private func updateTextField(isEditing: Bool) { + updateTextField(isVisible: isEditing) + updateApplyButton(isCustomSelected: isEditing) } -} - -extension CallVanPostPlaceBottomSheetView { - - private func resetState() { - (buttons1 + buttons2).forEach { - $0.isSelected = false - } - customButton.isSelected = false - customPlaceTextField.text = nil - customPlaceTextField.resignFirstResponder() - updateCustomPlaceTextField(isVisible: false) - updateApplyButtonTitle(isCustomSelected: false) - applyButton.backgroundColor = UIColor.appColor(.new500) - applyButton.isEnabled = true - } - - private func updateCustomPlaceTextField(isVisible: Bool) { + + private func updateTextField(isVisible: Bool) { customPlaceTextField.snp.remakeConstraints { $0.height.equalTo(isVisible ? 47 : 0) $0.top.equalTo(separatorView.snp.bottom).offset(isVisible ? 24 : 0) $0.leading.trailing.equalToSuperview().inset(32) } + + if isVisible { + customPlaceTextField.isHidden = false + } + + UIView.animate(springDuration: 0.2) { [weak self] in + self?.superview?.layoutIfNeeded() + self?.layoutIfNeeded() + self?.customPlaceTextField.alpha = isVisible ? 1 : 0 + } completion: { [weak self] _ in + self?.customPlaceTextField.isHidden = !isVisible + } + + if isVisible { + customPlaceTextField.becomeFirstResponder() + } else { + customPlaceTextField.resignFirstResponder() + } } - - private func updateApplyButtonTitle(isCustomSelected: Bool) { + + private func updateApplyButton(isCustomSelected: Bool) { let title = isCustomSelected ? "입력완료" : "선택하기" applyButton.setAttributedTitle(NSAttributedString( string: title, @@ -235,7 +202,52 @@ extension CallVanPostPlaceBottomSheetView { ]), for: .normal ) } +} + +extension CallVanPostPlaceBottomSheetView { + // MARK: - Update CollecitonView + private func updateSelection(_ selectedPlace: CallVanPlace) { + guard let selectedIndex = filterGroup.items.firstIndex(where: { $0.title == selectedPlace.rawValue }) else { + return + } + + let before = filterGroup.items.map(\.isSelected) + filterGroup.didTap(itemAt: selectedIndex) + let after = filterGroup.items.map(\.isSelected) + + filterGroupCollectionView.update( + filterGroup: filterGroup, + changed: Self.changedIndexPaths(before: before, after: after) + ) + } + private static func changedIndexPaths(before: [Bool], after: [Bool]) -> [IndexPath] { + zip(before, after).enumerated().compactMap { index, pair in + pair.0 != pair.1 ? IndexPath(row: index, section: 0) : nil + } + } +} + +extension CallVanPostPlaceBottomSheetView { + // MARK: - Validate + private func validate() { + applyButton.backgroundColor = isValid ? UIColor.appColor(.new500) : UIColor.appColor(.neutral400) + applyButton.isEnabled = isValid + } + + private var isValid: Bool { + guard let selectedPlace else { return false } + switch selectedPlace { + case .custom: + let isEmpty = customPlace?.isEmpty ?? true + return !isEmpty + default: + return true + } + } +} + +extension CallVanPostPlaceBottomSheetView { private func configureView() { setUpStyles() setUpLayouts() @@ -243,7 +255,7 @@ extension CallVanPostPlaceBottomSheetView { } private func setUpStyles() { - containerView.do { + self.do { $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] $0.layer.cornerRadius = 32 $0.backgroundColor = UIColor.appColor(.neutral0) @@ -260,11 +272,6 @@ extension CallVanPostPlaceBottomSheetView { [topSeparatorView, separatorView, bottomSeparatorView].forEach { $0.backgroundColor = UIColor.appColor(.neutral300) } - [buttonsStackView1, buttonsStackView2].forEach { - $0.axis = .horizontal - $0.spacing = 12 - $0.distribution = .fillProportionally - } customPlaceTextField.do { $0.layer.cornerRadius = 12 $0.layer.borderColor = UIColor.ColorSystem.Neutral.gray400.cgColor @@ -289,33 +296,18 @@ extension CallVanPostPlaceBottomSheetView { } private func setUpLayouts() { - buttons1.forEach { - buttonsStackView1.addArrangedSubview($0) - } - buttons2.forEach { - buttonsStackView2.addArrangedSubview($0) - } - buttonsStackView2.addArrangedSubview(customButton) - [titleLabel, closeButton, topSeparatorView, - buttonsStackView1, buttonsStackView2, + filterGroupCollectionView, separatorView, customPlaceTextField, applyButton, bottomSeparatorView].forEach { - containerView.addSubview($0) - } - - [containerView].forEach { addSubview($0) } } private func setUpConstraints() { - containerView.snp.makeConstraints { - $0.edges.equalToSuperview() - } titleLabel.snp.makeConstraints { $0.height.equalTo(29) - $0.top.equalTo(containerView).offset(12) + $0.top.equalToSuperview().offset(12) $0.leading.equalToSuperview().offset(32) } closeButton.snp.makeConstraints { @@ -327,21 +319,13 @@ extension CallVanPostPlaceBottomSheetView { $0.leading.trailing.equalToSuperview() $0.top.equalTo(titleLabel.snp.bottom).offset(12) } - - buttonsStackView1.snp.makeConstraints { - $0.height.equalTo(34) + filterGroupCollectionView.snp.makeConstraints { $0.top.equalTo(topSeparatorView.snp.bottom).offset(12) - $0.leading.equalToSuperview().offset(32) - } - buttonsStackView2.snp.makeConstraints { - $0.height.equalTo(34) - $0.top.equalTo(buttonsStackView1.snp.bottom).offset(8) - $0.leading.equalTo(buttonsStackView1) + $0.leading.trailing.equalToSuperview().inset(32) } - separatorView.snp.makeConstraints { $0.height.equalTo(1) - $0.top.equalTo(buttonsStackView2.snp.bottom).offset(12) + $0.top.equalTo(filterGroupCollectionView.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(32) } customPlaceTextField.snp.makeConstraints { diff --git a/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift index 5d015f9c..3b7bd16d 100644 --- a/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift +++ b/Koin/Presentation/Shared/FilterBottomSheet/Model/FilterGroupModel.swift @@ -26,6 +26,7 @@ struct FilterGroupModel { hasAllButton: Bool, items: [String], behavior: Behavior, + allowEmptySelection: Bool = false ) { self.title = title self.description = description @@ -40,7 +41,7 @@ struct FilterGroupModel { } if !self.items.contains(where: { $0.isSelected }) { - reset() + reset(allowEmptySelection) } } } @@ -52,8 +53,9 @@ extension FilterGroupModel { } extension FilterGroupModel { - mutating func reset() { - deselectAll(except: 0) + mutating func reset(_ allowEmptySelection: Bool = false) { + let selectedIndex: Int? = allowEmptySelection ? nil : 0 + deselectAll(except: selectedIndex) } mutating func didTap(itemAt index: Int) { @@ -67,7 +69,7 @@ extension FilterGroupModel { } extension FilterGroupModel { - mutating private func deselectAll(except selectedIndex: Int) { + mutating private func deselectAll(except selectedIndex: Int?) { for index in items.indices { items[index].isSelected = index == selectedIndex } From a4a4b9e5d544e1cd88c30d46f4a6de663d22d0c2 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Fri, 28 Aug 2026 19:50:01 +0900 Subject: [PATCH 083/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CallVan/CallVanList/Mapper/CallVanListRequest+.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift b/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift index b3e0723c..4cf40b38 100644 --- a/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift +++ b/Koin/Presentation/CallVan/CallVanList/Mapper/CallVanListRequest+.swift @@ -82,7 +82,7 @@ extension CallVanListRequest { } arrival.forEach { place in - filterGroupModels[3].didTap(itemAt: place.index) + filterGroupModels[4].didTap(itemAt: place.index) } return filterGroupModels From 6e7a1dba616460f874b0026697633a1fba277b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 5 Sep 2026 22:26:34 +0900 Subject: [PATCH 084/101] =?UTF-8?q?test:=20=EC=84=B8=EA=B7=B8=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EB=AF=B8=EC=84=A0=ED=83=9D=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=EB=AC=B4=EC=8B=9C=20=EC=8A=A4?= =?UTF-8?q?=EC=99=80=EC=9D=B4=ED=94=84=20=EB=A1=9C=EA=B9=85=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- koinUnitTests/Dining/DiningLoggingTests.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/koinUnitTests/Dining/DiningLoggingTests.swift b/koinUnitTests/Dining/DiningLoggingTests.swift index 4229a5c7..1988eb5b 100644 --- a/koinUnitTests/Dining/DiningLoggingTests.swift +++ b/koinUnitTests/Dining/DiningLoggingTests.swift @@ -99,4 +99,25 @@ struct DiningLoggingTests { #expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", "저녁")]) } + + @Test("세그먼트가 선택되지 않으면 로깅하지 않는다") + func 세그먼트가_선택되지_않으면_로깅하지_않는다() { + let bed = DiningLoggingTestBed() + bed.selectSegment(-1) + + bed.tapMenuImage(place: "A코너") + bed.scrollDiningList() + + #expect(bed.recorder.loggedEvents.isEmpty) + } + + @Test("무시된 스와이프는 로깅하지 않는다") + func 무시된_스와이프는_로깅하지_않는다() { + let bed = DiningLoggingTestBed() + bed.selectSegment(0) + + bed.swipe(.right) + + #expect(bed.recorder.loggedEvents.isEmpty) + } } From ed7831bd7bf2ea5d2fe4b39fee37955b974ccb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sat, 5 Sep 2026 22:32:44 +0900 Subject: [PATCH 085/101] =?UTF-8?q?fix:=20=EC=84=B8=EA=B7=B8=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EB=AF=B8=EC=84=A0=ED=83=9D/=EB=AC=B4=EC=8B=9C?= =?UTF-8?q?=EB=90=9C=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=98=EB=AA=BB=20=EA=B8=B0=EB=A1=9D=EB=90=98?= =?UTF-8?q?=EB=8D=98=20=EB=A1=9C=EA=B9=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dining/Dining/DiningViewController.swift | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Koin/Presentation/Dining/Dining/DiningViewController.swift b/Koin/Presentation/Dining/Dining/DiningViewController.swift index b664afc9..4a6e3f5f 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewController.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewController.swift @@ -173,8 +173,10 @@ final class DiningViewController: UIViewController { zoomedImageViewController.setImage(tappedDiningImage) self.present(zoomedImageViewController, animated: true, completion: nil) - inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(self.currentDiningType.name)_\(tappedPlaceText)")) - + if let currentDiningType = self.currentDiningType { + inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(currentDiningType.name)_\(tappedPlaceText)")) + } + }.store(in: &subscriptions) diningListCollectionView.shareButtonPublisher.sink { [weak self] item in @@ -183,8 +185,8 @@ final class DiningViewController: UIViewController { }.store(in: &subscriptions) diningListCollectionView.logScrollPublisher.sink { [weak self] _ in - guard let self = self else { return } - self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.currentDiningType.name)) + guard let self = self, let currentDiningType = self.currentDiningType else { return } + self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, currentDiningType.name)) }.store(in: &subscriptions) NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification).sink { [weak self] _ in @@ -213,21 +215,26 @@ extension DiningViewController { @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { let currentSegmentIndex = diningTypeSegmentControl.selectedSegmentIndex - if gesture.direction == .left { - if currentSegmentIndex < diningTypeSegmentControl.numberOfSegments - 1 { - diningTypeSegmentControl.selectedSegmentIndex = currentSegmentIndex + 1 - } - } else if gesture.direction == .right { - if currentSegmentIndex > 0 { - diningTypeSegmentControl.selectedSegmentIndex = currentSegmentIndex - 1 - } + let nextSegmentIndex: Int + + switch gesture.direction { + case .left: nextSegmentIndex = currentSegmentIndex + 1 + case .right: nextSegmentIndex = currentSegmentIndex - 1 + default: return } + + guard (0.. Date: Sun, 6 Sep 2026 17:04:39 +0900 Subject: [PATCH 086/101] =?UTF-8?q?delete:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=ED=8C=85=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrderFloatingButton.swift | 174 ------------------ koin.xcodeproj/project.pbxproj | 4 - 2 files changed, 178 deletions(-) delete mode 100644 Koin/Core/View/OrderHistoryUIComponents/OrderFloatingButton.swift diff --git a/Koin/Core/View/OrderHistoryUIComponents/OrderFloatingButton.swift b/Koin/Core/View/OrderHistoryUIComponents/OrderFloatingButton.swift deleted file mode 100644 index 5bca7eff..00000000 --- a/Koin/Core/View/OrderHistoryUIComponents/OrderFloatingButton.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// OrderFloatingButton.swift -// koin -// -// Created by 이은지 on 9/8/25. -// - -import UIKit -import Lottie -import Then -import SnapKit - -final class OrderFloatingButton: UIControl { - - // MARK: - Properties - var titleText: String? { - get { titleLabel.text } - set { titleLabel.text = newValue } - } - - var subtitleText: String? { - get { subtitleLabel.text } - set { subtitleLabel.text = newValue } - } - - var rightImage: UIImage? { - get { rightImageView.image } - set { rightImageView.image = newValue } - } - - // MARK: - UI Components - private let containerView = UIView().then { - $0.backgroundColor = .white - $0.layer.cornerRadius = 32 - $0.layer.shadowColor = UIColor.black.cgColor - $0.layer.shadowOffset = CGSize(width: 0, height: 4) - $0.layer.shadowRadius = 8 - $0.layer.shadowOpacity = 0.2 - } - - let lottieView = LottieAnimationView().then { - $0.contentMode = .scaleAspectFit - $0.loopMode = .loop - } - - private let labelStackView = UIStackView().then { - $0.axis = .vertical - $0.alignment = .leading - $0.distribution = .fillEqually - $0.spacing = 0 - } - - private let titleLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardBold, size: 16) - $0.textColor = UIColor.appColor(.new500) - } - - private let subtitleLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardMedium, size: 12) - $0.textColor = UIColor.appColor(.neutral600) - } - - private let rightImageView = UIImageView().then { - $0.contentMode = .scaleAspectFit - $0.image = UIImage.appImage(asset: .chevronRight)?.withRenderingMode(.alwaysTemplate) - $0.tintColor = UIColor.appColor(.new500) - } - - // MARK: - Initializers - override init(frame: CGRect) { - super.init(frame: frame) - configureView() - setAddTarget() - - containerView.isUserInteractionEnabled = false - lottieView.isUserInteractionEnabled = false - labelStackView.isUserInteractionEnabled = false - rightImageView.isUserInteractionEnabled = false - - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configureView() - setAddTarget() - } - - private func setAddTarget() { - addTarget(self, action: #selector(touchDown), for: .touchDown) - addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel]) - } - - // MARK: - Lottie Animation Methods - func setLottieAnimation(named name: String, bundle: Bundle = .main) { - lottieView.animation = LottieAnimation.named(name, bundle: bundle) - } - - func playLottieAnimation() { - lottieView.play() - } - - func stopLottieAnimation() { - lottieView.stop() - } -} - -// MARK: - @objc -extension OrderFloatingButton { - @objc private func touchDown() { - UIView.animate(withDuration: 0.1) { - self.containerView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) - self.containerView.alpha = 0.8 - } - } - - @objc private func touchUp() { - UIView.animate(withDuration: 0.1) { - self.containerView.transform = .identity - self.containerView.alpha = 1.0 - } - } -} - -// MARK: - UI Function -extension OrderFloatingButton { - private func setUpLayout() { - addSubview(containerView) - - containerView.addSubview(lottieView) - containerView.addSubview(labelStackView) - containerView.addSubview(rightImageView) - - labelStackView.addArrangedSubview(titleLabel) - labelStackView.addArrangedSubview(subtitleLabel) - } - - private func setUpConstraints() { - containerView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.height.equalTo(64) - } - - lottieView.snp.makeConstraints { - $0.leading.equalToSuperview().offset(6.5) - $0.centerY.equalToSuperview() - $0.width.equalTo(60) - $0.height.equalTo(57) - } - - labelStackView.snp.makeConstraints { - $0.leading.equalTo(lottieView.snp.trailing).offset(12) - $0.centerY.equalToSuperview() - $0.trailing.lessThanOrEqualTo(rightImageView.snp.leading).offset(-12) - } - - rightImageView.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(14.5) - $0.centerY.equalToSuperview() - $0.width.equalTo(24) - $0.height.equalTo(12) - } - } - - private func configureView() { - setUpLayout() - setUpConstraints() - } -} - -extension OrderFloatingButton { - var lottieAnimationView: LottieAnimationView { - return lottieView - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index c01a4cdd..551541d0 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -1007,7 +1007,6 @@ D8B1A9EC2E321E7800943535 /* ShopSortType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1A9EB2E321E7800943535 /* ShopSortType.swift */; }; D8B4E0022DBFAB95001FBC89 /* CheckVerificationCodeUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B4E0012DBFAB95001FBC89 /* CheckVerificationCodeUsecase.swift */; }; D8B4E0042DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B4E0032DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift */; }; - D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */; }; D8F5C54C2E6F012500FB6708 /* LottieAnimationManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */; }; D8F5C5562E6F144300FB6708 /* floatingLogo.json in Resources */ = {isa = PBXBuildFile; fileRef = D8F5C5532E6F144300FB6708 /* floatingLogo.json */; }; F1A000010000000000000001 /* ZoomingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A000010000000000000002 /* ZoomingCollectionView.swift */; }; @@ -2036,7 +2035,6 @@ D8B1A9EB2E321E7800943535 /* ShopSortType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSortType.swift; sourceTree = ""; }; D8B4E0012DBFAB95001FBC89 /* CheckVerificationCodeUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckVerificationCodeUsecase.swift; sourceTree = ""; }; D8B4E0032DBFAD1A001FBC89 /* CheckVerificationCodeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckVerificationCodeRequest.swift; sourceTree = ""; }; - D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderFloatingButton.swift; sourceTree = ""; }; D8F5C54B2E6F012500FB6708 /* LottieAnimationManageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationManageable.swift; sourceTree = ""; }; D8F5C5532E6F144300FB6708 /* floatingLogo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = floatingLogo.json; sourceTree = ""; }; EC692285302700A400EE26ED /* koinUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = koinUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4697,7 +4695,6 @@ B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */, B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */, B608F0E42EB5DC2C0006F355 /* OrderHistoryCustomSearchBar.swift */, - D8F5C5492E6EF76800FB6708 /* OrderFloatingButton.swift */, ); path = OrderHistoryUIComponents; sourceTree = ""; @@ -5865,7 +5862,6 @@ D215725C2CEDD04C0061E725 /* LectureRequest.swift in Sources */, D29F21F62C4FD2C700994554 /* FetchLandDetailRequest.swift in Sources */, D208A7A02CA10F1A007040E7 /* AbTestRepository.swift in Sources */, - D8F5C54A2E6EF76800FB6708 /* OrderFloatingButton.swift in Sources */, D2B2193B2D66626900EAF5B1 /* LostItemChatHistoryData.swift in Sources */, 83DD0DBB2C551E5100278ABD /* BusTimetableInfo.swift in Sources */, B47839052E70FAFE00D002E3 /* OrderShopSummary.swift in Sources */, From 4ad629bf296003082e2cde9de730e189684030cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sun, 6 Sep 2026 17:05:19 +0900 Subject: [PATCH 087/101] =?UTF-8?q?delete:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EA=B2=80=EC=83=89=20=EB=B0=94=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrderHistoryCustomSearchBar.swift | 143 ------------------ koin.xcodeproj/project.pbxproj | 4 - 2 files changed, 147 deletions(-) delete mode 100644 Koin/Core/View/OrderHistoryUIComponents/OrderHistoryCustomSearchBar.swift diff --git a/Koin/Core/View/OrderHistoryUIComponents/OrderHistoryCustomSearchBar.swift b/Koin/Core/View/OrderHistoryUIComponents/OrderHistoryCustomSearchBar.swift deleted file mode 100644 index 743a464c..00000000 --- a/Koin/Core/View/OrderHistoryUIComponents/OrderHistoryCustomSearchBar.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// OrderHistoryCustomSearchBar.swift -// koin -// -// Created by 김성민 on 9/8/25. -// - -import UIKit -import SnapKit - -final class OrderHistoryCustomSearchBar: UIView { - - //MARK: - CallBack - var onTextChanged: ((String) -> Void)? - var onReturn: ((String) -> Void)? - - // MARK: - UI - private let iconView = UIImageView().then { - $0.image = UIImage.appImage(asset: .search)?.withRenderingMode(.alwaysTemplate) - $0.tintColor = UIColor.appColor(.neutral500) - $0.contentMode = .scaleAspectFit - } - - let textField = UITextField().then { - - $0.clearButtonMode = .whileEditing - $0.returnKeyType = .search - $0.autocapitalizationType = .none - $0.clearButtonMode = .never - $0.autocorrectionType = .no - $0.spellCheckingType = .no - $0.font = UIFont.appFont(.pretendardRegular, size: 14) - $0.textColor = .label - $0.attributedPlaceholder = NSAttributedString( - string: "주문한 메뉴/매장을 찾아보세요", - attributes: [ - .foregroundColor: UIColor.appColor(.neutral400), - .font: UIFont.appFont(.pretendardRegular, size: 14) - ] - ) - } - - // MARK: - Layout Config - var contentInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) { didSet { remakeConstraints() } } - var spacing: CGFloat = 8 { didSet { remakeConstraints() } } - var iconSize: CGFloat = 18 { didSet { iconView.snp.updateConstraints { $0.size.equalTo(iconSize) } } } - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupActions() - setDelegate() - makeConstraints() - } - required init?(coder: NSCoder) { fatalError() } - - func setPlaceholder(_ text: String) { - textField.attributedPlaceholder = NSAttributedString( - string: text, - attributes: [ - .foregroundColor: UIColor.appColor(.neutral400), - .font: UIFont.appFont(.pretendardRegular, size: 14) - ] - ) - } - - func setLeftIcon(_ image: UIImage?, tint: UIColor? = nil) { - iconView.image = image?.withRenderingMode(.alwaysTemplate) - if let tint { iconView.tintColor = tint } - } - - @discardableResult - func focus() -> Bool { textField.becomeFirstResponder() } - func unfocus() { textField.resignFirstResponder() } - - // MARK: - Private - private func setupUI() { - backgroundColor = UIColor.appColor(.neutral0) - layer.cornerRadius = 16 - layer.masksToBounds = false - - layer.shadowColor = UIColor.black.cgColor - layer.shadowOffset = CGSize(width: 0, height: 2) - layer.shadowRadius = 4 - layer.shadowOpacity = 0.06 - - addSubview(iconView) - addSubview(textField) - } - - private func setupActions() { - textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged) - } - - private func setDelegate(){ - textField.delegate = self - } - - private func makeConstraints() { - iconView.snp.makeConstraints { - $0.size.equalTo(iconSize) - $0.leading.equalToSuperview().inset(contentInsets.left) - $0.centerY.equalToSuperview() - } - textField.snp.makeConstraints { - $0.leading.equalTo(iconView.snp.trailing).offset(spacing) - $0.top.equalToSuperview().inset(contentInsets.top) - $0.bottom.equalToSuperview().inset(contentInsets.bottom) - $0.trailing.equalToSuperview().inset(contentInsets.right) - $0.height.greaterThanOrEqualTo(28) - } - } - - private func remakeConstraints() { - iconView.snp.remakeConstraints { - $0.size.equalTo(iconSize) - $0.leading.equalToSuperview().inset(contentInsets.left) - $0.centerY.equalToSuperview() - } - textField.snp.remakeConstraints { - $0.leading.equalTo(iconView.snp.trailing).offset(spacing) - $0.top.equalToSuperview().inset(contentInsets.top) - $0.bottom.equalToSuperview().inset(contentInsets.bottom) - $0.trailing.equalToSuperview().inset(contentInsets.right) - $0.height.greaterThanOrEqualTo(28) - } - layoutIfNeeded() - } - - @objc private func textDidChange() { - onTextChanged?(textField.text ?? "") - } -} - -extension OrderHistoryCustomSearchBar: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - onReturn?(textField.text ?? "") - return true - } -} - - - diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 551541d0..49330408 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -757,7 +757,6 @@ B47839052E70FAFE00D002E3 /* OrderShopSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839042E70FAFE00D002E3 /* OrderShopSummary.swift */; }; B47839072E70FF2B00D002E3 /* OrderShopMenusGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839062E70FF2B00D002E3 /* OrderShopMenusGroups.swift */; }; B4B3EB262E6E7CAD00F8A23A /* OrderShopMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */; }; - B608F0E62EB5DC2D0006F355 /* OrderHistoryCustomSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E42EB5DC2C0006F355 /* OrderHistoryCustomSearchBar.swift */; }; B608F0E72EB5DC2D0006F355 /* FilteringButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */; }; B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */; }; B60D25462ED72017000E57B7 /* RadioButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25442ED72017000E57B7 /* RadioButtonState.swift */; }; @@ -1783,7 +1782,6 @@ B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopMenus.swift; sourceTree = ""; }; B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilteringButton.swift; sourceTree = ""; }; - B608F0E42EB5DC2C0006F355 /* OrderHistoryCustomSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderHistoryCustomSearchBar.swift; sourceTree = ""; }; B60D25412ED72017000E57B7 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; B60D25422ED72017000E57B7 /* RadioButtonColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonColors.swift; sourceTree = ""; }; B60D25432ED72017000E57B7 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; @@ -4694,7 +4692,6 @@ children = ( B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */, B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */, - B608F0E42EB5DC2C0006F355 /* OrderHistoryCustomSearchBar.swift */, ); path = OrderHistoryUIComponents; sourceTree = ""; @@ -6015,7 +6012,6 @@ 833D9C392C7058C300982145 /* NoticeListRepository.swift in Sources */, D27DD0E22BA609740081FD36 /* MenuDto.swift in Sources */, D27A5D9C2C6BE56500C4275F /* FetchShopListRequest.swift in Sources */, - B608F0E62EB5DC2D0006F355 /* OrderHistoryCustomSearchBar.swift in Sources */, B608F0E72EB5DC2D0006F355 /* FilteringButton.swift in Sources */, B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */, D29F21C32C4DE27C00994554 /* UserService.swift in Sources */, From fb4fd61ddcd49bf9a4ab43b4738db1e82cb527e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sun, 6 Sep 2026 17:06:04 +0900 Subject: [PATCH 088/101] =?UTF-8?q?delete:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20Sheet=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilteringButton.swift | 103 ------------------ koin.xcodeproj/project.pbxproj | 4 - 2 files changed, 107 deletions(-) delete mode 100644 Koin/Core/View/OrderHistoryUIComponents/FilteringButton.swift diff --git a/Koin/Core/View/OrderHistoryUIComponents/FilteringButton.swift b/Koin/Core/View/OrderHistoryUIComponents/FilteringButton.swift deleted file mode 100644 index 018b3c5a..00000000 --- a/Koin/Core/View/OrderHistoryUIComponents/FilteringButton.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// FilteringButton.swift -// koin -// -// Created by 김성민 on 9/6/25. -// - -import UIKit - -final class FilteringButton: UIButton { - - private var isSelectable: Bool = true - private var forcedOn: Bool? = nil - - override init(frame: CGRect) { - super.init(frame: frame) - titleLabel?.numberOfLines = 1 - - var config = UIButton.Configuration.plain() - config.imagePlacement = .trailing - config.imagePadding = 6 - config.contentInsets = .init(top: 6, leading: 8, bottom: 6, trailing: 8) - config.attributedTitle = AttributedString("필터", attributes: .init([ - .font: UIFont.appFont(.pretendardBold, size: 14) - ])) - - var background = UIBackgroundConfiguration.clear() - background.backgroundColor = UIColor.appColor(.neutral0) - background.cornerRadius = 24 - background.strokeWidth = 0.5 - background.strokeColor = UIColor.appColor(.neutral300) - config.background = background - - config.image = UIImage.appImage(asset: .chevronDown) - config.baseForegroundColor = UIColor.appColor(.neutral500) - - self.configuration = config - - self.configurationUpdateHandler = { [weak self] button in - guard let self = self, var config = button.configuration else { return } - var background = config.background - let on = self.forcedOn ?? button.isSelected - if on { - background.backgroundColor = UIColor.appColor(.new500) - background.strokeWidth = 0 - background.strokeColor = nil - config.baseForegroundColor = UIColor.appColor(.neutral0) - } else { - background.backgroundColor = UIColor.appColor(.neutral0) - background.strokeWidth = 0.5 - background.strokeColor = UIColor.appColor(.neutral300) - config.baseForegroundColor = UIColor.appColor(.neutral500) - } - config.background = background - button.configuration = config - } - - addTarget(self, action: #selector(handleTap), for: .touchUpInside) - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - func setTitle(_ text: String) { - guard var config = configuration else { return } - config.attributedTitle = AttributedString(text, attributes: .init([ - .font: UIFont.appFont(.pretendardBold, size: 14) - ])) - configuration = config - } - - func set(title: String, iconRight: UIImage? = nil, showsChevron: Bool = false) { - guard var config = configuration else { return } - config.attributedTitle = AttributedString(title, attributes: .init([ - .font: UIFont.appFont(.pretendardBold, size: 14) - ])) - if showsChevron { - config.image = UIImage.appImage(asset: .chevronDown) - } else { - config.image = iconRight?.withRenderingMode(.alwaysTemplate) - } - config.imagePlacement = .trailing - config.imagePadding = (config.image == nil) ? 0 : 6 - configuration = config - setNeedsUpdateConfiguration() - } - - func applyFilter(_ on: Bool) { - forcedOn = on - setNeedsUpdateConfiguration() - } - - func setSelectable(_ selectable: Bool) { - isSelectable = selectable - } - - @objc private func handleTap() { - if isSelectable { - isSelected.toggle() - setNeedsUpdateConfiguration() - } - } -} - diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 49330408..c9f5ad6e 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -757,7 +757,6 @@ B47839052E70FAFE00D002E3 /* OrderShopSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839042E70FAFE00D002E3 /* OrderShopSummary.swift */; }; B47839072E70FF2B00D002E3 /* OrderShopMenusGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839062E70FF2B00D002E3 /* OrderShopMenusGroups.swift */; }; B4B3EB262E6E7CAD00F8A23A /* OrderShopMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */; }; - B608F0E72EB5DC2D0006F355 /* FilteringButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */; }; B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */; }; B60D25462ED72017000E57B7 /* RadioButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25442ED72017000E57B7 /* RadioButtonState.swift */; }; B60D25472ED72017000E57B7 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25432ED72017000E57B7 /* RadioButtonGroup.swift */; }; @@ -1781,7 +1780,6 @@ B47839062E70FF2B00D002E3 /* OrderShopMenusGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopMenusGroups.swift; sourceTree = ""; }; B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopMenus.swift; sourceTree = ""; }; B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; - B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilteringButton.swift; sourceTree = ""; }; B60D25412ED72017000E57B7 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; B60D25422ED72017000E57B7 /* RadioButtonColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonColors.swift; sourceTree = ""; }; B60D25432ED72017000E57B7 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; @@ -4691,7 +4689,6 @@ isa = PBXGroup; children = ( B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */, - B608F0E32EB5DC2C0006F355 /* FilteringButton.swift */, ); path = OrderHistoryUIComponents; sourceTree = ""; @@ -6012,7 +6009,6 @@ 833D9C392C7058C300982145 /* NoticeListRepository.swift in Sources */, D27DD0E22BA609740081FD36 /* MenuDto.swift in Sources */, D27A5D9C2C6BE56500C4275F /* FetchShopListRequest.swift in Sources */, - B608F0E72EB5DC2D0006F355 /* FilteringButton.swift in Sources */, B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */, D29F21C32C4DE27C00994554 /* UserService.swift in Sources */, D837FB122E254BD8002BFB9F /* CustomSessionManager.swift in Sources */, From 338cdfaaff182f5d9dff4fa933b2d1c7eb1a760a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sun, 6 Sep 2026 17:06:33 +0900 Subject: [PATCH 089/101] =?UTF-8?q?delete:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20Empty=20View=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EmptyStateView.swift | 133 ------------------ koin.xcodeproj/project.pbxproj | 12 -- 2 files changed, 145 deletions(-) delete mode 100644 Koin/Core/View/OrderHistoryUIComponents/EmptyStateView.swift diff --git a/Koin/Core/View/OrderHistoryUIComponents/EmptyStateView.swift b/Koin/Core/View/OrderHistoryUIComponents/EmptyStateView.swift deleted file mode 100644 index 94db407b..00000000 --- a/Koin/Core/View/OrderHistoryUIComponents/EmptyStateView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// EmptyStateView.swift -// koin -// -// Created by 김성민 on 9/19/25. -// - -import UIKit -import SnapKit - -final class EmptyStateView: UIView { - - var onTapAction: (() -> Void)? - private var symbolCenterY: Constraint? - - struct Config{ - let title: String - let showSeeOrderHistoryButton: Bool - } - - private let centerGuide = UILayoutGuide() - - private let symbolImageView = UIImageView().then { - $0.image = UIImage.appImage(asset: .sleepBcsdSymbol) - } - - private let noOrderHistoryLabel = UILabel().then { - $0.text = "주문 내역이 없어요" - $0.font = UIFont.appFont(.pretendardBold, size: 18) - $0.textColor = UIColor.appColor(.new500) - $0.textAlignment = .center - } - - private let seeOrderHistoryButton = UIButton( - configuration: { - var config = UIButton.Configuration.plain() - config.attributedTitle = AttributedString("과거 주문 내역 보기", attributes: .init([ - .font: UIFont.appFont(.pretendardBold, size: 13) - ])) - config.baseForegroundColor = UIColor.appColor(.neutral500) - - var background = UIBackgroundConfiguration.clear() - background.cornerRadius = 8 - background.backgroundColor = UIColor.appColor(.neutral0) - config.background = background - - config.contentInsets = NSDirectionalEdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7) - return config - }() - ).then { - $0.layer.masksToBounds = false - $0.layer.shadowColor = UIColor.black.cgColor - $0.layer.shadowOpacity = 0.2 - $0.layer.shadowOffset = CGSize(width: 0, height: 2) - $0.layer.shadowRadius = 4 - } - - override init(frame: CGRect){ - super.init(frame: frame) - configureView() - setAddTarget() - isHidden = true - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} - - -// MARK: - Set UI - -extension EmptyStateView { - - private func configureView() { - backgroundColor = UIColor.appColor(.newBackground) - setLayout() - } - - private func setLayout(){ - [symbolImageView, noOrderHistoryLabel, seeOrderHistoryButton].forEach{ - addSubview($0) - } - - addLayoutGuide(centerGuide) - } - - override func didMoveToSuperview() { - super.didMoveToSuperview() - guard let host = superview else { return } - - symbolImageView.snp.remakeConstraints { - $0.centerX.equalTo(host.snp.centerX) - $0.centerY.equalTo(host.snp.centerY) - $0.width.equalTo(95) - $0.height.equalTo(75) - } - - noOrderHistoryLabel.snp.remakeConstraints { - $0.top.equalTo(symbolImageView.snp.bottom).offset(16) - $0.centerX.equalTo(host.snp.centerX) - } - - seeOrderHistoryButton.snp.remakeConstraints { - $0.top.equalTo(noOrderHistoryLabel.snp.bottom).offset(16) - $0.centerX.equalTo(host.snp.centerX) - $0.height.equalTo(35) - } - } - - - - private func setAddTarget(){ - seeOrderHistoryButton.addTarget(self, action: #selector(seeOrderHistoryButtonTapped), for: .touchUpInside) - } - - func apply(_ config: Config){ - noOrderHistoryLabel.text = config.title - seeOrderHistoryButton.isHidden = !config.showSeeOrderHistoryButton - layoutIfNeeded() - } - - - //MARK: - @Objc - @objc private func seeOrderHistoryButtonTapped() { - onTapAction?() - } - - - -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index c9f5ad6e..a5da4ca4 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -757,7 +757,6 @@ B47839052E70FAFE00D002E3 /* OrderShopSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839042E70FAFE00D002E3 /* OrderShopSummary.swift */; }; B47839072E70FF2B00D002E3 /* OrderShopMenusGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47839062E70FF2B00D002E3 /* OrderShopMenusGroups.swift */; }; B4B3EB262E6E7CAD00F8A23A /* OrderShopMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */; }; - B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */; }; B60D25462ED72017000E57B7 /* RadioButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25442ED72017000E57B7 /* RadioButtonState.swift */; }; B60D25472ED72017000E57B7 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25432ED72017000E57B7 /* RadioButtonGroup.swift */; }; B60D25482ED72017000E57B7 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60D25412ED72017000E57B7 /* RadioButton.swift */; }; @@ -1779,7 +1778,6 @@ B47839042E70FAFE00D002E3 /* OrderShopSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopSummary.swift; sourceTree = ""; }; B47839062E70FF2B00D002E3 /* OrderShopMenusGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopMenusGroups.swift; sourceTree = ""; }; B4B3EB252E6E7CAD00F8A23A /* OrderShopMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderShopMenus.swift; sourceTree = ""; }; - B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; B60D25412ED72017000E57B7 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; B60D25422ED72017000E57B7 /* RadioButtonColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonColors.swift; sourceTree = ""; }; B60D25432ED72017000E57B7 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; @@ -4685,14 +4683,6 @@ path = Fonts; sourceTree = ""; }; - B608F0E52EB5DC2C0006F355 /* OrderHistoryUIComponents */ = { - isa = PBXGroup; - children = ( - B608F0E22EB5DC2C0006F355 /* EmptyStateView.swift */, - ); - path = OrderHistoryUIComponents; - sourceTree = ""; - }; B60D25452ED72017000E57B7 /* RadioButton */ = { isa = PBXGroup; children = ( @@ -5316,7 +5306,6 @@ D89F8FDA2DF12847000265DC /* StatefulButton.swift */, D891F25E2E45DC9D006D1F41 /* TrackPaddedSlider.swift */, 7C61D3502F22B344005FAC3A /* ExtendedTouchAreaView.swift */, - B608F0E52EB5DC2C0006F355 /* OrderHistoryUIComponents */, 7C65FDCA2FE69A00007831CE /* AutoScrollableInfiniteCarouselCollectionView */, ); path = View; @@ -6009,7 +5998,6 @@ 833D9C392C7058C300982145 /* NoticeListRepository.swift in Sources */, D27DD0E22BA609740081FD36 /* MenuDto.swift in Sources */, D27A5D9C2C6BE56500C4275F /* FetchShopListRequest.swift in Sources */, - B608F0E82EB5DC2D0006F355 /* EmptyStateView.swift in Sources */, D29F21C32C4DE27C00994554 /* UserService.swift in Sources */, D837FB122E254BD8002BFB9F /* CustomSessionManager.swift in Sources */, D2DCB2922CE5D639005CABEE /* PostCallNotificationUseCase.swift in Sources */, From 6658b89027ebccedd7e99f3b1534abfd2e2fccd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=A7=80?= Date: Sun, 6 Sep 2026 17:07:22 +0900 Subject: [PATCH 090/101] =?UTF-8?q?delete:=20=EC=A3=BC=EB=AC=B8=20Slider?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/View/TrackPaddedSlider.swift | 26 -------------------------- koin.xcodeproj/project.pbxproj | 4 ---- 2 files changed, 30 deletions(-) delete mode 100644 Koin/Core/View/TrackPaddedSlider.swift diff --git a/Koin/Core/View/TrackPaddedSlider.swift b/Koin/Core/View/TrackPaddedSlider.swift deleted file mode 100644 index fcfb45de..00000000 --- a/Koin/Core/View/TrackPaddedSlider.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// TrackPaddedSlider.swift -// koin -// -// Created by 이은지 on 8/8/25. -// - -import UIKit - -final class TrackPaddedSlider: UISlider { - - public var horizontalPadding: CGFloat = 0 - - override func trackRect(forBounds bounds: CGRect) -> CGRect { - let originalRect = super.trackRect(forBounds: bounds) - - let newRect = CGRect( - x: originalRect.origin.x + horizontalPadding, - y: originalRect.origin.y, - width: originalRect.size.width - (horizontalPadding * 2), - height: originalRect.size.height - ) - - return newRect - } -} diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index a5da4ca4..ec9ae71a 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -995,7 +995,6 @@ D874DB4D2DBF1A6C0098EED0 /* SendVerificationCodeUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D874DB4C2DBF1A6B0098EED0 /* SendVerificationCodeUsecase.swift */; }; D874DB4F2DBF1FEE0098EED0 /* SendVerificationCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D874DB4E2DBF1FEE0098EED0 /* SendVerificationCodeRequest.swift */; }; D874DB512DBF200C0098EED0 /* SendVerificationCodeDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D874DB502DBF200C0098EED0 /* SendVerificationCodeDto.swift */; }; - D891F25F2E45DC9D006D1F41 /* TrackPaddedSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D891F25E2E45DC9D006D1F41 /* TrackPaddedSlider.swift */; }; D89BAA392DCA5B3D006D3BB7 /* GeneralRegisterFormRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89BAA382DCA5B3D006D3BB7 /* GeneralRegisterFormRequest.swift */; }; D89BAA3B2DCA5B47006D3BB7 /* StudentRegisterFormRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89BAA3A2DCA5B47006D3BB7 /* StudentRegisterFormRequest.swift */; }; D89BAA3D2DCA6483006D3BB7 /* RegisterFormUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89BAA3C2DCA6483006D3BB7 /* RegisterFormUseCase.swift */; }; @@ -2020,7 +2019,6 @@ D874DB4C2DBF1A6B0098EED0 /* SendVerificationCodeUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendVerificationCodeUsecase.swift; sourceTree = ""; }; D874DB4E2DBF1FEE0098EED0 /* SendVerificationCodeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendVerificationCodeRequest.swift; sourceTree = ""; }; D874DB502DBF200C0098EED0 /* SendVerificationCodeDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendVerificationCodeDto.swift; sourceTree = ""; }; - D891F25E2E45DC9D006D1F41 /* TrackPaddedSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackPaddedSlider.swift; sourceTree = ""; }; D89BAA382DCA5B3D006D3BB7 /* GeneralRegisterFormRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralRegisterFormRequest.swift; sourceTree = ""; }; D89BAA3A2DCA5B47006D3BB7 /* StudentRegisterFormRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudentRegisterFormRequest.swift; sourceTree = ""; }; D89BAA3C2DCA6483006D3BB7 /* RegisterFormUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFormUseCase.swift; sourceTree = ""; }; @@ -5304,7 +5302,6 @@ D289873D2D21B30D00AA7285 /* DebouncedButton.swift */, D89F8FD82DF0A8A4000265DC /* DefaultTextField.swift */, D89F8FDA2DF12847000265DC /* StatefulButton.swift */, - D891F25E2E45DC9D006D1F41 /* TrackPaddedSlider.swift */, 7C61D3502F22B344005FAC3A /* ExtendedTouchAreaView.swift */, 7C65FDCA2FE69A00007831CE /* AutoScrollableInfiniteCarouselCollectionView */, ); @@ -6667,7 +6664,6 @@ D2FB236C2C0409900098BA2B /* LogAnalyticsEventUseCase.swift in Sources */, B4B3EB262E6E7CAD00F8A23A /* OrderShopMenus.swift in Sources */, 7C17EEB02EBF708B008BCA89 /* ShopSearchDto.swift in Sources */, - D891F25F2E45DC9D006D1F41 /* TrackPaddedSlider.swift in Sources */, D21572522CEDC1720061E725 /* FetchFrameUseCase.swift in Sources */, D2908D282BB8D1580008F908 /* LectureDto.swift in Sources */, D2908D2C2BB9D4150008F908 /* TimetablesDto.swift in Sources */, From 90f9aef3d75db31857dc895b904bceadbe730951 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 6 Sep 2026 19:39:42 +0900 Subject: [PATCH 091/101] =?UTF-8?q?design:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Login/Login/LoginViewController.swift | 246 +++++++++++++----- 1 file changed, 182 insertions(+), 64 deletions(-) diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index 1e1bbb83..f0288ed8 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -8,23 +8,46 @@ import Combine import SafariServices import UIKit +import SnapKit final class LoginViewController: UIViewController { - - var completion: (() -> Void)? + // MARK: - Properties private let viewModel: LoginViewModel private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] // MARK: - UI Components + private let scrollView = UIScrollView() + + private let contentView = UIView() + + private let contentTopPaddingLayoutGuide = UILayoutGuide() + private let contentLayoutGuide = UILayoutGuide() + private let contentBottomPaddingLayoutGuide = UILayoutGuide() + private let logoImageView = UIImageView().then { - $0.image = UIImage.appImage(asset: .koinLogo) + $0.image = UIImage.appImage(asset: .bcsdSymbolLogo) + $0.contentMode = .scaleAspectFit + } + + private let logoTextImageView = UIImageView().then { + $0.image = UIImage.appImage(asset: .koinTextLogo) + $0.contentMode = .scaleAspectFit } private let idTextField = UITextField().then { - $0.placeholder = "아이디(Koreatech ID/전화번호)" + $0.attributedPlaceholder = NSAttributedString( + string: "아이디(Koreatech ID/전화번호)", + attributes: [ + .foregroundColor: UIColor.appColor(.neutral400), + .font: UIFont.appFont(.pretendardRegular, size: 16) + ] + ) $0.autocapitalizationType = .none + $0.autocorrectionType = .no + $0.textContentType = .username + $0.textColor = UIColor.appColor(.neutral800) $0.font = UIFont.appFont(.pretendardRegular, size: 16) } @@ -33,18 +56,27 @@ final class LoginViewController: UIViewController { } private let idWarningLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 13) - $0.textColor = UIColor.appColor(.sub500) + $0.font = UIFont.appFont(.pretendardRegular, size: 12) + $0.textColor = UIColor.appColor(.new800) } private let passwordTextField = UITextField().then { - $0.placeholder = "비밀번호" + $0.attributedPlaceholder = NSAttributedString( + string: "비밀번호", + attributes: [ + .foregroundColor: UIColor.appColor(.neutral400), + .font: UIFont.appFont(.pretendardRegular, size: 16) + ] + ) + $0.textContentType = .password + $0.textColor = UIColor.appColor(.neutral800) $0.font = UIFont.appFont(.pretendardRegular, size: 16) $0.isSecureTextEntry = true } private let changeSecureButton = UIButton().then { button in button.setImage(UIImage.appImage(asset: .visibility), for: .normal) + button.accessibilityLabel = "비밀번호 보기" } private let separateView2 = UIView().then { @@ -52,18 +84,17 @@ final class LoginViewController: UIViewController { } private let passwordWarningLabel = UILabel().then { - $0.font = UIFont.appFont(.pretendardRegular, size: 13) - $0.numberOfLines = 2 - $0.textColor = UIColor.appColor(.sub500) + $0.font = UIFont.appFont(.pretendardRegular, size: 12) + $0.textColor = UIColor.appColor(.new800) } private let warningImageView = UIImageView().then { - $0.image = UIImage.appImage(asset: .warningOrange) + $0.image = UIImage.appImage(asset: .warningOrange)?.withRenderingMode(.alwaysTemplate).withTintColor(.appColor(.new800)) $0.isHidden = true } private let loginButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.sub500) + $0.backgroundColor = UIColor.appColor(.new500) $0.setTitle("로그인", for: .normal) $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 15) @@ -71,40 +102,58 @@ final class LoginViewController: UIViewController { } private let registerButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) + $0.backgroundColor = UIColor.appColor(.neutral0) $0.setTitle("회원가입", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) + $0.setTitleColor(UIColor.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 15) + $0.layer.borderColor = UIColor.appColor(.new500).cgColor + $0.layer.borderWidth = 1 $0.layer.cornerRadius = 8 } + private let findButtonsLayoutGuide = UILayoutGuide() + private let findIdButton = UIButton().then { var configuration = UIButton.Configuration.plain() configuration.image = UIImage.appImage(asset: .findId) var text = AttributedString("아이디 찾기") - text.font = UIFont.appFont(.pretendardRegular, size: 13) + text.font = UIFont.appFont(.pretendardRegular, size: 12) configuration.attributedTitle = text - configuration.imagePadding = 1 + configuration.imagePadding = 4 + configuration.contentInsets = .zero configuration.baseForegroundColor = UIColor.appColor(.neutral500) - $0.backgroundColor = .clear $0.configuration = configuration } + private let findSeparatorLabel = UILabel().then { + $0.text = "|" + $0.textColor = .appColor(.neutral500) + $0.font = .appFont(.pretendardRegular, size: 15) + } + private let findPasswordButton = UIButton().then { var configuration = UIButton.Configuration.plain() configuration.image = UIImage.appImage(asset: .findPassword) var text = AttributedString("비밀번호 찾기") - text.font = UIFont.appFont(.pretendardRegular, size: 13) + text.font = UIFont.appFont(.pretendardRegular, size: 12) configuration.attributedTitle = text - configuration.imagePadding = 1 + configuration.imagePadding = 4 + configuration.contentInsets = .zero configuration.baseForegroundColor = UIColor.appColor(.neutral500) - $0.backgroundColor = .clear $0.configuration = configuration } + private let footerLayoutGuide = UILayoutGuide() + + private let ownerButton = UIButton().then { + $0.setTitle("사장님이신가요?", for: .normal) + $0.setTitleColor(UIColor.appColor(.new500), for: .normal) + $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 18) + } + private let copyrightLabel = UILabel().then { $0.text = "Copyright @ BCSD Lab All rights reserved." - $0.textColor = UIColor.appColor(.neutral700) + $0.textColor = UIColor.appColor(.neutral600) $0.font = UIFont.appFont(.pretendardRegular, size: 12) $0.textAlignment = .center } @@ -123,7 +172,7 @@ final class LoginViewController: UIViewController { // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = "로그인" + title = "로그인" configureView() bind() hideKeyboardWhenTappedAround() @@ -132,13 +181,20 @@ final class LoginViewController: UIViewController { registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside) findIdButton.addTarget(self, action: #selector(findIdButtonTapped), for: .touchUpInside) findPasswordButton.addTarget(self, action: #selector(findPasswordButtonTapped), for: .touchUpInside) + ownerButton.addTarget(self, action: #selector(ownerButtonTapped), for: .touchUpInside) idTextField.delegate = self passwordTextField.delegate = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - configureNavigationBar(style: .fill) + configureNavigationBar(style: .empty) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + configureNavigationBar(style: .empty + ) } private func bind() { @@ -152,7 +208,6 @@ final class LoginViewController: UIViewController { case .loginSuccess: self?.navigationController?.popViewController(animated: true) self?.inputSubject.send(.logEvent(EventParameter.EventLabel.User.login, .click, "로그인 완료")) - self?.completion?() case .showForceModal: self?.navigationController?.setViewControllers([ForceModifyUserViewController()], animated: true) case .showModifyModal: @@ -209,6 +264,7 @@ extension LoginViewController { @objc private func changeSecureButtonTapped() { passwordTextField.isSecureTextEntry.toggle() changeSecureButton.setImage(passwordTextField.isSecureTextEntry ? UIImage.appImage(asset: .visibility) : UIImage.appImage(asset: .visibilityNon), for: .normal) + changeSecureButton.accessibilityLabel = passwordTextField.isSecureTextEntry ? "비밀번호 보기" : "비밀번호 숨기기" } @objc private func findIdButtonTapped() { @@ -222,6 +278,11 @@ extension LoginViewController { navigationController?.pushViewController(findPasswordViewController, animated: true) inputSubject.send(.logEvent(EventParameter.EventLabel.User.login, .click, "비밀번호 찾기")) } + + @objc private func ownerButtonTapped() { + guard let url = URL(string: "https://owner.koreatech.in") else { return } + present(SFSafariViewController(url: url), animated: true) + } @objc func loginButtonTapped() { warningImageView.isHidden = true @@ -272,27 +333,57 @@ extension LoginViewController { extension LoginViewController { private func setUpLayOuts() { - [logoImageView, idTextField, separateView1, idWarningLabel, passwordTextField, changeSecureButton, separateView2, warningImageView, passwordWarningLabel, loginButton, registerButton, findIdButton, findPasswordButton, copyrightLabel].forEach { + [logoImageView, logoTextImageView, + idTextField, separateView1, passwordTextField, changeSecureButton, separateView2, + warningImageView, idWarningLabel, passwordWarningLabel, + loginButton, registerButton, + findIdButton, findSeparatorLabel, findPasswordButton, + ownerButton, copyrightLabel].forEach { + contentView.addSubview($0) + } + + [contentTopPaddingLayoutGuide, contentLayoutGuide, contentBottomPaddingLayoutGuide, findButtonsLayoutGuide, footerLayoutGuide].forEach { + contentView.addLayoutGuide($0) + } + + [contentView].forEach { + scrollView.addSubview($0) + } + + [scrollView].forEach { view.addSubview($0) } } + private func setUpConstraints() { - logoImageView.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(110) - make.leading.equalTo(view.snp.leading).offset(40) - make.height.equalTo(60) - make.width.equalTo(107) + scrollView.snp.makeConstraints { + $0.leading.trailing.top.equalToSuperview() + $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top) } - idTextField.snp.makeConstraints { make in - make.top.equalTo(logoImageView.snp.bottom).offset(32) - make.leading.equalTo(view.snp.leading).offset(48) - make.trailing.equalTo(view.snp.trailing).offset(-48) - make.height.equalTo(40) + contentView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalTo(scrollView) + $0.height.greaterThanOrEqualTo(view.safeAreaLayoutGuide) + } + + logoImageView.snp.makeConstraints { + $0.top.centerX.equalTo(contentLayoutGuide) + $0.height.equalTo(66) + } + logoTextImageView.snp.makeConstraints { + $0.top.equalTo(logoImageView.snp.bottom).offset(9) + $0.centerX.equalTo(contentLayoutGuide) + $0.width.equalTo(102) + $0.height.equalTo(41) + } + idTextField.snp.makeConstraints { + $0.top.equalTo(logoTextImageView.snp.bottom).offset(52) + $0.leading.trailing.equalTo(contentLayoutGuide).inset(48) + $0.height.equalTo(40) } separateView1.snp.makeConstraints { make in make.top.equalTo(idTextField.snp.bottom) - make.leading.equalTo(idTextField.snp.leading) - make.trailing.equalTo(idTextField.snp.trailing) + make.leading.trailing.equalTo(contentLayoutGuide).inset(48) make.height.equalTo(1) } idWarningLabel.snp.makeConstraints { make in @@ -301,20 +392,18 @@ extension LoginViewController { make.height.equalTo(20) } passwordTextField.snp.makeConstraints { make in - make.top.equalTo(idTextField.snp.bottom).offset(16) - make.leading.equalTo(idTextField.snp.leading) - make.trailing.equalTo(idTextField.snp.trailing) + make.top.equalTo(idTextField.snp.bottom).offset(24) + make.leading.trailing.equalTo(contentLayoutGuide).inset(48) make.height.equalTo(40) } changeSecureButton.snp.makeConstraints { make in make.centerY.equalTo(passwordTextField.snp.centerY) - make.trailing.equalTo(passwordTextField.snp.trailing) - make.width.height.equalTo(20) + make.trailing.equalTo(passwordTextField.snp.trailing).offset(12) + make.width.height.equalTo(44) } separateView2.snp.makeConstraints { make in make.top.equalTo(passwordTextField.snp.bottom) - make.leading.equalTo(passwordTextField.snp.leading) - make.trailing.equalTo(passwordTextField.snp.trailing) + make.leading.trailing.equalTo(contentLayoutGuide).inset(48) make.height.equalTo(1) } warningImageView.snp.makeConstraints { make in @@ -323,45 +412,74 @@ extension LoginViewController { make.width.height.equalTo(16) } passwordWarningLabel.snp.makeConstraints { make in - make.top.equalTo(separateView2.snp.bottom) + make.top.equalTo(separateView2.snp.bottom).offset(8) make.leading.equalTo(warningImageView.snp.trailing).offset(4) make.height.greaterThanOrEqualTo(20) } loginButton.snp.makeConstraints { make in make.top.equalTo(separateView2.snp.bottom).offset(48) - make.leading.equalTo(separateView2.snp.leading) - make.trailing.equalTo(separateView2.snp.trailing) + make.leading.trailing.equalTo(contentLayoutGuide).inset(48) make.height.equalTo(44) } registerButton.snp.makeConstraints { make in make.top.equalTo(loginButton.snp.bottom).offset(24) - make.leading.equalTo(loginButton.snp.leading) - make.trailing.equalTo(loginButton.snp.trailing) + make.leading.trailing.equalTo(contentLayoutGuide).inset(48) make.height.equalTo(44) } - findIdButton.snp.makeConstraints { make in - make.top.equalTo(registerButton.snp.bottom).offset(32) - make.trailing.equalTo(view.snp.centerX).offset(-5) - make.width.greaterThanOrEqualTo(84) - make.height.equalTo(20) + + findIdButton.snp.makeConstraints { + $0.leading.equalTo(findButtonsLayoutGuide) + $0.centerY.equalTo(findButtonsLayoutGuide) } - findPasswordButton.snp.makeConstraints { make in - make.top.equalTo(findIdButton.snp.top) - make.leading.equalTo(view.snp.centerX) - make.width.greaterThanOrEqualTo(100) - make.height.equalTo(20) + findSeparatorLabel.snp.makeConstraints { + $0.top.bottom.equalTo(findButtonsLayoutGuide) + $0.leading.equalTo(findIdButton.snp.trailing).offset(10) + } + findPasswordButton.snp.makeConstraints { + $0.leading.equalTo(findSeparatorLabel.snp.trailing).offset(10) + $0.trailing.equalTo(findButtonsLayoutGuide) + $0.centerY.equalTo(findButtonsLayoutGuide) + } + findButtonsLayoutGuide.snp.makeConstraints { + $0.centerX.bottom.equalTo(contentLayoutGuide) + $0.top.equalTo(registerButton.snp.bottom).offset(32) + $0.height.equalTo(22) + } + + copyrightLabel.snp.makeConstraints { + $0.bottom.centerX.equalTo(footerLayoutGuide) + $0.height.equalTo(12) + } + ownerButton.snp.makeConstraints { + $0.top.centerX.equalTo(footerLayoutGuide) + $0.bottom.equalTo(copyrightLabel.snp.top).offset(-21) + $0.height.equalTo(29) + } + + contentTopPaddingLayoutGuide.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.height.equalTo(contentBottomPaddingLayoutGuide.snp.height).multipliedBy(2.0/3.0) + } + contentLayoutGuide.snp.makeConstraints { + $0.top.equalTo(contentTopPaddingLayoutGuide.snp.bottom) + $0.bottom.equalTo(contentBottomPaddingLayoutGuide.snp.top) + $0.leading.trailing.equalToSuperview() + } + contentBottomPaddingLayoutGuide.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(footerLayoutGuide.snp.top) + $0.height.greaterThanOrEqualTo(80) } - copyrightLabel.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-32) - make.centerX.equalToSuperview() - make.height.equalTo(18) + footerLayoutGuide.snp.makeConstraints { + $0.bottom.equalToSuperview().offset(0.5 < view.safeAreaInsets.bottom ? 0 : -32) + $0.leading.trailing.equalToSuperview() } } private func configureView() { setUpLayOuts() setUpConstraints() - self.view.backgroundColor = .systemBackground + view.backgroundColor = UIColor.appColor(.neutral0) } } From c96edf91cd04676d869fbcc859e308290935f32b Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 6 Sep 2026 22:34:00 +0900 Subject: [PATCH 092/101] =?UTF-8?q?deisng:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Login/Login/LoginViewController.swift | 2 +- .../Register/RegisterFormViewModel.swift | 4 +- .../AgreementFormViewController.swift | 16 +++-- .../CertificationFormViewController.swift | 48 +++++++------ .../EnterFormViewController.swift | 57 +++++++++------- .../RegisterCompletionViewController.swift | 67 ++++++++++++++----- .../SelectTypeFormViewController.swift | 32 ++++++--- koin.xcodeproj/project.pbxproj | 2 +- 8 files changed, 149 insertions(+), 79 deletions(-) diff --git a/Koin/Presentation/Login/Login/LoginViewController.swift b/Koin/Presentation/Login/Login/LoginViewController.swift index f0288ed8..f42f6c9e 100644 --- a/Koin/Presentation/Login/Login/LoginViewController.swift +++ b/Koin/Presentation/Login/Login/LoginViewController.swift @@ -448,7 +448,7 @@ extension LoginViewController { copyrightLabel.snp.makeConstraints { $0.bottom.centerX.equalTo(footerLayoutGuide) - $0.height.equalTo(12) + $0.height.equalTo(18) } ownerButton.snp.makeConstraints { $0.top.centerX.equalTo(footerLayoutGuide) diff --git a/Koin/Presentation/Login/Register/RegisterFormViewModel.swift b/Koin/Presentation/Login/Register/RegisterFormViewModel.swift index de00b3d6..4e27ed98 100644 --- a/Koin/Presentation/Login/Register/RegisterFormViewModel.swift +++ b/Koin/Presentation/Login/Register/RegisterFormViewModel.swift @@ -106,7 +106,7 @@ extension RegisterFormViewModel { private func checkDuplicatedPhoneNumber(phone: String) { checkDuplicatedPhoneNumberUseCase.execute(phone: phone).sink { [weak self] completion in if case let .failure(error) = completion { - self?.outputSubject.send(.showHttpResult(error.message, .sub500)) + self?.outputSubject.send(.showHttpResult(error.message, .new600)) } } receiveValue: { [weak self] (_: Void) in self?.outputSubject.send(.changeSendVerificationButtonStatus) @@ -169,7 +169,7 @@ extension RegisterFormViewModel { private func checkDuplicatedNickname(nickname: String) { checkDuplicatedNicknameUseCase.execute(nickname: nickname).sink { [weak self] completion in if case let .failure(error) = completion { - self?.outputSubject.send(.showHttpResult(error.message, .danger700)) + self?.outputSubject.send(.showNicknameHttpResult(error.message, .new600)) } } receiveValue: { [weak self] _ in self?.outputSubject.send(.changeCheckButtonStatus) diff --git a/Koin/Presentation/Login/Register/ViewControllers/AgreementFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/AgreementFormViewController.swift index 642741e5..6931d1c0 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/AgreementFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/AgreementFormViewController.swift @@ -26,19 +26,19 @@ final class AgreementFormViewController: UIViewController { private let stepTextLabel = UILabel().then { $0.text = "1. 약관 동의" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "1 / 4" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 0.25 @@ -65,7 +65,7 @@ final class AgreementFormViewController: UIViewController { config.image = resizedImage config.imagePlacement = .leading config.imagePadding = 8 - config.baseForegroundColor = UIColor.appColor(.primary500) + config.baseForegroundColor = UIColor.appColor(.new500) config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 0) var attrTitle = AttributedString("모두 동의합니다.") @@ -79,7 +79,7 @@ final class AgreementFormViewController: UIViewController { $0.configurationUpdateHandler = { button in var updatedConfig = button.configuration - updatedConfig?.baseForegroundColor = UIColor.appColor(.primary500) + updatedConfig?.baseForegroundColor = UIColor.appColor(.new500) updatedConfig?.background.backgroundColor = UIColor.appColor(.neutral100) button.configuration = updatedConfig } @@ -150,12 +150,14 @@ extension AgreementFormViewController { let requiredChecked = agreementItems[0].checkButton.isSelected && agreementItems[1].checkButton.isSelected nextButton.isEnabled = requiredChecked - nextButton.backgroundColor = requiredChecked ? UIColor.appColor(.primary500) : UIColor.appColor(.neutral300) + nextButton.backgroundColor = requiredChecked ? UIColor.appColor(.new500) : UIColor.appColor(.neutral300) nextButton.setTitleColor(requiredChecked ? .white : UIColor.appColor(.neutral600), for: .normal) } private func updateCheckboxImage(checkbox: UIButton, isSelected: Bool) { - let original = isSelected ? UIImage.appImage(asset: .checkFilledCircle) : UIImage.appImage(asset: .checkEmptyCircle) + let original = isSelected + ? UIImage.appImage(asset: .checkFilledCircle)?.withTintColor(.appColor(.new500), renderingMode: .alwaysTemplate) + : UIImage.appImage(asset: .checkEmptyCircle) let resized = original?.resize(to: CGSize(width: 16, height: 16)) checkbox.setImage(resized, for: .normal) } diff --git a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift index 684aaa85..c2ab46f4 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift @@ -27,19 +27,19 @@ final class CertificationFormViewController: UIViewController { private let stepTextLabel = UILabel().then { $0.text = "2. 본인 인증" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "2 / 4" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 0.5 @@ -70,16 +70,16 @@ final class CertificationFormViewController: UIViewController { ) private let nameHelpLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "올바른 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "올바른 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } private let femaleButton = UIButton().then { - $0.applyRadioStyle(title: "여성", font: .appFont(.pretendardRegular, size: 16), image: .appImage(asset: .circlePrimary500), foregroundColor: .black) + $0.applyRadioStyle(title: "여성", font: .appFont(.pretendardRegular, size: 16), image: .appImage(asset: .circlePrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal), foregroundColor: .black) } private let maleButton = UIButton().then { - $0.applyRadioStyle(title: "남성", font: .appFont(.pretendardRegular, size: 16), image: .appImage(asset: .circlePrimary500), foregroundColor: .black) + $0.applyRadioStyle(title: "남성", font: .appFont(.pretendardRegular, size: 16), image: .appImage(asset: .circlePrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal), foregroundColor: .black) } private let phoneNumberLabel = UILabel().then { @@ -100,7 +100,7 @@ final class CertificationFormViewController: UIViewController { private let sendVerificationButton = StatefulButton( title: "인증번호 발송", font: .appFont(.pretendardRegular, size: 10), - enabledColor: .appColor(.primary500), + enabledColor: .appColor(.new500), disabledColor: .appColor(.neutral300), cornerRadius: 4 ).then { @@ -109,14 +109,14 @@ final class CertificationFormViewController: UIViewController { } private let phoneNumberReponseLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.danger700)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.numberOfLines = 2 $0.isHidden = true } let goToLoginButton = UIButton().then { $0.setTitle("로그인 하기", for: .normal) - $0.setTitleColor(.appColor(.primary500), for: .normal) + $0.setTitleColor(.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 12) $0.isHidden = true } @@ -130,7 +130,7 @@ final class CertificationFormViewController: UIViewController { private let contactButton = UIButton().then { $0.setTitle("문의하기", for: .normal) - $0.setTitleColor(.appColor(.primary500), for: .normal) + $0.setTitleColor(.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 12) $0.isHidden = true } @@ -154,7 +154,7 @@ final class CertificationFormViewController: UIViewController { private let verificationButton = StatefulButton( title: "인증번호 확인", font: .appFont(.pretendardRegular, size: 10), - enabledColor: .appColor(.primary500), + enabledColor: .appColor(.new500), disabledColor: .appColor(.neutral300), cornerRadius: 4 ).then { @@ -163,7 +163,7 @@ final class CertificationFormViewController: UIViewController { } private let verificationHelpLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.danger700)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } @@ -238,7 +238,7 @@ final class CertificationFormViewController: UIViewController { self?.viewModel.tempPhoneNumber = self?.phoneNumberTextField.text self?.viewModel.tempGender = self?.femaleButton.configuration?.image == UIImage.appImage(asset: .circleCheckedPrimary500) ? "1" : "0" self?.nextButton.isEnabled = true - self?.nextButton.backgroundColor = UIColor.appColor(.primary500) + self?.nextButton.backgroundColor = UIColor.appColor(.new500) self?.nextButton.setTitleColor(.white, for: .normal) let customSessionId = CustomSessionManager.getOrCreateSessionId(duration: .fifteenMinutes, eventName: "sign_up", loginStatus: 0, platform: "iOS") self?.inputSubject.send(.logEventWithSessionId(EventParameter.EventLabel.User.identityVerification, .click, "인증완료", customSessionId)) @@ -342,8 +342,8 @@ extension CertificationFormViewController { var femaleConfig = femaleButton.configuration var maleConfig = maleButton.configuration - femaleConfig?.image = UIImage.appImage(asset: isFemale ? .circleCheckedPrimary500 : .circlePrimary500) - maleConfig?.image = UIImage.appImage(asset: isFemale ? .circlePrimary500 : .circleCheckedPrimary500) + femaleConfig?.image = UIImage.appImage(asset: isFemale ? .circleCheckedPrimary500 : .circlePrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal) + maleConfig?.image = UIImage.appImage(asset: isFemale ? .circlePrimary500 : .circleCheckedPrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal) femaleButton.configuration = femaleConfig maleButton.configuration = maleConfig @@ -352,7 +352,8 @@ extension CertificationFormViewController { private func updatePhoneNumberSectionVisibility() { let nameCount = nameTextField.text?.count ?? 0 let isNameValid = (2...5).contains(nameCount) - let isGenderSelected = (femaleButton.configuration?.image == UIImage.appImage(asset: .circleCheckedPrimary500)) || (maleButton.configuration?.image == UIImage.appImage(asset: .circleCheckedPrimary500)) + let isGenderSelected = (femaleButton.configuration?.image == UIImage.appImage(asset: .circleCheckedPrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal)) + || (maleButton.configuration?.image == UIImage.appImage(asset: .circleCheckedPrimary500)?.withTintColor(.appColor(.new500), renderingMode: .alwaysOriginal)) let shouldShowPhoneFields = isNameValid && isGenderSelected @@ -401,10 +402,10 @@ extension CertificationFormViewController { private func showVerificationHelpResult(_ message: String, _ color: ColorAsset) { verificationHelpLabel.isHidden = false verificationHelpLabel.setImageText( - image: UIImage.appImage(asset: .warningOrange), + image: UIImage.appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: message, font: UIFont.appFont(.pretendardRegular, size: 12), - textColor: UIColor.appColor(color) + textColor: UIColor.appColor(.new600) ) } @@ -419,13 +420,18 @@ extension CertificationFormViewController { private func showHttpResult(_ message: String, _ color: ColorAsset) { phoneNumberReponseLabel.isHidden = false - if message == "이미 존재하는 전화번호입니다." { + if message.contains("이미 존재") { phoneNumberReponseLabel.setImageText(image: .appImage(asset: .warningRed), text: message, font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.danger600)) [goToLoginButton, phoneNotFoundLabel, contactButton].forEach { $0.isHidden = false } + } else if message.contains("24시간 이후 재시도") { + phoneNumberReponseLabel.setImageText(image: .appImage(asset: .warningRed), text: message, font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.danger600)) + [goToLoginButton, phoneNotFoundLabel, contactButton].forEach { + $0.isHidden = true + } } else { - phoneNumberReponseLabel.setImageText(image: .appImage(asset: .warningOrange), text: message, font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(color)) + phoneNumberReponseLabel.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: message, font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) [goToLoginButton, phoneNotFoundLabel, contactButton].forEach { $0.isHidden = true } @@ -474,7 +480,7 @@ extension CertificationFormViewController { self.timer?.invalidate() self.timer = nil self.verificationHelpLabel.isHidden = false - self.verificationHelpLabel.setImageText(image: .appImage(asset: .warningOrange), text: "유효시간이 지났습니다. 인증번호를 재발송 해주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + self.verificationHelpLabel.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "유효시간이 지났습니다. 인증번호를 재발송 해주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) } } } diff --git a/Koin/Presentation/Login/Register/ViewControllers/EnterFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/EnterFormViewController.swift index 9e652f3b..a58a8de4 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/EnterFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/EnterFormViewController.swift @@ -25,19 +25,19 @@ final class EnterFormViewController: UIViewController { private let stepTextLabel = UILabel().then { $0.text = "4. 정보 입력" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "4 / 4" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 1 @@ -65,12 +65,15 @@ final class EnterFormViewController: UIViewController { placeholder: "5~13자리로 입력해 주세요.", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14) - ) + ).then { + $0.autocorrectionType = .no + $0.textContentType = .oneTimeCode + } private let checkIdDuplicateButton = StatefulButton( title: "중복 확인", font: .appFont(.pretendardRegular, size: 10), - enabledColor: .appColor(.primary500), + enabledColor: .appColor(.new500), disabledColor: .appColor(.neutral300), cornerRadius: 4 ).then { @@ -99,10 +102,12 @@ final class EnterFormViewController: UIViewController { font: UIFont.appFont(.pretendardRegular, size: 13) ).then { $0.isSecureTextEntry = true + $0.autocorrectionType = .no + $0.textContentType = .oneTimeCode } private let passwordInfoLabel: UILabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "올바른 비밀번호 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "올바른 비밀번호 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } @@ -112,6 +117,8 @@ final class EnterFormViewController: UIViewController { font: UIFont.appFont(.pretendardRegular, size: 13) ).then { $0.isSecureTextEntry = true + $0.autocorrectionType = .no + $0.textContentType = .oneTimeCode $0.isHidden = true } @@ -159,11 +166,12 @@ final class EnterFormViewController: UIViewController { placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14) ).then { + $0.keyboardType = .numberPad $0.isHidden = true } private let studentIdWarningLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "올바른 학번 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "올바른 학번 양식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } @@ -178,7 +186,7 @@ final class EnterFormViewController: UIViewController { private let nicknameDuplicateButton = StatefulButton( title: "중복 확인", font: .appFont(.pretendardRegular, size: 10), - enabledColor: .appColor(.primary500), + enabledColor: .appColor(.new500), disabledColor: .appColor(.neutral300), cornerRadius: 4 ).then { @@ -187,7 +195,7 @@ final class EnterFormViewController: UIViewController { } private let nicknameResponseLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "중복된 닉네임입니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "이미 존재하는 닉네임입니다..", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } @@ -215,7 +223,7 @@ final class EnterFormViewController: UIViewController { } private let generalEmailResponseLabel = UILabel().then { - $0.setImageText(image: .appImage(asset: .warningOrange), text: "올바른 이메일 형식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.sub500)) + $0.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: "올바른 이메일 형식이 아닙니다. 다시 입력해 주세요.", font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) $0.isHidden = true } @@ -266,10 +274,10 @@ final class EnterFormViewController: UIViewController { guard !message.isEmpty else { return } self?.checkIdResponseLabel.isHidden = false self?.checkIdResponseLabel.setImageText( - image: UIImage.appImage(asset: .warningOrange), + image: UIImage.appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: message, font: UIFont.appFont(.pretendardRegular, size: 12), - textColor: .appColor(.sub500) + textColor: .appColor(.new600) ) case .successCheckDuplicatedId: self?.checkIdResponseLabel.isHidden = false @@ -285,13 +293,13 @@ final class EnterFormViewController: UIViewController { case let .showDeptDropDownList(deptList): self?.setUpDropDown(dropDown: strongSelf.deptDropDown, button: strongSelf.departmentDropdownButton, dataSource: deptList) case let .showNicknameHttpResult(message, color): - self?.nicknameResponseLabel.isHidden = false self?.nicknameResponseLabel.setImageText( - image: UIImage.appImage(asset: .warningOrange), + image: UIImage.appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: message, font: UIFont.appFont(.pretendardRegular, size: 12), - textColor: .appColor(.sub500) + textColor: .appColor(.new600) ) + self?.nicknameResponseLabel.isHidden = false case .changeCheckButtonStatus: self?.nicknameDuplicateButton.updateState(isEnabled: false) self?.nicknameResponseLabel.setImageText( @@ -399,10 +407,13 @@ extension EnterFormViewController { @objc private func keyboardWillShow(_ notification: Notification) { guard let userInfo = notification.userInfo, - let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + let bottomInset = keyboardFrame.height - (view.frame.height - nextButton.frame.minY) - scrollView.contentInset.bottom = keyboardFrame.height - scrollView.verticalScrollIndicatorInsets.bottom = keyboardFrame.height + scrollView.contentInset.bottom = bottomInset + scrollView.verticalScrollIndicatorInsets.bottom = bottomInset } @objc private func keyboardWillHide(_ notification: Notification) { @@ -509,7 +520,7 @@ extension EnterFormViewController { if isValid { nextButton.isEnabled = true - nextButton.backgroundColor = UIColor.appColor(.primary500) + nextButton.backgroundColor = UIColor.appColor(.new500) nextButton.setTitleColor(.white, for: .normal) } else { nextButton.isEnabled = false @@ -690,7 +701,7 @@ extension EnterFormViewController { } checkIdResponseLabel.snp.makeConstraints { - $0.top.equalTo(idTextField.snp.bottom).offset(8) + $0.top.equalTo(idTextField.snp.bottom) $0.leading.equalTo(idTextField.snp.leading).offset(4) $0.height.equalTo(20) } @@ -757,7 +768,7 @@ extension EnterFormViewController { } nicknameTextField.snp.makeConstraints { - $0.top.equalTo(studentIdTextField.snp.bottom).offset(8) + $0.top.equalTo(studentIdTextField.snp.bottom).offset(20) $0.leading.equalTo(departmentDropdownButton.snp.leading) $0.trailing.equalTo(nicknameDuplicateButton.snp.leading).offset(-16) $0.height.equalTo(40) @@ -777,7 +788,7 @@ extension EnterFormViewController { } studentEmailTextField.snp.makeConstraints { - $0.top.equalTo(nicknameTextField.snp.bottom).offset(8) + $0.top.equalTo(nicknameTextField.snp.bottom).offset(20) $0.leading.equalTo(departmentDropdownButton.snp.leading) $0.trailing.equalToSuperview().offset(-126) $0.height.equalTo(40) @@ -844,7 +855,7 @@ extension EnterFormViewController { setUpGeneralConstraints() nextButton.isEnabled = true - nextButton.backgroundColor = UIColor.appColor(.primary500) + nextButton.backgroundColor = UIColor.appColor(.new500) nextButton.setTitleColor(.white, for: .normal) } diff --git a/Koin/Presentation/Login/Register/ViewControllers/RegisterCompletionViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/RegisterCompletionViewController.swift index a1d8f429..2ead5772 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/RegisterCompletionViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/RegisterCompletionViewController.swift @@ -11,8 +11,18 @@ import SnapKit final class RegisterCompletionViewController: UIViewController { // MARK: - UI Components - private let koinLogoImageView = UIImageView().then { - $0.image = UIImage.appImage(asset: .koinLogo) + private let contentTopLayoutGuide = UILayoutGuide() + private let contentLayoutGuide = UILayoutGuide() + private let contentBottomLayoutGuide = UILayoutGuide() + + private let logoImageView = UIImageView().then { + $0.image = UIImage.appImage(asset: .bcsdSymbolLogo) + $0.contentMode = .scaleAspectFit + } + + private let logoTextImageView = UIImageView().then { + $0.image = UIImage.appImage(asset: .koinTextLogo) + $0.contentMode = .scaleAspectFit } private let registerCompletionLabel = UILabel().then { @@ -24,7 +34,7 @@ final class RegisterCompletionViewController: UIViewController { private let loginButton = UIButton().then { $0.setTitle("로그인 바로가기", for: .normal) $0.layer.cornerRadius = 8 - $0.backgroundColor = UIColor.appColor(.sub500) + $0.backgroundColor = .appColor(.new500) $0.setTitleColor(UIColor(.white), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) } @@ -32,9 +42,11 @@ final class RegisterCompletionViewController: UIViewController { private let homeButton = UIButton().then { $0.setTitle("홈화면 바로가기", for: .normal) $0.layer.cornerRadius = 8 - $0.backgroundColor = UIColor.appColor(.primary500) - $0.setTitleColor(UIColor(.white), for: .normal) + $0.backgroundColor = .appColor(.neutral0) + $0.setTitleColor(.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) + $0.layer.borderColor = UIColor.appColor(.new500).cgColor + $0.layer.borderWidth = 1 } // MARK: - Life Cycle @@ -71,36 +83,61 @@ extension RegisterCompletionViewController { extension RegisterCompletionViewController { private func setUpLayout() { - [koinLogoImageView, registerCompletionLabel, loginButton, homeButton].forEach { + [logoImageView, logoTextImageView, registerCompletionLabel, loginButton, homeButton].forEach { view.addSubview($0) } + + [contentTopLayoutGuide, contentLayoutGuide, contentBottomLayoutGuide].forEach { + view.addLayoutGuide($0) + } } private func setUpConstraints() { - koinLogoImageView.snp.makeConstraints { - $0.bottom.equalTo(registerCompletionLabel.snp.top).offset(-24) - $0.centerX.equalToSuperview() - $0.height.greaterThanOrEqualTo(56) - $0.width.greaterThanOrEqualTo(96) + logoImageView.snp.makeConstraints { + $0.top.centerX.equalTo(contentLayoutGuide) + $0.height.equalTo(66) + } + + logoTextImageView.snp.makeConstraints { + $0.top.equalTo(logoImageView.snp.bottom).offset(9) + $0.centerX.equalTo(contentLayoutGuide) + $0.width.equalTo(102) + $0.height.equalTo(41) } registerCompletionLabel.snp.makeConstraints { + $0.top.equalTo(logoTextImageView.snp.bottom).offset(24) $0.bottom.equalTo(loginButton.snp.top).offset(-56) - $0.centerX.equalToSuperview() + $0.centerX.equalTo(contentLayoutGuide) $0.height.equalTo(29) } loginButton.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.horizontalEdges.equalToSuperview().inset(24) + $0.horizontalEdges.equalTo(contentLayoutGuide).inset(48) $0.height.equalTo(48) } homeButton.snp.makeConstraints { $0.top.equalTo(loginButton.snp.bottom).offset(24) - $0.horizontalEdges.equalToSuperview().inset(24) + $0.horizontalEdges.equalTo(contentLayoutGuide).inset(48) + $0.bottom.equalTo(contentLayoutGuide) $0.height.equalTo(48) } + + contentTopLayoutGuide.snp.makeConstraints { + $0.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) + $0.bottom.equalTo(contentLayoutGuide.snp.top) + $0.height.equalTo(contentBottomLayoutGuide.snp.height).multipliedBy(2.0/3.0) + } + + contentLayoutGuide.snp.makeConstraints { + $0.leading.trailing.equalTo(view.safeAreaLayoutGuide) + $0.bottom.equalTo(contentBottomLayoutGuide.snp.top) + } + + contentBottomLayoutGuide.snp.makeConstraints { + $0.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide) + } } private func configureView() { diff --git a/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift index 196fd36f..ee4b2933 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift @@ -47,11 +47,17 @@ final class SelectTypeFormViewController: UIViewController { } private let logoImageView = UIImageView().then { - $0.image = UIImage.appImage(asset: .koinLogo) + $0.image = UIImage.appImage(asset: .bcsdSymbolLogo) + $0.contentMode = .scaleAspectFit + } + + private let logoTextImageView = UIImageView().then { + $0.image = UIImage.appImage(asset: .koinTextLogo) + $0.contentMode = .scaleAspectFit } private let studentButton = UIButton().then { - $0.backgroundColor = .appColor(.sub500) + $0.backgroundColor = .appColor(.new500) $0.setTitle("한국기술교육대학교 학생", for: .normal) $0.setTitleColor(.white, for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) @@ -59,11 +65,13 @@ final class SelectTypeFormViewController: UIViewController { } private let generalButton = UIButton().then { - $0.backgroundColor = .appColor(.primary500) + $0.backgroundColor = .appColor(.neutral0) $0.setTitle("외부인", for: .normal) - $0.setTitleColor(.white, for: .normal) + $0.setTitleColor(.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) $0.layer.cornerRadius = 8 + $0.layer.borderColor = UIColor.appColor(.new500).cgColor + $0.layer.borderWidth = 1 } // MARK: - Init @@ -133,7 +141,7 @@ extension SelectTypeFormViewController { // MARK: UI Settings extension SelectTypeFormViewController { private func setUpLayouts() { - [stepTextLabel, stepLabel, progressView, logoImageView, studentButton, generalButton].forEach { + [stepTextLabel, stepLabel, progressView, logoImageView, logoTextImageView, studentButton, generalButton].forEach { view.addSubview($0) } } @@ -156,14 +164,20 @@ extension SelectTypeFormViewController { } logoImageView.snp.makeConstraints { - $0.top.equalTo(progressView.snp.bottom).offset(100) + $0.top.equalTo(progressView.snp.bottom).offset(52) + $0.centerX.equalToSuperview() + $0.height.equalTo(66) + } + + logoTextImageView.snp.makeConstraints { + $0.top.equalTo(logoImageView.snp.bottom).offset(9) $0.centerX.equalToSuperview() - $0.width.greaterThanOrEqualTo(96) - $0.height.greaterThanOrEqualTo(56) + $0.width.equalTo(102) + $0.height.equalTo(41) } studentButton.snp.makeConstraints { - $0.top.equalTo(logoImageView.snp.bottom).offset(80) + $0.top.equalTo(logoTextImageView.snp.bottom).offset(52) $0.leading.equalToSuperview().offset(48) $0.trailing.equalToSuperview().offset(-48) $0.height.equalTo(48) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index c01a4cdd..06ca8366 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -3598,9 +3598,9 @@ children = ( 7CED8F262EF2D1F900457128 /* AgreementFormViewController.swift */, 7CED8F272EF2D1F900457128 /* CertificationFormViewController.swift */, + 7CED8F2B2EF2D1F900457128 /* SelectTypeFormViewController.swift */, 7CED8F282EF2D1F900457128 /* EnterFormViewController.swift */, 7CED8F292EF2D1F900457128 /* RegisterCompletionViewController.swift */, - 7CED8F2B2EF2D1F900457128 /* SelectTypeFormViewController.swift */, ); path = ViewControllers; sourceTree = ""; From 324616c7ee599c51039d3db214618370007dbb84 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Sun, 6 Sep 2026 23:47:31 +0900 Subject: [PATCH 093/101] =?UTF-8?q?design:=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=20=EC=B0=BE=EA=B8=B0=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Login/FindId/StateButton.swift | 4 ++-- Koin/Presentation/Login/FindId/StateView.swift | 8 ++++---- .../FindPhoneIdViewController.swift | 18 +++++++++++++----- .../FoundIdViewController.swift | 14 ++++++++------ 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Koin/Presentation/Login/FindId/StateButton.swift b/Koin/Presentation/Login/FindId/StateButton.swift index e129bfcb..929c0b9a 100644 --- a/Koin/Presentation/Login/FindId/StateButton.swift +++ b/Koin/Presentation/Login/FindId/StateButton.swift @@ -31,11 +31,11 @@ final class StateButton: UIButton { self.setTitleColor(UIColor.appColor(.neutral600), for: .normal) self.isEnabled = false case .usable: - self.backgroundColor = UIColor.appColor(.primary500) + self.backgroundColor = UIColor.appColor(.new500) self.setTitleColor(.white, for: .normal) self.isEnabled = true case .retry: - self.backgroundColor = UIColor.appColor(.sub500) + self.backgroundColor = UIColor.appColor(.new600) self.setTitleColor(.white, for: .normal) self.isEnabled = false } diff --git a/Koin/Presentation/Login/FindId/StateView.swift b/Koin/Presentation/Login/FindId/StateView.swift index e7b71c03..d9348b05 100644 --- a/Koin/Presentation/Login/FindId/StateView.swift +++ b/Koin/Presentation/Login/FindId/StateView.swift @@ -37,13 +37,13 @@ final class StateView: UIView { messageLabel.text = message switch state { case .success: - imageView.image = UIImage(named: "successCircle") + imageView.image = UIImage.appImage(asset: .successCircle) messageLabel.textColor = UIColor.appColor(.success700) case .warning: - imageView.image = UIImage(named: "warningOrange") - messageLabel.textColor = UIColor.appColor(.sub500) + imageView.image = UIImage.appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600)) + messageLabel.textColor = UIColor.appColor(.new600) case .dangerous: - imageView.image = UIImage(named: "warningRed") + imageView.image = UIImage.appImage(asset: .warningRed) messageLabel.textColor = UIColor.appColor(.danger700) } } diff --git a/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift b/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift index 7f87f2cd..d81ac380 100644 --- a/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift +++ b/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift @@ -28,7 +28,9 @@ final class FindPhoneIdViewController: UIViewController { $0.text = certType == .phone ? "휴대전화 번호" : "이메일" } - private lazy var phoneNumberTextField = DefaultTextField(placeholder: certType == .phone ? "- 없이 번호를 입력해 주세요." : "등록된 이메일을 입력해 주세요.", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)) + private lazy var phoneNumberTextField = DefaultTextField(placeholder: certType == .phone ? "- 없이 번호를 입력해 주세요." : "등록된 이메일을 입력해 주세요.", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { + $0.keyboardType = .numberPad + } private let sendButton = StateButton().then { $0.setState(state: .unusable) @@ -41,7 +43,7 @@ final class FindPhoneIdViewController: UIViewController { private let changeButton = UIButton().then { $0.setTitle("이메일로 찾기", for: .normal) - $0.setTitleColor(UIColor.appColor(.primary500), for: .normal) + $0.setTitleColor(UIColor.appColor(.new500), for: .normal) $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 12) } @@ -73,6 +75,8 @@ final class FindPhoneIdViewController: UIViewController { private let saveButton = StateButton(font: UIFont.appFont(.pretendardMedium, size: 16)).then { $0.setState(state: .unusable) $0.setTitle("저장", for: .normal) + }.then { + $0.layer.cornerRadius = 8 } init(viewModel: FindIdViewModel, certType: CertType = .phone) { @@ -172,6 +176,10 @@ extension FindPhoneIdViewController { } } @objc private func sendButtonTapped() { + [helpLabel, changeButton].forEach { + $0.isHidden = true + } + if certType == .phone { viewModel.sendVerificationCode(phoneNumber: phoneNumberTextField.text ?? "") } else { @@ -242,15 +250,15 @@ extension FindPhoneIdViewController { phoneStateView.snp.makeConstraints { $0.top.equalTo(phoneNumberTextField.snp.bottom).offset(5) $0.leading.equalTo(phoneNumberTextField) - $0.height.equalTo(19) } helpLabel.snp.makeConstraints { - $0.top.equalTo(phoneStateView.snp.bottom).offset(5) + $0.top.equalTo(phoneNumberTextField.snp.bottom).offset(5) $0.leading.equalTo(phoneNumberLabel) + $0.height.equalTo(19) } changeButton.snp.makeConstraints { $0.leading.equalTo(helpLabel.snp.trailing).offset(5) - $0.top.bottom.equalTo(helpLabel) + $0.centerY.equalTo(helpLabel) $0.width.equalTo(66) $0.height.equalTo(19) } diff --git a/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift b/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift index 641fc2e6..0412df99 100644 --- a/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift +++ b/Koin/Presentation/Login/FindId/ViewControllers/FoundIdViewController.swift @@ -23,18 +23,20 @@ final class FoundIdViewController: UIViewController { private let subMessageLabel = UILabel() private let loginButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.sub500) + $0.backgroundColor = UIColor.appColor(.new500) $0.setTitle("로그인 바로가기", for: .normal) $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 15) + $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) $0.layer.cornerRadius = 8 } private let findPasswordButton = UIButton().then { - $0.backgroundColor = UIColor.appColor(.primary500) + $0.backgroundColor = UIColor.appColor(.neutral0) $0.setTitle("비밀번호 찾기", for: .normal) - $0.setTitleColor(UIColor.appColor(.neutral0), for: .normal) - $0.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 15) + $0.setTitleColor(UIColor.appColor(.new500), for: .normal) + $0.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 16) + $0.layer.borderColor = UIColor.appColor(.new500).cgColor + $0.layer.borderWidth = 1 $0.layer.cornerRadius = 8 } @@ -260,7 +262,7 @@ extension FoundIdViewController { private func setupComponents() { messageLabel.font = UIFont.appFont(.pretendardBold, size: 24) - messageLabel.textColor = UIColor.appColor(.primary500) + messageLabel.textColor = UIColor.appColor(.new500) } private func setupUI() { From 6407a77a06abb26a1fab831dcd9dbaef3eafc49b Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 00:14:51 +0900 Subject: [PATCH 094/101] =?UTF-8?q?design:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangePasswordSuccessViewController.swift | 6 ++++-- .../FindPasswordCertViewController.swift | 15 +++++++++------ .../FindPasswordChangeViewController.swift | 8 +++++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift b/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift index 32599802..b5a19637 100644 --- a/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift +++ b/Koin/Presentation/Login/FindPassword/ChangePasswordSuccessViewController.swift @@ -15,7 +15,7 @@ final class ChangePasswordSuccessViewController: UIViewController { // MARK: - UI Components private let circleImageView = UIImageView().then { - $0.image = UIImage(named: "checkFilledCircle") + $0.image = .appImage(asset: .checkEmptyCircle)?.withTintColor(.appColor(.new600)) } private let messageLabel = UILabel().then { @@ -29,6 +29,8 @@ final class ChangePasswordSuccessViewController: UIViewController { private let goLoginButton = StateButton(font: UIFont.appFont(.pretendardBold, size: 15)).then { $0.setState(state: .usable) $0.setTitle("로그인 화면 바로가기", for: .normal) + }.then { + $0.layer.cornerRadius = 8 } init() { @@ -232,7 +234,7 @@ extension ChangePasswordSuccessViewController { private func setupComponents() { messageLabel.font = UIFont.appFont(.pretendardBold, size: 24) - messageLabel.textColor = UIColor.appColor(.primary500) + messageLabel.textColor = UIColor.appColor(.new500) subMessageLabel.font = UIFont.appFont(.pretendardMedium, size: 16) subMessageLabel.textColor = UIColor.appColor(.gray) } diff --git a/Koin/Presentation/Login/FindPassword/FindPasswordCertViewController.swift b/Koin/Presentation/Login/FindPassword/FindPasswordCertViewController.swift index de9a1d46..8a080609 100644 --- a/Koin/Presentation/Login/FindPassword/FindPasswordCertViewController.swift +++ b/Koin/Presentation/Login/FindPassword/FindPasswordCertViewController.swift @@ -25,19 +25,19 @@ final class FindPasswordCertViewController: UIViewController { private let stepTextLabel = UILabel().then { $0.text = "1. 계정 인증" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "1 / 2" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 0.5 @@ -195,6 +195,9 @@ extension FindPasswordCertViewController { } } @objc private func sendButtonTapped() { + [helpLabel, changeButton].forEach { + $0.isHidden = true + } switch certType { case .phone: viewModel.sendVerificationCode() case .email: viewModel.sendVerificationEmail() @@ -283,7 +286,7 @@ extension FindPasswordCertViewController { $0.height.equalTo(32) } helpLabel.snp.makeConstraints { - $0.top.equalTo(phoneTextField.snp.bottom).offset(3) + $0.top.equalTo(phoneTextField.snp.bottom).offset(8) $0.leading.equalTo(phoneTextField) } changeButton.snp.makeConstraints { @@ -293,7 +296,7 @@ extension FindPasswordCertViewController { $0.height.equalTo(19) } phoneStateView.snp.makeConstraints { - $0.top.equalTo(helpLabel.snp.bottom).offset(4) + $0.top.equalTo(phoneTextField.snp.bottom).offset(8) $0.leading.equalTo(stepTextLabel) $0.height.equalTo(19) } @@ -336,7 +339,7 @@ extension FindPasswordCertViewController { helpLabel.font = UIFont.appFont(.pretendardRegular, size: 12) helpLabel.textColor = UIColor.appColor(.neutral500) changeButton.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 12) - changeButton.setTitleColor(UIColor.appColor(.primary500), for: .normal) + changeButton.setTitleColor(UIColor.appColor(.new500), for: .normal) } private func setUpTextFieldUnderline() { [idtextField, phoneTextField, certNumberTextField].forEach { diff --git a/Koin/Presentation/Login/FindPassword/FindPasswordChangeViewController.swift b/Koin/Presentation/Login/FindPassword/FindPasswordChangeViewController.swift index 415c780d..c5d233d4 100644 --- a/Koin/Presentation/Login/FindPassword/FindPasswordChangeViewController.swift +++ b/Koin/Presentation/Login/FindPassword/FindPasswordChangeViewController.swift @@ -19,19 +19,19 @@ final class FindPasswordChangeViewController: UIViewController { private let stepTextLabel = UILabel().then { $0.text = "2. 비밀번호 변경" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "2 / 2" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 1 @@ -68,6 +68,8 @@ final class FindPasswordChangeViewController: UIViewController { private let nextButton = StateButton(font: UIFont.appFont(.pretendardMedium, size: 15)).then { $0.setTitle("다음", for: .normal) $0.setState(state: .unusable) + }.then { + $0.layer.cornerRadius = 8 } init(viewModel: FindPasswordViewModel, certType: FindPasswordCertViewController.CertType) { From 85b3d1c10e7f585a7ae3d03fc44b8a42fa5f2b17 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 01:14:27 +0900 Subject: [PATCH 095/101] =?UTF-8?q?design:=20=EC=84=A4=EC=A0=95=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeMyProfileViewController.swift | 11 ++++--- .../ChangePasswordViewController.swift | 23 ++++++++----- .../SubViews/CertificationView.swift | 4 +-- .../SubViews/ChangePasswordView.swift | 6 ++-- .../Setting/Noti/NotiViewController.swift | 33 +++++++------------ .../Setting/Policy/PolicyViewController.swift | 2 +- .../Settings/SettingsViewController.swift | 2 +- 7 files changed, 39 insertions(+), 42 deletions(-) diff --git a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift index 5a9f0478..8be40d54 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift @@ -172,8 +172,8 @@ final class ChangeMyProfileViewController: UIViewController { var updatedConfig = button.configuration ?? UIButton.Configuration.plain() let isSelected = button.isSelected updatedConfig.image = isSelected - ? UIImage(named: "circleCheckedPrimary500") - : UIImage(named: "circlePrimary500") + ? UIImage.appImage(asset: .circleCheckedPrimary500)?.withTintColor(.appColor(.new500)) + : UIImage.appImage(asset: .circlePrimary500)?.withTintColor(.appColor(.new500)) var text = AttributedString("남성") text.font = UIFont.appFont(.pretendardRegular, size: 12) updatedConfig.attributedTitle = text @@ -196,8 +196,8 @@ final class ChangeMyProfileViewController: UIViewController { var updatedConfig = button.configuration ?? UIButton.Configuration.plain() let isSelected = button.isSelected updatedConfig.image = isSelected - ? UIImage(named: "circleCheckedPrimary500") - : UIImage(named: "circlePrimary500") + ? UIImage.appImage(asset: .circleCheckedPrimary500)?.withTintColor(.appColor(.new500)) + : UIImage.appImage(asset: .circlePrimary500)?.withTintColor(.appColor(.new500)) var text = AttributedString("여성") text.font = UIFont.appFont(.pretendardRegular, size: 12) updatedConfig.attributedTitle = text @@ -209,6 +209,7 @@ final class ChangeMyProfileViewController: UIViewController { private let saveButton = StateButton(font: UIFont.appFont(.pretendardMedium, size: 15)).then { $0.setTitle("저장", for: .normal) $0.setState(state: .unusable) + $0.layer.cornerRadius = 8 } // MARK: - Initialization @@ -713,7 +714,7 @@ extension ChangeMyProfileViewController { helpLabel.font = UIFont.appFont(.pretendardRegular, size: 12) helpLabel.textColor = UIColor.appColor(.neutral500) inquryButton.titleLabel?.font = UIFont.appFont(.pretendardRegular, size: 12) - inquryButton.setTitleColor(UIColor.appColor(.primary500), for: .normal) + inquryButton.setTitleColor(UIColor.appColor(.new500), for: .normal) emailTextLabel.font = UIFont.appFont(.pretendardRegular, size: 14) emailTextLabel.textColor = .black } diff --git a/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift b/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift index e086c7d4..8739243d 100644 --- a/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift +++ b/Koin/Presentation/Setting/ChangePassword/ChangePasswordViewController.swift @@ -21,18 +21,18 @@ final class ChangePasswordViewController: UIViewController { private let progressTitleLabel = UILabel().then { $0.text = "1. 계정 인증" $0.font = UIFont.appFont(.pretendardMedium, size: 16) - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) } private let progressStepLabel = UILabel().then { $0.text = "1 / 2" $0.font = UIFont.appFont(.pretendardMedium, size: 16) - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral300) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 0.5 @@ -47,6 +47,7 @@ final class ChangePasswordViewController: UIViewController { button.backgroundColor = UIColor.appColor(.neutral300) button.setTitleColor(UIColor.appColor(.neutral600), for: .normal) button.titleLabel?.font = UIFont.appFont(.pretendardMedium, size: 15) + button.layer.cornerRadius = 8 } private let certificationView = CertificationView(frame: .zero).then { view in @@ -85,7 +86,7 @@ final class ChangePasswordViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - configureNavigationBar(style: .fill) + configureNavigationBar(style: .empty) } @@ -97,7 +98,12 @@ final class ChangePasswordViewController: UIViewController { outputSubject.receive(on: DispatchQueue.main).sink { [weak self] output in switch output { case let .showToast(message, success, dismiss): - self?.showToast(message: message, success: success) + self?.showToastMessage(config: .init( + intent: success ? .neutral : .negative, + variant: .standard, + message: message, + bottomInset: 68 + )) if dismiss { self?.navigationController?.popViewController(animated: true) } @@ -126,7 +132,7 @@ final class ChangePasswordViewController: UIViewController { extension ChangePasswordViewController { private func changeButtonEnable(isEnable: Bool) { - completeButton.backgroundColor = isEnable ? UIColor.appColor(.primary500) : UIColor.appColor(.neutral300) + completeButton.backgroundColor = isEnable ? UIColor.appColor(.new500) : UIColor.appColor(.neutral300) completeButton.setTitleColor(isEnable ? UIColor.appColor(.neutral0) : UIColor.appColor(.neutral600), for: .normal) completeButton.isEnabled = isEnable ? true : false } @@ -194,9 +200,8 @@ extension ChangePasswordViewController { make.height.equalTo(300) } completeButton.snp.makeConstraints { make in - make.bottom.equalTo(view.snp.bottom).offset(-24) - make.leading.equalTo(view.snp.leading).offset(24) - make.trailing.equalTo(view.snp.trailing).offset(-24) + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-10) + make.leading.trailing.equalToSuperview().inset(24) make.height.equalTo(48) } } diff --git a/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift b/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift index 93e01996..a38db9a7 100644 --- a/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift +++ b/Koin/Presentation/Setting/ChangePassword/SubViews/CertificationView.swift @@ -57,7 +57,7 @@ final class CertificationView: UIView { } private let errorResponseLabel = UILabel().then { - $0.textColor = UIColor.appColor(.sub500) + $0.textColor = UIColor.appColor(.new600) $0.font = UIFont.appFont(.pretendardRegular, size: 12) } @@ -83,7 +83,7 @@ final class CertificationView: UIView { } func showErrorMessage(message: String) { - passwordTextField.layer.borderColor = UIColor.appColor(.sub500).cgColor + passwordTextField.layer.borderColor = UIColor.appColor(.new600).cgColor passwordTextField.layer.borderWidth = 1.0 errorResponseLabel.text = "⚠ \(message)" } diff --git a/Koin/Presentation/Setting/ChangePassword/SubViews/ChangePasswordView.swift b/Koin/Presentation/Setting/ChangePassword/SubViews/ChangePasswordView.swift index b404e616..f8c7a086 100644 --- a/Koin/Presentation/Setting/ChangePassword/SubViews/ChangePasswordView.swift +++ b/Koin/Presentation/Setting/ChangePassword/SubViews/ChangePasswordView.swift @@ -22,6 +22,7 @@ final class ChangePasswordView: UIView { let passwordTextField = UITextField().then { textField in textField.placeholder = "새 비밀번호를 입력해주세요." textField.isSecureTextEntry = true + textField.rightViewMode = .never } private let englishStackView = UIStackView().then { stackView in @@ -44,8 +45,9 @@ final class ChangePasswordView: UIView { let passwordCheckTextField = UITextField().then { textField in textField.placeholder = "새 비밀번호를 다시 입력해주세요." - textField.layer.borderColor = UIColor.appColor(.sub500).cgColor + textField.layer.borderColor = UIColor.appColor(.new600).cgColor textField.isSecureTextEntry = true + textField.rightViewMode = .never } private let changeSecureButton1 = UIButton().then { button in @@ -57,7 +59,7 @@ final class ChangePasswordView: UIView { } private let errorResponseLabel = UILabel().then { - $0.textColor = UIColor.appColor(.sub500) + $0.textColor = UIColor.appColor(.new600) $0.font = UIFont.appFont(.pretendardRegular, size: 12) } diff --git a/Koin/Presentation/Setting/Noti/NotiViewController.swift b/Koin/Presentation/Setting/Noti/NotiViewController.swift index 7bee9bd1..b6f633b0 100644 --- a/Koin/Presentation/Setting/Noti/NotiViewController.swift +++ b/Koin/Presentation/Setting/Noti/NotiViewController.swift @@ -81,7 +81,7 @@ final class NotiViewController: UIViewController { private let soldOutSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() @@ -93,7 +93,7 @@ final class NotiViewController: UIViewController { private let diningImageUploadSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() @@ -123,7 +123,7 @@ final class NotiViewController: UIViewController { private let chatSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() @@ -183,13 +183,13 @@ final class NotiViewController: UIViewController { private let eventSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() private let reviewSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() @@ -235,9 +235,8 @@ final class NotiViewController: UIViewController { self.mealLabels.append(label) // 스위치 생성 let switchControl = UISwitch() - switchControl.onTintColor = UIColor.appColor(.primary500) + switchControl.onTintColor = UIColor.appColor(.new500) wrapView.addSubview(switchControl) - switchControl.transform = CGAffineTransformMakeScale(0.9, 0.75) switchControl.snp.makeConstraints { switchControl in switchControl.trailing.equalToSuperview().inset(21) switchControl.top.equalToSuperview().inset(15) @@ -278,7 +277,7 @@ final class NotiViewController: UIViewController { private let callVanSwitch: UISwitch = { let uiSwitch = UISwitch() - uiSwitch.onTintColor = UIColor.appColor(.primary500) + uiSwitch.onTintColor = UIColor.appColor(.new500) return uiSwitch }() @@ -320,7 +319,7 @@ final class NotiViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) - configureNavigationBar(style: .fill) + configureNavigationBar(style: .empty) } // MARK: - Bind @@ -548,7 +547,6 @@ extension NotiViewController { make.top.equalTo(16) } - soldOutSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) soldOutSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) @@ -583,7 +581,6 @@ extension NotiViewController { make.top.equalTo(16) } - diningImageUploadSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) diningImageUploadSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) @@ -614,7 +611,6 @@ extension NotiViewController { make.top.equalTo(chatNotiLabel.snp.bottom).offset(8) make.height.equalTo(17) } - chatSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) chatSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) @@ -645,9 +641,7 @@ extension NotiViewController { keywordNotiChevronImage.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(21) - make.top.equalToSuperview().inset(15) - make.width.equalTo(16) - make.height.equalTo(20) + make.centerY.equalToSuperview() } lostItemKeywordNotiLabel.snp.makeConstraints { make in @@ -664,9 +658,7 @@ extension NotiViewController { lostItemKeywordNotiChevronImage.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(21) - make.top.equalToSuperview().inset(15) - make.width.equalTo(16) - make.height.equalTo(20) + make.centerY.equalToSuperview() } shopGuideLabel.snp.makeConstraints { make in @@ -686,8 +678,7 @@ extension NotiViewController { make.height.equalTo(26) make.top.equalTo(16) } - - eventSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) + eventSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) @@ -710,7 +701,6 @@ extension NotiViewController { make.top.equalTo(16) } - reviewSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) reviewSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) @@ -736,7 +726,6 @@ extension NotiViewController { make.height.equalTo(26) make.top.equalTo(16) } - callVanSwitch.transform = CGAffineTransformMakeScale(0.9, 0.75) callVanSwitch.snp.makeConstraints { make in make.trailing.equalTo(view.snp.trailing).inset(21) make.top.equalToSuperview().inset(15) diff --git a/Koin/Presentation/Setting/Policy/PolicyViewController.swift b/Koin/Presentation/Setting/Policy/PolicyViewController.swift index 71c215df..a486b402 100644 --- a/Koin/Presentation/Setting/Policy/PolicyViewController.swift +++ b/Koin/Presentation/Setting/Policy/PolicyViewController.swift @@ -63,7 +63,7 @@ final class PolicyViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - configureNavigationBar(style: .fill) + configureNavigationBar(style: .empty) } private func bind() { diff --git a/Koin/Presentation/Setting/Settings/SettingsViewController.swift b/Koin/Presentation/Setting/Settings/SettingsViewController.swift index ab85e5d0..db250eb7 100644 --- a/Koin/Presentation/Setting/Settings/SettingsViewController.swift +++ b/Koin/Presentation/Setting/Settings/SettingsViewController.swift @@ -108,7 +108,7 @@ final class SettingsViewController: UIViewController { self.recentVersionLabel.textColor = UIColor.appColor(.neutral400) } else { self.recentVersionLabel.text = "최신 버전 \(version)" - self.recentVersionLabel.textColor = UIColor.appColor(.primary500) + self.recentVersionLabel.textColor = UIColor.appColor(.new500) } } } From 8e850238bd07e8a2d6fee58a91263573104ae8c8 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 10:39:31 +0900 Subject: [PATCH 096/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CallVanPostPlaceBottomSheetView.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift index 68abcf02..eaa06547 100644 --- a/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift +++ b/Koin/Presentation/CallVan/CallVanPost/Subviews/CallVanPostPlaceBottomSheetView.swift @@ -20,10 +20,6 @@ final class CallVanPostPlaceBottomSheetView: UIView { // MARK: - State private var selectedPlace: CallVanPlace? { didSet { - guard let selectedPlace else { - applyButton.isEnabled = false - return - } updateSelection(selectedPlace) updateTextField(isEditing: selectedPlace == .custom) validate() @@ -206,13 +202,15 @@ extension CallVanPostPlaceBottomSheetView: UITextFieldDelegate { extension CallVanPostPlaceBottomSheetView { // MARK: - Update CollecitonView - private func updateSelection(_ selectedPlace: CallVanPlace) { - guard let selectedIndex = filterGroup.items.firstIndex(where: { $0.title == selectedPlace.rawValue }) else { - return + private func updateSelection(_ selectedPlace: CallVanPlace?) { + let before = filterGroup.items.map(\.isSelected) + + filterGroup.reset(true) + if let selectedPlace, + let selectedIndex = filterGroup.items.firstIndex(where: { $0.title == selectedPlace.rawValue }) { + filterGroup.didTap(itemAt: selectedIndex) } - let before = filterGroup.items.map(\.isSelected) - filterGroup.didTap(itemAt: selectedIndex) let after = filterGroup.items.map(\.isSelected) filterGroupCollectionView.update( From 4f8ba3b97dfcb673b8d93e3c4225d24f58ae17b3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 23:08:18 +0900 Subject: [PATCH 097/101] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/FindPhoneIdViewController.swift | 8 ++++++-- .../ViewControllers/CertificationFormViewController.swift | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift b/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift index d81ac380..1957d876 100644 --- a/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift +++ b/Koin/Presentation/Login/FindId/ViewControllers/FindPhoneIdViewController.swift @@ -28,8 +28,12 @@ final class FindPhoneIdViewController: UIViewController { $0.text = certType == .phone ? "휴대전화 번호" : "이메일" } - private lazy var phoneNumberTextField = DefaultTextField(placeholder: certType == .phone ? "- 없이 번호를 입력해 주세요." : "등록된 이메일을 입력해 주세요.", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { - $0.keyboardType = .numberPad + private lazy var phoneNumberTextField = DefaultTextField( + placeholder: certType == .phone ? "- 없이 번호를 입력해 주세요." : "등록된 이메일을 입력해 주세요.", + placeholderColor: UIColor.appColor(.neutral400), + font: UIFont.appFont(.pretendardRegular, size: 14) + ).then { + $0.keyboardType = certType == .phone ? .numberPad : .emailAddress } private let sendButton = StateButton().then { diff --git a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift index c2ab46f4..ba8f166c 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift @@ -430,6 +430,7 @@ extension CertificationFormViewController { [goToLoginButton, phoneNotFoundLabel, contactButton].forEach { $0.isHidden = true } + sendVerificationButton.updateState(isEnabled: false) } else { phoneNumberReponseLabel.setImageText(image: .appImage(asset: .warningOrange)?.withTintColor(.appColor(.new600), renderingMode: .alwaysOriginal), text: message, font: .appFont(.pretendardRegular, size: 12), textColor: .appColor(.new600)) [goToLoginButton, phoneNotFoundLabel, contactButton].forEach { From 6099454e0c407f9571fab35e90332a126b8afc71 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 23:08:34 +0900 Subject: [PATCH 098/101] =?UTF-8?q?chore:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/SelectTypeFormViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift index ee4b2933..2fc12b98 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/SelectTypeFormViewController.swift @@ -24,19 +24,19 @@ final class SelectTypeFormViewController: UIViewController { // MARK: - UI Components private let stepTextLabel = UILabel().then { $0.text = "3. 회원 유형 선택" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let stepLabel = UILabel().then { $0.text = "3 / 4" - $0.textColor = UIColor.appColor(.primary500) + $0.textColor = UIColor.appColor(.new500) $0.font = UIFont.appFont(.pretendardMedium, size: 16) } private let progressView = UIProgressView().then { $0.trackTintColor = UIColor.appColor(.neutral200) - $0.progressTintColor = UIColor.appColor(.primary500) + $0.progressTintColor = UIColor.appColor(.new500) $0.layer.cornerRadius = 4 $0.clipsToBounds = true $0.progress = 0.75 From d61a2ff32baacb6ad3e27c5561ff830220f2b585 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 7 Sep 2026 23:08:42 +0900 Subject: [PATCH 099/101] =?UTF-8?q?chore:=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CertificationFormViewController.swift | 2 ++ .../ChangeMyProfileViewController.swift | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift index ba8f166c..af3e3ffd 100644 --- a/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift +++ b/Koin/Presentation/Login/Register/ViewControllers/CertificationFormViewController.swift @@ -94,6 +94,7 @@ final class CertificationFormViewController: UIViewController { placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14) ).then { + $0.keyboardType = .numberPad $0.isHidden = true } @@ -140,6 +141,7 @@ final class CertificationFormViewController: UIViewController { placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14) ).then { + $0.keyboardType = .numberPad $0.isHidden = true } diff --git a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift index 8be40d54..945912e5 100644 --- a/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift +++ b/Koin/Presentation/Setting/ChangeMyProfile/ChangeMyProfileViewController.swift @@ -50,6 +50,7 @@ final class ChangeMyProfileViewController: UIViewController { private let idTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { $0.isUserInteractionEnabled = false + $0.keyboardType = .alphabet } private let nameTitleLabel = UILabel().then { @@ -76,7 +77,9 @@ final class ChangeMyProfileViewController: UIViewController { $0.text = "휴대전화" } - private let phoneTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)) + private let phoneTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { + $0.keyboardType = .numberPad + } private let sendButton = StateButton(title: "인증번호 발송").then { $0.setState(state: .unusable) @@ -88,6 +91,7 @@ final class ChangeMyProfileViewController: UIViewController { private let certNumberTextField = DefaultTextField(placeholder: "인증번호를 입력해주세요.", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { $0.isHidden = true + $0.keyboardType = .numberPad } private let remainTimeLabel = UILabel().then { @@ -117,7 +121,9 @@ final class ChangeMyProfileViewController: UIViewController { $0.text = "이메일" } - private let emailTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)) + private let emailTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { + $0.keyboardType = .alphabet + } private lazy var emailTextLabel = UILabel().then { $0.text = "@koreatech.ac.kr" @@ -139,6 +145,7 @@ final class ChangeMyProfileViewController: UIViewController { private lazy var studentNumberTextField = DefaultTextField(placeholder: "", placeholderColor: UIColor.appColor(.neutral400), font: UIFont.appFont(.pretendardRegular, size: 14)).then { $0.isHidden = userType == .general ? true : false + $0.keyboardType = .numberPad } private lazy var majorTitleLabel = UILabel().then { From 11b89fbb1d41d46020e2613433f86d7b2f9b93db Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 14 Sep 2026 19:55:50 +0900 Subject: [PATCH 100/101] =?UTF-8?q?chore:=20=ED=8C=80=EC=9B=90=EB=AA=A8?= =?UTF-8?q?=EC=A7=91=20=EC=9B=B9=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Core/Extensions/Asset/ImageAsset.swift | 1 + Koin/Domain/Model/Home/CategoryModels.swift | 9 +++++++-- .../Category/CategoryHostingController.swift | 12 ++++++++++++ .../Home/Category/CategoryView.swift | 18 ++++++++++++------ .../categoryRecruit.imageset/Contents.json | 12 ++++++++++++ .../categoryRecruit.svg | 8 ++++++++ 6 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/Contents.json create mode 100644 Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/categoryRecruit.svg diff --git a/Koin/Core/Extensions/Asset/ImageAsset.swift b/Koin/Core/Extensions/Asset/ImageAsset.swift index faaeda9c..18714587 100644 --- a/Koin/Core/Extensions/Asset/ImageAsset.swift +++ b/Koin/Core/Extensions/Asset/ImageAsset.swift @@ -256,6 +256,7 @@ public enum ImageAsset: String { // MARK: - Home case categoryBusiness + case categoryRecruit case categoryBusSearch case categoryBusTimetable case categoryCallVan diff --git a/Koin/Domain/Model/Home/CategoryModels.swift b/Koin/Domain/Model/Home/CategoryModels.swift index 1d281044..41031cc0 100644 --- a/Koin/Domain/Model/Home/CategoryModels.swift +++ b/Koin/Domain/Model/Home/CategoryModels.swift @@ -20,6 +20,7 @@ enum HomeCategoryItem: Identifiable { case land case business case department + case recruit var id: String { title } @@ -49,13 +50,15 @@ enum HomeCategoryItem: Identifiable { return "복덕방" case .business: return "코인 for Business" + case .recruit: + return "팀원모집" } } var subtitle: String? { switch self { - case .timetable: - return "내 강의 정보 확인하기" + case .recruit: + return "교내 활동 팀원 구하기" case .lostItem: return "분실물 신고 / 조회하기" default: @@ -89,6 +92,8 @@ enum HomeCategoryItem: Identifiable { return .categoryLand case .business: return .categoryBusiness + case .recruit: + return .categoryRecruit } } } diff --git a/Koin/Presentation/Home/Category/CategoryHostingController.swift b/Koin/Presentation/Home/Category/CategoryHostingController.swift index 1a39c77a..5d6334d1 100644 --- a/Koin/Presentation/Home/Category/CategoryHostingController.swift +++ b/Koin/Presentation/Home/Category/CategoryHostingController.swift @@ -50,6 +50,8 @@ final class CategoryHostingController: UIHostingController, Hostin navigationController?.pushViewController(makeLandViewController(), animated: true) case .showBusiness: presentBusiness() + case .showRecruit: + showRecruit() case .showLoginToast: showToastMessageWithButton( @@ -204,4 +206,14 @@ extension CategoryHostingController { present(safariViewController, animated: true) } } + + private func showRecruit() { + guard var components = URLComponents(string: Bundle.main.baseUrl), + let host = components.host else { return } + components.host = host.hasPrefix("api.") ? String(host.dropFirst("api.".count)) : host + components.path = "/team" + guard let url = components.url else { return } + let safariViewController = SFSafariViewController(url: url) + present(safariViewController, animated: true) + } } diff --git a/Koin/Presentation/Home/Category/CategoryView.swift b/Koin/Presentation/Home/Category/CategoryView.swift index e2bfe625..eb4bcc4d 100644 --- a/Koin/Presentation/Home/Category/CategoryView.swift +++ b/Koin/Presentation/Home/Category/CategoryView.swift @@ -24,6 +24,7 @@ struct CategoryView: ActionBindableView { case showChatList case showLand case showBusiness + case showRecruit case showLoginToast } @@ -51,8 +52,8 @@ struct CategoryView: ActionBindableView { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 12) { - CategoryFeaturedButton(item: .timetable) { - didTapItem(.timetable) + CategoryFeaturedButton(item: .recruit) { + didTapItem(.recruit) } CategoryFeaturedButton(item: .lostItem) { didTapItem(.lostItem) @@ -64,7 +65,8 @@ struct CategoryView: ActionBindableView { .facility, .department, .dining, - .shop + .shop, + .timetable ], action: { item in didTapItem(item) @@ -86,7 +88,7 @@ struct CategoryView: ActionBindableView { items: [ .chat, .land, - .business, + .business ], action: { item in didTapItem(item) @@ -150,13 +152,15 @@ private extension CategoryView { return .showLand case .business: return .showBusiness + case .recruit: + return .showRecruit } } private func loggingInfo(for action: Action) -> (label: EventParameter.EventLabel.Campus, value: String)? { switch action { - case .showTimetable: - return (.categoryTimetable, "시간표") + case .showRecruit: + return nil // TODO: 로깅 추가 case .showLostItem: return (.categoryLostProperty, "분실물") case .showFacility: @@ -167,6 +171,8 @@ private extension CategoryView { return (.categoryCampus, "식단") case .showShop: return (.categoryCampus, "주변상점") + case .showTimetable: + return (.categoryTimetable, "시간표") // TODO: 로깅 수정 case .showBusTimetable: return (.categoryTransportation, "버스 시간표") case .showBusRoute: diff --git a/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/Contents.json b/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/Contents.json new file mode 100644 index 00000000..b26f0aac --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "categoryRecruit.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/categoryRecruit.svg b/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/categoryRecruit.svg new file mode 100644 index 00000000..2e945dbb --- /dev/null +++ b/Koin/Resources/Assets.xcassets/Home/categoryRecruit.imageset/categoryRecruit.svg @@ -0,0 +1,8 @@ + + + + + + + + From dd6d7f5ed98f8965ebec65a2382e97f57c3a82dd Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Mon, 14 Sep 2026 20:58:09 +0900 Subject: [PATCH 101/101] 5.2.0 --- koin.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index e7bfe0f3..d9971c4a 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -6999,7 +6999,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 444; + CURRENT_PROJECT_VERSION = 446; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -7018,7 +7018,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.1.2; + MARKETING_VERSION = 5.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin.stage; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -7034,7 +7034,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 444; + CURRENT_PROJECT_VERSION = 446; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -7053,7 +7053,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.1.2; + MARKETING_VERSION = 5.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES;