diff --git a/.github/.gitMessage.md b/.github/.gitMessage.md new file mode 100644 index 0000000..c2d3596 --- /dev/null +++ b/.github/.gitMessage.md @@ -0,0 +1,31 @@ +# Commit 규칙 +> 커밋 제목은 최대 50자 입력
+본문은 한 줄 최대 72자 입력
+Commit 메세지
+ +🪛[chore]: 코드 수정, 내부 파일 수정.
+✨[feat]: 새로운 기능 구현.
+🎨[style]: 스타일 관련 기능.(코드의 구조/형태 개선)
+➕[add]: Feat 이외의 부수적인 코드 추가, 라이브러리 추가
+🔧[file]: 새로운 파일 생성, 삭제 시
+🐛[fix]: 버그, 오류 해결.
+🔥[del]: 쓸모없는 코드/파일 삭제.
+📝[docs]: README나 WIKI 등의 문서 개정.
+💄[mod]: storyboard 파일,UI 수정한 경우.
+✏️[correct]: 주로 문법의 오류나 타입의 변경, 이름 변경 등에 사용합니다.
+🚚[move]: 프로젝트 내 파일이나 코드(리소스)의 이동.
+⏪️[rename]: 파일 이름 변경이 있을 때 사용합니다.
+⚡️[improve]: 향상이 있을 때 사용합니다.
+♻️[refactor]: 전면 수정이 있을 때 사용합니다.
+🔀[merge]: 다른브렌치를 merge 할 때 사용합니다.
+✅ [test]: 테스트 코드를 작성할 때 사용합니다.
+ +
+ +### Commit Body 규칙 +> 제목 끝에 마침표(.) 금지
+한글로 작성
+브랜치 이름 규칙 + +- `STEP1`, `STEP2`, `STEP3` + diff --git a/.github/Convention/Common.md b/.github/Convention/Common.md new file mode 100644 index 0000000..d6fb58c --- /dev/null +++ b/.github/Convention/Common.md @@ -0,0 +1,572 @@ +# 공통 + +## 목차 +[한줄 최대 길이](#한-줄-최대-길이)
+[들여쓰기 규칙](#들여쓰기-규칙)
+[Guard 규칙](#guard-규칙)
+[final 규칙](#final-규칙)
+[접근자 규칙](#접근자-규칙)
+[함수정의 줄내림 규칙](#함수정의-줄내림-규칙)
+[Enum 줄내림 규칙](#enum-줄내림-규칙)
+[조건문 줄내림 규칙](#조건문-줄내림-규칙)
+[연산자 줄내림 규칙](#연산자-줄내림-규칙)
+[삼항연산자 규칙](#삼항연산자-규칙)
+[self 규칙](#self-규칙)
+[Array 선언 규칙](#array-선언-규칙)
+[메모리 관리 규칙](#메모리-관리-규칙)
+[클로저 사용 규칙](#클로저-사용-규칙)
+[Unwrapping 규칙](#unwrapping-규칙)
+[자주 사용되는 값 체크에 확장 변수 사용하기](#자주-사용되는-값-체크에-확장-변수-사용하기)
+[상수 선언 규칙](#상수-선언-규칙)
+[RxSwift 스케쥴러 지정 규칙](#rxswift-스케쥴러-지정-규칙)
+[VIPER 모듈 사이의 콜백 전달 규칙](#viper-모듈-사이의-콜백-전달-규칙)
+ +## 코드 컨벤션 + +### 한 줄 최대 길이 + +- 한 줄은 최대 120자를 넘지 않도록 합니다. +- Xcode에서 **Preferences -> Text Editing -> Display -> Page guide at column** 부분을 120로 설정해서 사용해주세요. + +### 들여쓰기 규칙 + +- Indent는 2칸으로 지정합니다. +- Xcode에서 **Preferences -> Text Editing -> Display -> Line wrapping** 부분을 2 spaces로 설정해서 사용해주세요. + +### Guard 규칙 + +- `guard`는 코드에서 분기를 빨리 끝낼 때, 과도한 조건문 복잡도가 생길 때 사용합니다. +- `guard ~ else` 문이 한줄에 써진다면, 한줄로 사용합니다. +- `Swift 5.7`부터 Shorthand syntax 사용이 가능하여 Optional Binding에 단축 구문을 사용합니다. + + ```swift + // Preferred + var number: Int? + guard let number else { return } + + // Not Preffered + var number: Int? + guard let number = number else { return } + ``` + +- `guard`의 condition이 하나이고, `else`가 여러줄이라면 다음과 같이 사용합니다. + + ```swift + guard let number else { + .... + return + } + ``` + +- `guard`의 condition이 여러개라면 다음과 같이 사용합니다. ( 단, 최대 한줄로 작성이 가능한 경우는 한줄로 작성합니다. ) + + ```swift + // Preferred + guard let number, number > 0 else { + .... + return + } + + guard + let name, + let number, + isFavorited + else { return } + + // Not Preffered + guard let name, + let number, + isFavorited + else { + return + } + ``` + +- `guard`가 끝난 이후, 한 줄 띄우고 코드를 작성합니다. + + ```swift + guard let number else { return } + + if number > 0 { + ... + } else { + ... + } + ``` + +- `guard`가 중간에 오는 경우, 위 아래로 한줄씩 띄우고 코드를 작성합니다. + + ```swift + let number: Int? = 0 + + guard let number else { return } + + if number > 0 { + ... + } else { + ... + } + ``` + + +### final 규칙 + +- 더이상 상속이 일어나지 않는 class는 `final`을 붙여서 명시해줍니다. + + ```swift + final class Channel { + ... + } + ``` + +### 접근자 규칙 + +- class 내부에서만 쓰이는 변수는 `private`으로 명시해줍니다. +- `fileprivate`는 필요한 경우가 아니면 피하고, `private`으로 써줍니다. + + ```swift + final class Channel { + private var number = 0 + ... + } + +### 함수정의 줄내림 규칙 + +- 함수 정의가 길 경우 다음과 같이 줄내림 합니다. + + ```swift + // Preferred + func changeChannel( + name: String, + number: Int, + isFavorited: Bool + ) { + ... + } + + // Not Preferred + func changeChannel( + name: String, + number: Int, + isFavorited: Bool) { + ... + } + ``` + +### Enum 줄내림 규칙 + +- 모든 `case` 내의 코드가 한줄이고 return 문이라면 붙여서 사용합니다. + ```swift + // Preferred + switch channelNumber { + case .main: return 0 + case .sub: return 1 + } + + // Not Preferred + switch channelNumber { + case .main: + return 0 + case .sub: + return 1 + } + ``` + +- 단, switch 문 case 내 로직이 포함되는 경우 아래와 같이 줄내림 후 한 줄 띄우고 다음 case를 작성합니다. + ```swift + // Preferred + switch checkChannel { + case .main: + guard channel.number != 0 else { return } + + channel.changeChannel(number: 0) + + case .sub: + channel.changeChannel(number: 1) + } + + // Not Preferred + switch checkChannel { + case .main: + guard channel.number != 0 else { return } + + channel.changeChannel(number: 0) + case .sub: + channel.changeChannel(number: 1) + } + ``` + +### 조건문 줄내림 규칙 + +- `if` 나 `guard`에서 조건문이 여러개인 경우 줄이 길면 다음과 같이 줄내림 해줍니다. + - `,`로 나뉠 땐 인덴트를 한번 넣어줍니다. + - `||`, `&&`는 한줄 내에서 줄내림 된거처럼 한번 더 인덴트를 넣어줍니다. + + ```swift + if let currentNumber, + let number = channel.number, + currentNumber > 0 + || number > 0 + || currentNumber == number, + let isFavorited = isFavorited { + .... + } + + guard + let currentNumber, + let number = channel.number, + currentNumber > 0 + || number > 0 + || currentNumber == number, + let isFavorited = isFavorited { + .... + } + + ``` + +### 연산자 줄내림 규칙 + +- `+`, `||`, `&&` 등의 연산자에 대한 줄내림은 연산자를 같이 내려줍니다. + + ```swift + // Preferred + if number > 0 + || isFavorited == ture { + ... + } + + var name = "this" + + " is" + + " main" + + let isSuccess = !channel.isEmpty + && isFavorited + && channel.number > 0 + + // Not Preferred + if number > 0 || + isFavorited == ture { + ... + } + + var name = "this" + + " is" + + " main" + + let isSuccess = !channel.isEmpty && + isFavorited && + channel.number > 5 + ``` + +### 삼항연산자 규칙 + +- `if ~ else`로 묶인 `return` 또는 값대입인 경우 삼항연산자로 줄일 수 있으면 줄여줍니다. + + ```swift + // Preferred + return number == 0 ? .main : .sub + + var channel = number == 0 ? .main : .sub + + // Not Preferred + if number == 0 { + return .main + } else { + return .sub + } + + var result: Channel + if number > 0 { + result = .main + } else { + result = .sub + } + ``` + +- 줄내림이 필요한 경우 `?`를 기준으로 내려줍니다. + + ```swift + // Preferred + return number == 0 + ? .main : .sub + + // Not Preferred + return number == 0 ? + .main : .sub + ``` + +- 조건에 따른 단순 분기일 때는 삼항연산자를 피해줍니다. + + ```swift + // Preferred + if isFavorited { + channel.turnOn() + } else { + channel.turnOff() + } + + // Not Preferred + isFavorited ? channel.turnOn() : channel.turnOff() + ``` + +- 삼항 연산자가 너무 길어질 경우 가독성을 위해 분리해줍니다. + + ```swift + // Preferred + let firstCondition = c == 2 ? d : e + let secondCondition = b == 1 : firstCondition : f + return test == 0 ? a : secondCondition + // 또는 적절히 if를 나눠서 구현해준다. + + // Not Preferred + return test == 0 + ? a + : b == 1 + ? c == 2 + : d + : e + : f + ``` + +### self 규칙 + +- 클래스와 구조체 내부에서는 `self`를 명시적으로 표시해줍니다. + +### Array 선언 규칙 + +- Array를 선언할 때는 다음과 같은 포맷을 지향합니다. + + ```swift + // Preferred + var managers: [Manager] = [] + var counts: [String: Int] = [:] + + // Not Preferred + var managers = [Manager]() + var counts = [String: Int]() + ``` + +### 메모리 관리 규칙 + +- Retain cycle이 발생하지 않도록 `weak self`를 이용합니다. 필요하다면 `guard let self = self else`를 통해서 unwrapping을 해줍니다. + + ```swift + self.closePopup() { [weak self] _ in + guard let self else { return } + + self.popAllController() + } + ``` + +### 클로저 사용 규칙 + +- 클로저의 파라미터는 괄호를 빼고 사용합니다. + + ```swift + // Preferred + { manager, user in + ... + } + + // Not Preferred + { (manager, user) in + ... + } + ``` + +- 클로져가 파라미터중 하나만 있고, 마지막에 항목이 클로져라면 파라미터 명을 생략해줍니다. + ```swift + // Preferred + UIView.animate(withDuration: 0.25) { + ... + } + + // Not Preferred + UIView.animate(withDuration: 0.25, animations: { () -> Void in + ... + }) + ``` +- 파라미터의 타입 정의는 가능하다면 생략해줍니다. + + ```swift + // Preferred + { manager, user in + ... + } + + // Not Preferred + { (manager: Manager, user: User) -> Void in + ... + } + ``` + +- 클로져 밖의 괄호는 가능한 생략해 줍니다. + ```swift + // Preferred + self.channelView.snp.makeConstraints { + $0.leading.equalToSuperview().inset(xMargin) + $0.trailing.equalToSuperview().inset(xMargin) + } + + // Not Preferred + self.channelView.snp.makeConstraints ({ + $0.left.equalToSuperview().offset(-xMargin) + $0.right.equalToSuperview().offset(xMargin) + }) + ``` + +### unwrapping 규칙 + +- 최대한 force unwrapping은 피해줍니다. 옵셔널(`?`)의 경우는 optional chaining 등으로 풀어서 사용해주시고 `!`는 최대한 피해줍니다. + + ```swift + // Preferred + func getResultText(with text: String?) -> String { + if let resultText = text { + return resultText + } + ... + } + + // Not Preferred + func getResultText(with text: String?) -> String { + return text! + ... + } + ``` + +### 자주 사용되는 값 체크에 확장 변수 사용하기 + +- `nil`이나 `0`과 같은 값을 체크할 때 정의된 확장 변수가 있다면 등호/부등호를 사용하는 대신 해당 확장 변수를 사용합니다. + + ```swift + // Preferred + if optionalValue.isNil { ... } + if optionalValue.isNotNil { ... } + if numberValue.isZero { ... } + if optionalBoolValue.beTrue { ... } + if optionalBoolValue.beFalse { ... } + + // Not Preferred + if optionalValue == nil { ... } + if optionalValue != nil { ... } + if numberValue == 0 { ... } + if optionalBoolValue == true { ... } + if optionalBoolValue == false { ... } + ``` + +### 상수 선언 규칙 + +- 코드 상단에 `private`로 정의하여 일반적인 상수를 단순히 정의할때는 `struct` 대신 `enum`을 사용해줍니다. + Generic 사용 시 class 내부에서 static 선언이 어렵기 때문에 class 밖에서 사용합니다. + + - Snapkit 등에서 autolayout을 설정할 때 상수는 위쪽에 `Metric`으로 빼줍니다. + - 여러번 쓰이는 폰트는 `Font`로 빼줍니다 + - 테이블뷰 등의 Section 관련은 `Section`으로 빼줍니다. + - 테이블뷰 등의 row 관련은 `Row`로 빼줍니다. + - 그외 내부적으로 쓰이는 상수는 `Constant`로 빼줍니다. + + ```swift + private enum Metric { + static let avatarLength = 3.f + ... + } + + private enum Font { + static let titleLabel = UIFont.boldSystemFont(ofSize: 14) + ... + } + + private enum Section { + static let managers = 0 + static let users = 1 + ... + } + + private enum Row { + static let name = 0 // managers 섹션의 0번째 row + static let username = 0 // users 섹션의 0번째 row + ... + } + + private enum Constant { + static let maxLinesWithOnlyText = 2 + ... + } + ``` +- Localize 상수는 기존 상수 규칙과 다르게, enum - case로 정리합니다. 규칙은 다음과 같습니다. + ```swift + private enum Localized { + case leavedThread + case managersCount(String) + ... + + var rawValue: String { + switch self { + case .leaveThread: return "thread.header.leave_thread".localized + case .managersCount(let count): return "team_chat_info.manager_list.title".localized(with: count) + ... + } + } + } + ``` + +### RxSwift 스케쥴러 지정 규칙 + +- `Observable`에 대해서 `subscribe(on:)`과 `observe(on:)` 함수를 호출해 어떤 어떤 스케쥴러에서 작동할지 지정할 수 있는데, 이를 `Observable`을 생성하는 곳이 아닌 생성된 `Observable`을 사용하는 곳에서 지정합니다. +- 자세한 내용은 ["SubscribeOn / ObserveOn 논의 정리"](https://www.notion.so/channelio/SubscribeOn-ObserveOn-dfd918eee039412ea3ac9d72d3da08fd?pvs=4)를 확인해주세요. + + ```swift + // Preferred + func someObservable() -> Observable { ... } + + someObservable() + .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background)) + .observe(on: MainScheduler.instance) + .subscribe { _ in + ... + } + + // Not Preferred + Observable + .create { ... } + .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background)) + .observe(on: MainScheduler.instance) + ... + ``` + +### VIPER 모듈 사이의 콜백 전달 규칙 + +- VIPER 모듈이 다른 VIPER 모듈을 생성하면서 생성한 모듈로부터 어떤 동작이 완료되었다는 콜백을 받고 싶을 때, 이를 클로져를 사용하기보다는 `PublishRelay`, `PublishSubject`와 같은 옵저버 타입을 전달하도록 합니다. + + ```swift + // Preferred + extension CreateValueRouter { + static func createModule(createValueSignal: PublishRelay) { + ... + } + } + + class CreateValueModuleClass { + var createValueSignal: PublishRelay? + ... + func valueCreated(_ value: Value) { + self.createValueSignal?.accept(value) + } + } + + // Not Preferred + extension CreateValueRouter { + static func createModule(valueCreated: (Value) -> Void) { + ... + } + } + + class CreateValueModuleClass { + var valueCreated: ((Value) -> Void)? + ... + func valueCreated(_ value: Value) { + self.valueCreated?(value) + } + } + ``` diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 0000000..4cc7673 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,15 @@ +--- +name: ISSUE_TEMPLATE +about: 이슈탬플릿 +title: "✨[feat]:" +labels: '' +assignees: '' + +--- + +# 목적 + +# 작업상세내역 +- [] + +# 참고사항 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1eefbaf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,50 @@ +## 🔗 관련 이슈 + +- 관련 이슈: # + +--- + +## ✨ 작업 내용 + +- +- +- + +--- + +## 📸 Showcase + +| 변경 전 | 변경 후 | +|--------|--------| +| 이미지 | 이미지 | + +> 📌 이미지가 없다면 이 섹션은 생략해도 됩니다. + +--- + +## 📝 참고 사항 + +- + +## Motivation 🥳 (코드를 추가/변경하게 된 이유) + +## Key Changes 🔥 (주요 구현/변경 사항) + +## To Reviewers 🙏 (리뷰어에게 전달하고 싶은 말) + +## Reference 🔗 + + +## Close Issues 🔒 (닫을 Issue) +Close #No. + +## Checklist +- [ ] 브랜치를 가져와 작업한 경우 이전 브랜치에 PR을 보냈는지 확인 +- [ ] 빌드를 위해 SceneDelegate 수정한 것 PR로 올리지 않았는지 확인 +- [ ] 필요없는 주석, 프린트문 제거했는지 확인 +- [ ] 컨벤션 지켰는지 확인 +- [ ] final, private 제대로 넣었는지 확인 +- [ ] 다양한 디바이스에 레이아웃이 대응되는지 확인 + - [ ] iPhone SE + - [ ] iPhone 13 + - [ ] iPhone 13 Pro Max diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..71a1396 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,22 @@ +addReviewers: true + +# Set to true to add assignees to pull requests +addAssignees: author + +# A list of reviewers to be added to pull requests (GitHub user name) +reviewers: + - minneee + - Peter1119 + - Roy-wonji + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +numberOfReviewers: 2 + +# A number of assignees to add to the pull request +# Set to 0 to add all of the assignees. +# Uses numberOfReviewers if unset. +numberOfAssignees: 2 + +skipKeywords: + - wip diff --git a/.github/workflows/AutoAssign.yml b/.github/workflows/AutoAssign.yml new file mode 100644 index 0000000..e29cbd3 --- /dev/null +++ b/.github/workflows/AutoAssign.yml @@ -0,0 +1,12 @@ +name: 'Auto Assign' +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + add-reviews: + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@v2.0.0 + with: + configuration-path: '.github/auto_assign.yml' # Only needed if you use something other than .github/auto_assign.yml diff --git a/MovieBooking.xcodeproj/project.pbxproj b/MovieBooking.xcodeproj/project.pbxproj index 544918b..532e300 100644 --- a/MovieBooking.xcodeproj/project.pbxproj +++ b/MovieBooking.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 7F6F8B432E9D27850046FA0E /* Auth in Frameworks */ = {isa = PBXBuildFile; productRef = 7F6F8B422E9D27850046FA0E /* Auth */; }; + 7F6F8B452E9D27850046FA0E /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 7F6F8B442E9D27850046FA0E /* Supabase */; }; + 7F6F8B482E9D279B0046FA0E /* WeaveDI in Frameworks */ = {isa = PBXBuildFile; productRef = 7F6F8B472E9D279B0046FA0E /* WeaveDI */; }; F24B91D52E9D1A5800EF7944 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = F24B91D42E9D1A5800EF7944 /* ComposableArchitecture */; }; F24B91D82E9D1A8B00EF7944 /* TCACoordinators in Frameworks */ = {isa = PBXBuildFile; productRef = F24B91D72E9D1A8B00EF7944 /* TCACoordinators */; }; /* End PBXBuildFile section */ @@ -34,9 +37,22 @@ F24B90012E9D0D2C00EF7944 /* MovieBookingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovieBookingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 7F6F8C092E9DCCD60046FA0E /* Exceptions for "MovieBooking" folder in "MovieBooking" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resources/Info.plist, + ); + target = F24B8FE92E9D0D2900EF7944 /* MovieBooking */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ F24B8FEC2E9D0D2900EF7944 /* MovieBooking */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7F6F8C092E9DCCD60046FA0E /* Exceptions for "MovieBooking" folder in "MovieBooking" target */, + ); path = MovieBooking; sourceTree = ""; }; @@ -57,8 +73,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7F6F8B452E9D27850046FA0E /* Supabase in Frameworks */, + 7F6F8B482E9D279B0046FA0E /* WeaveDI in Frameworks */, F24B91D82E9D1A8B00EF7944 /* TCACoordinators in Frameworks */, F24B91D52E9D1A5800EF7944 /* ComposableArchitecture in Frameworks */, + 7F6F8B432E9D27850046FA0E /* Auth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -121,6 +140,9 @@ packageProductDependencies = ( F24B91D42E9D1A5800EF7944 /* ComposableArchitecture */, F24B91D72E9D1A8B00EF7944 /* TCACoordinators */, + 7F6F8B422E9D27850046FA0E /* Auth */, + 7F6F8B442E9D27850046FA0E /* Supabase */, + 7F6F8B472E9D279B0046FA0E /* WeaveDI */, ); productName = MovieBooking; productReference = F24B8FEA2E9D0D2900EF7944 /* MovieBooking.app */; @@ -207,6 +229,8 @@ packageReferences = ( F24B91D32E9D1A5800EF7944 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, F24B91D62E9D1A8B00EF7944 /* XCRemoteSwiftPackageReference "TCACoordinators" */, + 7F6F8B412E9D27850046FA0E /* XCRemoteSwiftPackageReference "supabase-swift" */, + 7F6F8B462E9D279B0046FA0E /* XCRemoteSwiftPackageReference "WeaveDI" */, ); preferredProjectObjectVersion = 77; productRefGroup = F24B8FEB2E9D0D2900EF7944 /* Products */; @@ -284,6 +308,8 @@ /* Begin XCBuildConfiguration section */ F24B90092E9D0D2C00EF7944 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Dev.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -348,6 +374,8 @@ }; F24B900A2E9D0D2C00EF7944 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Realse.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -405,14 +433,18 @@ }; F24B900C2E9D0D2C00EF7944 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Dev.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MovieBooking/Resources/MovieBooking.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = URUB4S4795; + DEVELOPMENT_TEAM = N94CS4N6VR; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieBooking/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -437,14 +469,18 @@ }; F24B900D2E9D0D2C00EF7944 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Realse.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MovieBooking/Resources/MovieBooking.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = URUB4S4795; + DEVELOPMENT_TEAM = N94CS4N6VR; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieBooking/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -469,6 +505,8 @@ }; F24B900F2E9D0D2C00EF7944 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Dev.xcconfig; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -491,6 +529,8 @@ }; F24B90102E9D0D2C00EF7944 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Realse.xcconfig; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -513,6 +553,8 @@ }; F24B90122E9D0D2C00EF7944 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Dev.xcconfig; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -533,6 +575,8 @@ }; F24B90132E9D0D2C00EF7944 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = F24B8FEC2E9D0D2900EF7944 /* MovieBooking */; + baseConfigurationReferenceRelativePath = Config/Realse.xcconfig; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -593,6 +637,22 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 7F6F8B412E9D27850046FA0E /* XCRemoteSwiftPackageReference "supabase-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/supabase/supabase-swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.5.1; + }; + }; + 7F6F8B462E9D279B0046FA0E /* XCRemoteSwiftPackageReference "WeaveDI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Roy-wonji/WeaveDI"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.3.1; + }; + }; F24B91D32E9D1A5800EF7944 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; @@ -612,6 +672,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 7F6F8B422E9D27850046FA0E /* Auth */ = { + isa = XCSwiftPackageProductDependency; + package = 7F6F8B412E9D27850046FA0E /* XCRemoteSwiftPackageReference "supabase-swift" */; + productName = Auth; + }; + 7F6F8B442E9D27850046FA0E /* Supabase */ = { + isa = XCSwiftPackageProductDependency; + package = 7F6F8B412E9D27850046FA0E /* XCRemoteSwiftPackageReference "supabase-swift" */; + productName = Supabase; + }; + 7F6F8B472E9D279B0046FA0E /* WeaveDI */ = { + isa = XCSwiftPackageProductDependency; + package = 7F6F8B462E9D279B0046FA0E /* XCRemoteSwiftPackageReference "WeaveDI" */; + productName = WeaveDI; + }; F24B91D42E9D1A5800EF7944 /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; package = F24B91D32E9D1A5800EF7944 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; diff --git a/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc8bc82..eeb7cba 100644 --- a/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0c1eca297b548e9ec69bfb69a1bc5aa2267a470775adc7e482e6d899e20ee3cd", + "originHash" : "561b1586e0fb6f910745bf5bfd05cfc1cf6da60209bfc8223e7d3958750b34c4", "pins" : [ { "identity" : "combine-schedulers", @@ -19,6 +19,33 @@ "version" : "0.4.1" } }, + { + "identity" : "logmacro", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Roy-wonji/LogMacro.git", + "state" : { + "revision" : "593b210346cf7145074c2d94fa4206bbc70198e0", + "version" : "1.1.1" + } + }, + { + "identity" : "supabase-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/supabase/supabase-swift.git", + "state" : { + "revision" : "21425be5a493bb24bfde51808ccfa82a56111430", + "version" : "2.34.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", + "version" : "1.4.0" + } + }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", @@ -64,6 +91,15 @@ "version" : "1.3.2" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc", + "version" : "3.15.1" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", @@ -76,12 +112,21 @@ { "identity" : "swift-dependencies", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", + "location" : "https://github.com/pointfreeco/swift-dependencies.git", "state" : { "revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc", "version" : "1.10.0" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03", + "version" : "1.4.0" + } + }, { "identity" : "swift-identified-collections", "kind" : "remoteSourceControl", @@ -121,10 +166,10 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", + "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "4799286537280063c85a32f09884cfbca301b1a1", - "version" : "602.0.0" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -136,6 +181,15 @@ "version" : "0.13.0" } }, + { + "identity" : "weavedi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Roy-wonji/WeaveDI", + "state" : { + "revision" : "89728f122d41633d8ebac182fc15040065e2492c", + "version" : "3.3.1" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/MovieBooking/App/Application/AppDelegate.swift b/MovieBooking/App/Application/AppDelegate.swift new file mode 100644 index 0000000..9bb2e2f --- /dev/null +++ b/MovieBooking/App/Application/AppDelegate.swift @@ -0,0 +1,21 @@ +// +// AppDelegate.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import UIKit +import WeaveDI + +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + WeaveDI.Container.bootstrapInTask { _ in + await AppDIManager.shared.registerDefaultDependencies() + } + + return true + } +} + diff --git a/MovieBooking/App/Application/MovieBookingApp.swift b/MovieBooking/App/Application/MovieBookingApp.swift new file mode 100644 index 0000000..12d9a4d --- /dev/null +++ b/MovieBooking/App/Application/MovieBookingApp.swift @@ -0,0 +1,30 @@ +// +// MovieBookingApp.swift +// MovieBooking +// +// Created by 김민희 on 10/13/25. +// + +import SwiftUI +import ComposableArchitecture + +@main +struct MovieBookingApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + init() { + + } + + var body: some Scene { + WindowGroup { + let store = Store(initialState: AppReducer.State()) { + AppReducer() + ._printChanges() + ._printChanges(.actionLabels) + } + + AppView(store: store) + } + } +} diff --git a/MovieBooking/App/Di/DIRegistry.swift b/MovieBooking/App/Di/DIRegistry.swift new file mode 100644 index 0000000..8267346 --- /dev/null +++ b/MovieBooking/App/Di/DIRegistry.swift @@ -0,0 +1,36 @@ +// +// DIRegistry.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import WeaveDI + +/// 모든 의존성을 자동으로 등록하는 레지스트리 +extension WeaveDI.Container { + private static let helper = RegisterModule() + + /// Repository 등록 + static func registerRepositories() async { + let repositories: [Module] = [ + + ] + + await repositories.asyncForEach { module in + await module.register() + } + } + + /// UseCase 등록 + static func registerUseCases() async { + + let useCases: [Module] = [ + + ] + + await useCases.asyncForEach { module in + await module.register() + } + } +} diff --git a/MovieBooking/App/Di/Extension+AppDIContainer.swift b/MovieBooking/App/Di/Extension+AppDIContainer.swift new file mode 100644 index 0000000..eebaf12 --- /dev/null +++ b/MovieBooking/App/Di/Extension+AppDIContainer.swift @@ -0,0 +1,21 @@ +// +// Extension+AppDIContainer.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import WeaveDI + +extension AppWeaveDI.Container { + @DIContainerActor + func registerDefaultDependencies() async { + await registerDependencies(logLevel: .errors) { container in + // Repository 먼저 등록 + let factory = ModuleFactoryManager() + + await factory.registerAll(to: container) + } + } +} + diff --git a/MovieBooking/App/MovieBookingApp.swift b/MovieBooking/App/MovieBookingApp.swift deleted file mode 100644 index a0dfaf8..0000000 --- a/MovieBooking/App/MovieBookingApp.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// MovieBookingApp.swift -// MovieBooking -// -// Created by 김민희 on 10/13/25. -// - -import SwiftUI - -@main -struct MovieBookingApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/MovieBooking/App/Reducer/AppReducer.swift b/MovieBooking/App/Reducer/AppReducer.swift new file mode 100644 index 0000000..65ae415 --- /dev/null +++ b/MovieBooking/App/Reducer/AppReducer.swift @@ -0,0 +1,111 @@ +// +// AppReducer.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import ComposableArchitecture + +@Reducer +struct AppReducer { + + @ObservableState + enum State { + case splash(Splash.State) + case auth(AuthCoordinator.State) + + + + init() { + self = .splash(.init()) + } + } + + enum Action: ViewAction { + case view(View) + case scope(ScopeAction) + } + + @CasePathable + enum View { + case presentAuth + case presentMain + } + + + @CasePathable + enum ScopeAction { + case splash(Splash.Action) + case auth(AuthCoordinator.Action) + } + + @Dependency(\.continuousClock) var clock + + public var body: some Reducer { + Reduce { state, action in + switch action { + case .view(let viewAction): + return handleViewAction(&state, action: viewAction) + + case .scope(let scopeAction): + return handleScopeAction(&state, action: scopeAction) + } + } + .ifCaseLet(\.splash, action: \.scope.splash) { + Splash() + } + .ifCaseLet(\.auth, action: \.scope.auth) { + AuthCoordinator() + } + } +} + +extension AppReducer { + func handleViewAction( + _ state: inout State, + action: View + ) -> Effect { + switch action { + // MARK: - 로그인 화면으로 + case .presentAuth: + state = .auth(.init()) + return .none + + case .presentMain: + return .none + + } + } + + + func handleScopeAction( + _ state: inout State, + action: ScopeAction + ) -> Effect { + switch action { + case .splash(.navigation(.presentLogin)): + return .run { send in + try await clock.sleep(for: .seconds(1)) + await send(.view(.presentAuth)) + } + + case .splash(.navigation(.presentMain)): + return .run { send in + try await clock.sleep(for: .seconds(1)) + await send(.view(.presentMain)) + } + + +// case .auth(.navigation(.presentMain)): +// return .send(.view(.presentMain)) +// +// case .auth(.navigation(.presentMain)): +// return .send(.view(.presentMain)) + + + default: + return .none + } + } +} diff --git a/MovieBooking/App/View/AppView.swift b/MovieBooking/App/View/AppView.swift new file mode 100644 index 0000000..1a72cea --- /dev/null +++ b/MovieBooking/App/View/AppView.swift @@ -0,0 +1,30 @@ +// +// AppView.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +import ComposableArchitecture + +struct AppView: View { + @Bindable var store: StoreOf + + var body: some View { + SwitchStore(store) { state in + switch state { + case .splash: + if let store = store.scope(state: \.splash, action: \.scope.splash) { + SplashView(store: store) + } + + case .auth: + if let store = store.scope(state: \.auth, action: \.scope.auth) { + AuthCoordinatorView(store: store) + } + } + } + } +} diff --git a/MovieBooking/Config/Dev.xcconfig b/MovieBooking/Config/Dev.xcconfig new file mode 100644 index 0000000..901b4ac --- /dev/null +++ b/MovieBooking/Config/Dev.xcconfig @@ -0,0 +1,14 @@ +// +// Shared.xcconfig +// GoGo +// +// Created by ha sungyong on 10/29/24. +// + +#include "./Shared.xcconfig" + +OTHER_SWIFT_FLAGS[config=STAGE][sdk=*] = $(inherited) -DDEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS = STAGE +SUPERBASE_URL=depahiavjicplpqpcbwd.supabase.co +SUPERBASE_KEY=sb_publishable_DnDwIbBsCHselcdXZrgt7A_OiIO7BAd + diff --git a/MovieBooking/Config/Realse.xcconfig b/MovieBooking/Config/Realse.xcconfig new file mode 100644 index 0000000..1c1a15a --- /dev/null +++ b/MovieBooking/Config/Realse.xcconfig @@ -0,0 +1,14 @@ +// +// Shared.xcconfig +// GoGo +// +// Created by ha sungyong on 10/29/24. +// + +#include "./Shared.xcconfig" + +OTHER_SWIFT_FLAGS[config=PROD][sdk=*] = $(inherited) -PROD +SWIFT_ACTIVE_COMPILATION_CONDITIONS = PROD +SUPERBASE_URL=depahiavjicplpqpcbwd.supabase.co +SUPERBASE_KEY=sb_publishable_DnDwIbBsCHselcdXZrgt7A_OiIO7BAd + diff --git a/MovieBooking/Config/Shared.xcconfig b/MovieBooking/Config/Shared.xcconfig new file mode 100644 index 0000000..5ad0be4 --- /dev/null +++ b/MovieBooking/Config/Shared.xcconfig @@ -0,0 +1,10 @@ +// +// Shared.xcconfig +// GoGo +// +// Created by ha sungyong on 10/29/24. +// + +MARKETING_VERSION=1.0.0 +CURRENT_PROJECT_VERSION=20 + diff --git a/MovieBooking/DesignSystem/Color/Colors.swift b/MovieBooking/DesignSystem/Color/Colors.swift new file mode 100644 index 0000000..9697496 --- /dev/null +++ b/MovieBooking/DesignSystem/Color/Colors.swift @@ -0,0 +1,14 @@ +// +// Colors.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +public extension ShapeStyle where Self == Color { + static var basicPurple: Color { .init(hex: "503396") } + static var violet: Color { .init(hex: "9333EA") } + static var brightYellow: Color { .init(hex: "FEE500") } +} diff --git a/MovieBooking/DesignSystem/Color/Extension+Color.swift b/MovieBooking/DesignSystem/Color/Extension+Color.swift new file mode 100644 index 0000000..3dd907f --- /dev/null +++ b/MovieBooking/DesignSystem/Color/Extension+Color.swift @@ -0,0 +1,23 @@ +// +// Extension+Color.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +public extension Color { + init(hex: String) { + let scanner = Scanner(string: hex) + _ = scanner.scanString("#") + + var rgb: UInt64 = 0 + scanner.scanHexInt64(&rgb) + + let r = Double((rgb >> 16) & 0xFF) / 255.0 + let g = Double((rgb >> 8) & 0xFF) / 255.0 + let b = Double((rgb >> 0) & 0xFF) / 255.0 + self.init(red: r, green: g, blue: b) + } +} diff --git a/MovieBooking/DesignSystem/Font/PretendardFont.swift b/MovieBooking/DesignSystem/Font/PretendardFont.swift new file mode 100644 index 0000000..b710d99 --- /dev/null +++ b/MovieBooking/DesignSystem/Font/PretendardFont.swift @@ -0,0 +1,37 @@ +// +// PretendardFont.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +struct PretendardFont: ViewModifier { + public let family: PretendardFontFamily + public let size: CGFloat + + public func body(content: Content) -> some View { + return content.font(.custom("PretendardVariable-\(family)", fixedSize: size)) + } +} + + extension View { + func pretendardFont(family: PretendardFontFamily, size: CGFloat) -> some View { + return self.modifier(PretendardFont(family: family, size: size)) + } +} + + extension UIFont { + static func pretendardFontFamily(family: PretendardFontFamily, size: CGFloat) -> UIFont { + let fontName = "PretendardVariable-\(family)" + return UIFont(name: fontName, size: size) ?? UIFont.systemFont(ofSize: size, weight: .regular) + } +} + + extension Font { + static func pretendardFont(family: PretendardFontFamily, size: CGFloat) -> Font{ + let font = Font.custom("PretendardVariable-\(family)", size: size) + return font + } +} diff --git a/MovieBooking/DesignSystem/Font/PretendardFontFamily.swift b/MovieBooking/DesignSystem/Font/PretendardFontFamily.swift new file mode 100644 index 0000000..ea8473d --- /dev/null +++ b/MovieBooking/DesignSystem/Font/PretendardFontFamily.swift @@ -0,0 +1,43 @@ +// +// PretendardFontFamily.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import Foundation + +public enum PretendardFontFamily: CustomStringConvertible { + case black + case bold + case extraBold + case extraLight + case light + case medium + case regular + case semiBold + case thin + + public var description: String { + switch self { + case .black: + return "Black" + case .bold: + return "Bold" + case .extraBold: + return "ExtraBold" + case .extraLight: + return "ExtraLight" + case .light: + return "Light" + case .medium: + return "Medium" + case .regular: + return "Regular" + case .semiBold: + return "SemiBold" + case .thin: + return "Thin" + } + } +} diff --git a/MovieBooking/Feature/Auth/Coordinator/Reducer/AuthCoordinator.swift b/MovieBooking/Feature/Auth/Coordinator/Reducer/AuthCoordinator.swift new file mode 100644 index 0000000..a6fefb8 --- /dev/null +++ b/MovieBooking/Feature/Auth/Coordinator/Reducer/AuthCoordinator.swift @@ -0,0 +1,66 @@ +// +// AuthCoordinator.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import Foundation +import ComposableArchitecture +import TCACoordinators + +@Reducer +public struct AuthCoordinator { + public init() {} + + @ObservableState + public struct State: Equatable { + var routes: [Route] + + public init() { + self.routes = [.root(.login(.init()), embedInNavigationView: true)] + } + } + + public enum Action: BindableAction { + case binding(BindingAction) + case router(IndexedRouterActionOf) + + } + + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(_): + return .none + + case .router(let routeAction): + return routerAction(state: &state, action: routeAction) + + } + } + .forEachRoute(\.routes, action: \.router) + } +} + +extension AuthCoordinator { + private func routerAction( + state: inout State, + action: IndexedRouterActionOf + ) -> Effect { + switch action { + default: + return .none + } + } +} + + +extension AuthCoordinator { + @Reducer(state: .equatable, .hashable) + public enum AuthScreen { + case login(Login) + } +} diff --git a/MovieBooking/Feature/Auth/Coordinator/View/AuthCoordinatorView.swift b/MovieBooking/Feature/Auth/Coordinator/View/AuthCoordinatorView.swift new file mode 100644 index 0000000..c62c4f5 --- /dev/null +++ b/MovieBooking/Feature/Auth/Coordinator/View/AuthCoordinatorView.swift @@ -0,0 +1,33 @@ +// +// AuthCoordinatorView.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +import ComposableArchitecture +import TCACoordinators + +public struct AuthCoordinatorView: View { + @Bindable private var store: StoreOf + + public init( + store: StoreOf + ) { + self.store = store + } + + public var body: some View { + TCARouter(store.scope(state: \.routes, action: \.router)) { screens in + switch screens.case { + case .login(let loginStore): + LoginView(store: loginStore) + .navigationBarBackButtonHidden() + + + } + } + } +} diff --git a/MovieBooking/Feature/Auth/Login/Reducer/Login.swift b/MovieBooking/Feature/Auth/Login/Reducer/Login.swift new file mode 100644 index 0000000..29e4437 --- /dev/null +++ b/MovieBooking/Feature/Auth/Login/Reducer/Login.swift @@ -0,0 +1,115 @@ +// +// Login.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + + +import Foundation +import ComposableArchitecture + + +@Reducer +public struct Login { + public init() {} + + @ObservableState + public struct State: Equatable, Hashable { + public init() {} + } + + public enum Action: ViewAction, BindableAction, Equatable { + case binding(BindingAction) + case view(View) + case async(AsyncAction) + case inner(InnerAction) + case navigation(NavigationAction) + + } + + //MARK: - ViewAction + @CasePathable + public enum View: Equatable { + + } + + + + //MARK: - AsyncAction 비동기 처리 액션 + public enum AsyncAction: Equatable { + + } + + //MARK: - 앱내에서 사용하는 액션 + public enum InnerAction: Equatable { + } + + //MARK: - NavigationAction + public enum NavigationAction: Equatable { + + + } + + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(_): + return .none + + case .view(let viewAction): + return handleViewAction(state: &state, action: viewAction) + + case .async(let asyncAction): + return handleAsyncAction(state: &state, action: asyncAction) + + case .inner(let innerAction): + return handleInnerAction(state: &state, action: innerAction) + + case .navigation(let navigationAction): + return handleNavigationAction(state: &state, action: navigationAction) + } + } + } +} + +extension Login { + private func handleViewAction( + state: inout State, + action: View + ) -> Effect { + switch action { + + } + } + + private func handleAsyncAction( + state: inout State, + action: AsyncAction + ) -> Effect { + switch action { + + } + } + + private func handleNavigationAction( + state: inout State, + action: NavigationAction + ) -> Effect { + switch action { + + } + } + + private func handleInnerAction( + state: inout State, + action: InnerAction + ) -> Effect { + switch action { + + } + } +} + diff --git a/MovieBooking/Feature/Auth/Login/View/LoginView.swift b/MovieBooking/Feature/Auth/Login/View/LoginView.swift new file mode 100644 index 0000000..c299eae --- /dev/null +++ b/MovieBooking/Feature/Auth/Login/View/LoginView.swift @@ -0,0 +1,76 @@ +// +// LoginView.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI + +import ComposableArchitecture + +public struct LoginView: View { + @Bindable var store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + public var body: some View { + VStack { + Spacer() + + loginLogo + + loginTitle() + + Spacer() + } + + } +} + +extension LoginView { + private var loginLogo: some View { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill( + LinearGradient( + colors: [ + .basicPurple, + .violet + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 120, height: 120) + .shadow(color: .basicPurple.opacity(0.3), radius: 16, x: 0, y: 8) + .overlay( + Image(systemName: "film.fill") + .font(.pretendardFont(family: .medium, size: 48)) + .foregroundColor(.white) + ) + } + + @ViewBuilder + private func loginTitle() -> some View { + VStack(spacing: 6) { + Text("MEGABOX") + .font(.pretendardFont(family: .semiBold, size: 32)) + .foregroundColor(.primary) + + Text("간편하게 로그인하고 예매를 시작하세요") + .font(.pretendardFont(family: .medium, size: 16)) + .foregroundColor(.secondary) + } + } +} + + +#Preview { + LoginView(store: .init(initialState: Login.State(), reducer: { + Login() + })) +} + + diff --git a/MovieBooking/Feature/Splash/Reducer/Splash.swift b/MovieBooking/Feature/Splash/Reducer/Splash.swift new file mode 100644 index 0000000..0cd41c0 --- /dev/null +++ b/MovieBooking/Feature/Splash/Reducer/Splash.swift @@ -0,0 +1,153 @@ +// +// Splash.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + + +import Foundation +import ComposableArchitecture +import SwiftUI + + +@Reducer +public struct Splash { + public init() {} + + @ObservableState + public struct State: Equatable, Hashable { + + var fadeOut: Bool = false + var pulse: Bool = false + public init() {} + } + + @CasePathable + public enum Action: ViewAction, Equatable, BindableAction { + case binding(BindingAction) + case view(View) + case async(AsyncAction) + case inner(InnerAction) + case navigation(NavigationAction) + + } + + //MARK: - ViewAction + @CasePathable + public enum View : Equatable{ + case onAppear + case startAnimation + + } + + + //MARK: - AsyncAction 비동기 처리 액션 + @CasePathable + public enum AsyncAction: Equatable { + + } + + //MARK: - 앱내에서 사용하는 액션 + @CasePathable + public enum InnerAction: Equatable { + case setPulse(Bool) + case setFadeOut(Bool) + } + + //MARK: - NavigationAction + @CasePathable + public enum NavigationAction: Equatable { + case presentLogin + case presentMain + + + } + + @Dependency(\.continuousClock) var clock + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(_): + return .none + + case .view(let viewAction): + return handleViewAction(state: &state, action: viewAction) + + case .async(let asyncAction): + return handleAsyncAction(state: &state, action: asyncAction) + + case .inner(let innerAction): + return handleInnerAction(state: &state, action: innerAction) + + case .navigation(let navigationAction): + return handleNavigationAction(state: &state, action: navigationAction) + } + } + } +} + +extension Splash { + private func handleViewAction( + state: inout State, + action: View + ) -> Effect { + switch action { + case .onAppear: + return .send(.view(.startAnimation)) + + case .startAnimation: + return .run { send in + await send(.inner(.setPulse(true))) + + try await clock.sleep(for: .seconds(1.3)) + await send(.inner(.setFadeOut(true))) + await send(.navigation(.presentLogin)) + } + } + } + + private func handleAsyncAction( + state: inout State, + action: AsyncAction + ) -> Effect { + switch action { + + } + } + + private func handleNavigationAction( + state: inout State, + action: NavigationAction + ) -> Effect { + switch action { + // 로그인 안했을경우 + case .presentLogin: + return .none + + // 로그인 했을경우 + case .presentMain: + return .none + } + } + + private func handleInnerAction( + state: inout State, + action: InnerAction + ) -> Effect { + switch action { + case .setPulse(let on): + state.pulse = on + return .none + + case .setFadeOut(let on): + withAnimation(.easeInOut(duration: 3)) { + state.fadeOut = on + } + return .none + } + } +} + diff --git a/MovieBooking/Feature/Splash/View/SplashView.swift b/MovieBooking/Feature/Splash/View/SplashView.swift new file mode 100644 index 0000000..82a2f72 --- /dev/null +++ b/MovieBooking/Feature/Splash/View/SplashView.swift @@ -0,0 +1,96 @@ +// +// SplashView.swift +// MovieBooking +// +// Created by Wonji Suh on 10/14/25. +// + +import SwiftUI +import ComposableArchitecture + +@ViewAction(for: Splash.self) +struct SplashView: View { + @State var store: StoreOf + + var body: some View { + ZStack { + LinearGradient( + gradient: Gradient(colors: [ + .white, + .white, + .basicPurple.opacity(0.05) + ]), + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() + + VStack(spacing: 24) { + + splashLogo() + + titleView() + } + .scaleEffect(store.fadeOut ? 0.95 : 1.0) + .opacity(store.fadeOut ? 0.0 : 1.0) + .animation(.easeInOut(duration: 1), value: store.fadeOut) + .onAppear { + send(.onAppear) + } + } + } +} + + +extension SplashView { + + @ViewBuilder + fileprivate func splashLogo() -> some View { + ZStack { + Circle() + .fill(.basicPurple.opacity(0.20)) + .blur(radius: 24) + .scaleEffect(store.pulse ? 1.08 : 0.95) + .animation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true), value: store.pulse) + + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill( + LinearGradient( + colors: [ + .basicPurple, + Color.purple + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .shadow(color: .black.opacity(0.25), radius: 18, x: 0, y: 10) + .frame(width: 120, height: 120) + .overlay( + Image(systemName: "film.fill") + .font(.pretendardFont(family: .medium, size: 48)) + .foregroundColor(.white) + ) + } + } + + @ViewBuilder + fileprivate func titleView() -> some View { + VStack(spacing: 6) { + Text("MEGABOX") + .font(.pretendardFont(family: .semiBold, size: 32)) + .foregroundColor(.primary) + + Text("나만의 영화관, 지금 시작합니다 🎬") + .font(.pretendardFont(family: .medium, size: 16)) + .foregroundColor(.secondary) + } + } +} + + +#Preview { + SplashView(store: .init(initialState: Splash.State(), reducer: { + Splash() + })) +} diff --git a/MovieBooking/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/MovieBooking/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from MovieBooking/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to MovieBooking/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/MovieBooking/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/MovieBooking/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from MovieBooking/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to MovieBooking/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/MovieBooking/App/Resources/Assets.xcassets/Contents.json b/MovieBooking/Resources/Assets.xcassets/Contents.json similarity index 100% rename from MovieBooking/App/Resources/Assets.xcassets/Contents.json rename to MovieBooking/Resources/Assets.xcassets/Contents.json diff --git a/MovieBooking/Resources/FontAsset/PretendardVariable.ttf b/MovieBooking/Resources/FontAsset/PretendardVariable.ttf new file mode 100644 index 0000000..19063ad Binary files /dev/null and b/MovieBooking/Resources/FontAsset/PretendardVariable.ttf differ diff --git a/MovieBooking/Resources/Info.plist b/MovieBooking/Resources/Info.plist new file mode 100644 index 0000000..188e71a --- /dev/null +++ b/MovieBooking/Resources/Info.plist @@ -0,0 +1,16 @@ + + + + + UIUserInterfaceStyle + Light + UIAppFonts + + PretendardVariable.ttf + + SuperBaseKey + ${SUPERBASE_KEY} + SuperBaseURL + ${SUPERBASE_URL} + + diff --git a/MovieBooking/Resources/MovieBooking.entitlements b/MovieBooking/Resources/MovieBooking.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/MovieBooking/Resources/MovieBooking.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + +