{"id":20931314,"url":"https://github.com/dev-junehee/all-for-lesson","last_synced_at":"2025-06-11T06:05:49.134Z","repository":{"id":263144990,"uuid":"842811559","full_name":"dev-junehee/all-for-lesson","owner":"dev-junehee","description":"전공자와 비전공자 모두를 위한 클래식 음악 레슨 매칭 서비스","archived":false,"fork":false,"pushed_at":"2024-12-17T03:48:19.000Z","size":21690,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-11T06:05:02.356Z","etag":null,"topics":["iamport","ios","mvvm","rxswift","swift","uikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dev-junehee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-15T06:18:31.000Z","updated_at":"2025-01-01T07:52:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"d4c5da9c-822a-4066-bfdf-26232f766af5","html_url":"https://github.com/dev-junehee/all-for-lesson","commit_stats":null,"previous_names":["dev-junehee/all-for-lesson"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-junehee%2Fall-for-lesson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-junehee%2Fall-for-lesson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-junehee%2Fall-for-lesson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-junehee%2Fall-for-lesson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dev-junehee","download_url":"https://codeload.github.com/dev-junehee/all-for-lesson/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-junehee%2Fall-for-lesson/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259211827,"owners_count":22822378,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["iamport","ios","mvvm","rxswift","swift","uikit"],"created_at":"2024-11-18T21:41:11.185Z","updated_at":"2025-06-11T06:05:49.117Z","avatar_url":"https://github.com/dev-junehee.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🎶 올포레슨 (All For Lesson) - 클래식 음악 레슨 매칭 플랫폼\n\n\u003cbr /\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg width=180\" src=\"https://github.com/user-attachments/assets/325b9fc9-fbf3-446b-94b4-70993b3af496\" /\u003e\n  \u003cbr /\u003e\u003cbr /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Swift-v5.1-F05138?logo=swift\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Xcode-v15.4-147EFB?logo=Xcode\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/iOS-15.0+-000000?logo=apple\" /\u003e\n\n\u003c/div\u003e\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n## 기획의도 (Intention)\n\u003e ### 나와 멀다고 생각했던 클래식 음악, 이제 내 손으로 직접 배워보자!\n\n- \"가사가 없어서 지루해요\", \"제목이 어려워요\", \"어떻게 즐겨야 하는지 모르겠어요\"\n- 위와 같은 다양한 이유들로 클래식 음악은 아는 사람만 아는 고급 예술 문화가 되었습니다.\n- 하지만 클래식은 단순 감상을 넘어 직접 연주해보고, 연관된 배경 지식을 채우는 것만으로도 쉽게 즐길 수 있다고 생각합니다.\n- 올포레슨은 클래식을 처음 알아가고 싶은 비전공자와, 전공을 시작하려는 예비 전공자 모두를 위해 다양한 클래식 음악 레슨과 강의를 매칭할 수 있도록 도와주는 플랫폼입니다.\n\n\u003cbr /\u003e\n\n## 프로젝트 소개 (Description)\n\u003e **개발 기간** : 2024. 08. 15 ~ 2024. 09. 08 (약 3주)\u003cbr /\u003e\n\u003e **개발 인원** : 1명 (기획·디자인·개발)\u003cbr /\u003e\n\u003e **최소 버전** : iOS 15.0+\u003cbr /\u003e\n\u003e **지원 모드** : 세로 모드, 라이트 모드\n\n\u003cbr /\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg width=\"19%\" src=\"https://github.com/user-attachments/assets/ea25efac-4679-4692-a105-33bb7d65e027\" /\u003e\n  \u003cimg width=\"19%\" src=\"https://github.com/user-attachments/assets/26c72e9b-c95b-4257-8ff1-bfccbdfe4bfb\" /\u003e\n  \u003cimg width=\"19%\" src=\"https://github.com/user-attachments/assets/0af30259-aeab-46ab-9b14-9b7323727075\" /\u003e\n  \u003cimg width=\"19%\" src=\"https://github.com/user-attachments/assets/fbf30d34-e1d7-4deb-97ff-e5dc2274520a\" /\u003e\n  \u003cimg width=\"19%\" src=\"https://github.com/user-attachments/assets/7f60cfc5-8578-475e-9933-434d8fd8df4d\" /\u003e\n\u003c/div\u003e\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n## 사용 기술 및 개발 환경  (Tech Stack \u0026 Environment)\n- **Language \u0026 Tool** : Swift 5.1, Xcode 15.4\n- **iOS** : UIKit, WebKit, PhotosUI\n- **Library** : Kingfisher, SnapKit, Tabman \u0026 Pageboy, TextFieldEffects, Then, Toast\n- **Architecture** : MVVM\n- **Design Pattern** : Input-Output, Router, Singleton\n- **Network** : Alamofire\n- **Reactive** : RxSwift, RxCocoa\n- **Payments** : iamport-iOS\n- **Management** : Git, GitHub, Notion, Figma\n\n\u003cbr /\u003e\n\n## 아키텍쳐 (Architecture)\n\u003cimg width=\"\" src=\"https://github.com/user-attachments/assets/149b27e3-6220-40a7-b2d9-dae11f8518ab\" /\u003e\n\n- RxSwift와 MVVM 패턴을 활용해 UI와 Business Logic 분리 (View-ViewController 역할 분리)\n- Input-Output 패턴을 활용한 양방향 데이터 바인딩으로 데이터 흐름 일원화\n- Router 패턴을 활용해 반복되는 네트워크 작업 추상화, RxSwift Single Traits와 Result Type을 통한 에러 핸들링\n\n\u003cbr /\u003e\n\n## 개발 방식 및 브랜치 전략 (Development \u0026 Branch Strategy)\n### Issue, Pull Request(PR) 템플릿 활용한 프로젝트 관리\n- 개발 시작 전 새로운 이슈를 생성하고, 이슈와 브랜치를 연결\n- 이슈 번호를 브랜치명에 활용하여 일관된 작업 내용 기록\n- 레이블을 활용하여 작업 종류와 진행 상황을 한 눈에 알 수 있도록 처리\n- 템플릿을 정의하여 작업 내용과 스크린샷을 상세히 기록하여 추후에도 프로젝트 진행 현황을 알 수 있도록 문서화\n\n### 간소화된 Git Flow 도입\n- **`main`**\n  - 실제 서비스 배포용 브랜치\n  - 큰 기능 단위 개발 작업이 완료된 후 병합 (Version Release)\n- **`dev`**\n  - 개발 및 QA 작업용 브랜치\n  - Main 브랜치에서 분기\n  - 각 기능 단위 브랜치 작업이 완료된 후 병합\n- **`feat`** , **`design`**, **`fix`**, **`refactor`**...\n  - 작은 기능 단위 브랜치\n  - dev 브랜치에서 분기\n  - Issue, PR, Commit 컨벤션과 동일한 Prefix 사용하여 일관된 작업 구분\n- 각 브랜치별 작업 내용 확인을 위해 브랜치명 컨벤션 도입\n  - prefix/이슈번호-작업설명\n  - `design/1-home-ui`\n\n```mermaid\n---\ntitle: Example of All-For-Lesson Git Flow\n---\ngitGraph\n   commit id: \"initial\"\n   branch dev order: 2\n   commit id: \"dev\"\n   branch feat order: 3\n   commit id: \"feat/1-some-feature\"\n   checkout dev\n   merge feat\n   branch design order: 4\n   commit id: \"design/2-some-ui\"\n   checkout dev\n   merge design\n   branch fix order: 5\n   commit id: \"fix/3-some-error\"\n   checkout dev\n   merge fix\n   checkout main\n   merge dev tag: \"Release: v1.0.0\"\n   branch hotfix order: 1\n   commit id: \"HOTFIX/4-something-change\"\n   checkout main\n   merge hotfix tag: \"Release: v1.0.1\"\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePrefix Convention 전체보기\u003c/b\u003e\u003c/summary\u003e\n\u003cdiv\u003e\n\n| Prefix  | Description | Prefix  | Description | \n|------------|-----------|------------|-----------|\n| Feat | 새로운 기능에 대한 커밋 | Style | UI 스타일에 관한 커밋 |\n| Fix | 버그 수정에 대한 커밋 | Refactor | 코드 리팩토링에 대한 커밋 |\n| Build | 빌드 관련 파일 수정에 대한 커밋 | Test | 테스트 코드 수정에 대한 커밋 |\n| Chore | 그 외 자잘한 수정에 대한 커밋 | Init | 프로젝트 시작에 대한 커밋 |\n| Ci | CI 관련 설정 수정에 대한 커밋 | Release | 릴리즈에 대한 커밋 |\n| Docs | 문서 수정에 대한 커밋 | WIP | 미완성 작업에 대한 임시 커밋 |           \n\n\u003c/div\u003e\n\u003c/details\u003e\n\n\u003cbr /\u003e\n        \n## 주요 기능 (Main Feature)\n### 레슨 관련 기능\n- 전체 및 악기군별 레슨 목록 및 레슨 상세 정보 조회\n- 레슨 북마크 기능, 레슨 후기 작성 기능\n- PG사 결제 모듈을 연동한 레슨 결제 기능\n\n### 회원 유형별 차별화된 기능\n- 선생님 : 레슨 개설/수정, 레슨 수강 후기 관리\n- 수강생 : 북마크/결제한 레슨 관리, 수강 후기 작성\n\n### 해시태그 검색\n- 해시태그가 포함된 레슨 및 커뮤니티 게시물 목록 조회\n- 사용자가 입력한 검색어에 대한 실시간 해시태그 추천 기능 제공\n\n### 사용자 인증\n- 로그인/로그아웃\n- 회원가입/회원탈퇴\n- 토큰 갱신\n\n\u003cbr /\u003e\n\n## 주요 기술 구현 내용 (Implementation Details)\n### RxSwift와 Input-Output 패턴을 적용한 MVVM 아키텍쳐 설계\n- ViewController가 복잡해지는 것을 막고 UI와 비즈니스 로직을 분리하기 위해 MVVM 아키텍쳐 도입\n- associatedType으로 Input, Output 구성을 강제하고 transform 메서드 포함하는 ViewModelType 프로토콜 구현\n- ViewModel : ViewModelType 프로토콜을 채택하여 데이터 가공과 비즈니스 로직을 처리, 데이터 흐름을 Input과 Output으로 제어\n- ViewController : bind 메서드 실행 이후 Stream에 이어지는 UI 업데이트 및 화면 전환 담당\n\n\u003cbr /\u003e\n\n### Alamofire, TargetType, Router 패턴을 활용한 네트워크 관리\n- Alamofire의 URLRequestConvertible을 채택한 TargetType 프로토콜을 정의해 네트워크 요청에 필요한 속성 정의\n- API 특성에 따라 Router를 나누어 관리, TargetType 프로토콜을 채택해 필요한 엔드포인트 정의 및 URLRequest 생성\n- 네트워크 요청에 필요한 Query, Body를 구조체로 정의하여 Router case에 연관값으로 선언하여 사용\n- 네트워크 에러를 열거형으로 정의하고, 연산 프로퍼티를 활용해 각 케이스에 맞는 커스텀 에러 메세지 구성\n- Encodable/Decodable 프로토콜을 채택한 네트워크 요청/응답 데이터 모델 정의, 네이밍 컨벤션을 적용하여 관리\n\n\u003cbr /\u003e\n\n### Alamofire RequestInterceptor를 활용한 AccessToken 갱신\n- Alamofire의 RequestInterceptor를 채택한 NetworkInspector 구현\n- adapt 메서드를 통해 네트워크 통신 전 HTTP Header 내 액세스 토큰 추가 \n- HTTP 상태 코드가 419(액세스 토큰 만료)인 경우 retry 메서드를 통해 토큰을 갱신시키고 네트워크 재요청 처리\n\n\u003cbr /\u003e\n\n### PG사 결제 모듈을 연동한 레슨 결제 기능 구현 \u0026 결제 유효성 검증\n- iamport-iOS 라이브러리 사용하여 PG사 결제 모듈 연동\n- 레슨 결제 버튼 클릭 시 PublishSubject로 해당 레슨 정보를 전달 받고, PG사 서버에 레슨 정보와 함께 결제 요청\n- PG사 서버에서 응답 받은 결제 결과를 바탕으로 서비스 서버에 결제 영수증 검증 요청\n- 서비스 서버에서 응답 받은 영수증 검증 결과를 바탕으로 최종 레슨 결제 성공/실패 처리\n\n\u003cbr /\u003e\n\n### PhotosUI를 활용한 사진첩  Multipart-form 활용한 이미지 업로드 처리\n- PHPickerViewControllerDelegate의 pick 메서드를 사용해 사진첩 내 이미지 접근\n- 조건문을 활용하여 사용자 가 선택한 이미지 개수에 따라 분기 처리 진행\n- 사용자가 선택한 이미지의 파일명(String)과 데이터(Data)를 BehaviorSubject로 전달하고 Observable.zip 활용해 데이터 가공\n- Alamofire.upload 메서드를 통해 multipart-form 형식으로 처리 후 Result Type으로 에러 핸들링\n\n\u003cbr /\u003e\n\n### 화면 전환에 필요한 메서드를 구성하는 NavigationManager 구현\n- Singleton 패턴으로 화면 전환 및 RootViewController 관리에 필요한 메서드를 구성하여 한 곳에서 관리\n- 화면 전환이 필요한 부분에서 NavigationManager.shared 인스턴스에 접근하여 원하는 메서드 호출\n- 사용자 앱 진입 시 SceneDelegate에서 UserDefaults에 저장된 RefreshToken 유무에 따라 첫 화면 핸들링\n    - 토큰이 있는 경우 로그인화면\n    - 토큰이 없는 경우 온보딩 화면\n\n\u003cbr /\u003e\n\n### Base 코드, 공통 컴포넌트, 리소스 관리\n- 여러 View에서 공통적으로 활용하는 Base 코드 정의, 필요한 메서드를 오버라이딩하여 사용\n- 여러 화면에서 반복적으로 사용되는 UI를 재사용과 커스텀이 가능하도록 컴포넌트화하여 활용\n- 열거형과 타입 프로퍼티를 통해 앱에서 사용하는 문자열, 폰트, 이미지 등의 리소스 코드를 데이터로 인식하여 관리\n\n\u003cbr /\u003e\n\n### 성능 최적화와 메모리 누수 방지\n- final, private 키워드를 사용하여 서브클래싱과 오버라이딩을 방지,  파일 외부에서 접근하지 않는 프로퍼티에 대해 접근 제한 처리\n- Static Dispatch로 동작하도록 처리함으로써 컴파일 최적화\n- 클로저 내부에서 외부 데이터를 참조할 때 [weak self] 처리를 통해  강한 순환 참조 문제 해결\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n## 트러블 슈팅  (Trouble Shooting)\n### 1. 네트워크 통신 중 Token 갱신이 필요한 상황에 대한 핸들링\n- **원인** : 네트워크 통신 중 AccessToken이 만료될 경우, RefreshToken을 사용해 AccessToken을 갱신하는데, 이 과정에서 이전에 진행하던 네트워크 통신이 종료되어 사용자가 같은 액션을 2번 이상 진행해야 하는 문제\n- **고민** : 사용자가 알 수 없게 AccessToken을 갱신 과정을 숨기고, 기존 네트워크 통신까지 다시 진행하여 사용자 경험과 비즈니스 로직 두 가지를 모두 대한 고려한 기능 구현이 필요\n- **해결** : Router Pattern을 사용해 추상화 되어있는 apiCall 함수 내에서 Status Code가 419일 때 AccessToken을 갱신하고, 재귀를 통해 현재 진행 중인 네트워크를 재호출하여 해결\n\t- AccessToken을 갱신하는 과정에서 RefreshToken도 함께 갱신해주어야 하는 경우에는 로그인 화면으로 전환하여 사용자 재로그인 유도\n- **리팩토링** : Alamofier RequestInterceptor의 adapt, retry 함수를 사용하여 AccessToken 갱신이 필요한 경우 retry 함수 내에서 갱신을 진행하고, 네트워크 재호출 요청 처리\n\t- retry의 경우 API에 문제가 있을 때 끊임없이 Error가 발생하여 무한 루프에 빠질 수 있으므로 retryLimit을 정하여 토큰 갱신 시도를 제한\n- **성과** : Token 갱신 처리에 대한 다양한 접근과 사용자 경험을 함께 고려한 시도를 해보게 되고, Alamofire RequestInterceptor 개념에 대해 새롭게 알게 됨\n\n\u003cbr /\u003e\n\n### 2. 네트워크 통신 실패 시 onError 이벤트 방출로 인해 Stream이 끊기는 문제\n- **원인** : RxSwift에서 네트워크 통신 결과가 실패했을 때 onError 이벤트를 방출하여 Stream이 dispose 처리되기 때문에, 이후 같은 네트워크 통신이 필요한 User Interaction이 생겼을 경우 이벤트를 받을 수 없는 문제 발생\n- **해결** : Single Traits와 Result Type을 사용하여 네트워크 통신이 실패하더라도 성공값으로 간주해 Success Case로 래핑하여 방출하고, bind 구문 안에서 switch문을 통해 래핑을 해제하고 성공/실패를 모두 처리할 수 있도록 구현 \n- **성과** : Stream을 유지하면서 에러 핸들링을 할 수 있는 방법에 대해 고민하고 적용해볼 수 있게 됨. \n\n\u003cimg src=\"https://github.com/user-attachments/assets/c0c2d5f9-2119-42f4-8d71-a32526d72847\" /\u003e\n\n\u003c!--\n```swift\nswitch response.result {\ncase .success(let value):\n\tobserver(.success(.success(value)))\ncase .failure(_):\n\tguard let networkError = NetworkErrorCase(rawValue: statusCode) else {\n\t\treturn observer(.success(.failure(NetworkErrorCase.UnknownError)))\n\t}\n\tobserver(.success(.failure(networkError)))\n}\n```\n--\u003e\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n### 3. 홈 화면에서 여러 Section마다 다른 Cell을 바인딩 해야 하는 문제\n- **원인** : 홈 화면에는 메뉴 버튼으로 구성된 CollectionView와 인기 레슨/흥미 레슨으로 구성된 2개의 CollectionView까지 총 3개이 Section으로 이루어져 있었고, 메뉴와 레슨에서 각각 다른 Cell을 사용해야 하는 상황 \n- **고민** : RxDataSoure 라이브러리를 활용해 해결할 수 있지만 외부 라이브러리 의존성이 높아지고, 기술 확장보다 우선순위가 높은 프로젝트 진행도와 완성도가 떨어지는 것에 대한 우려 발생\n- **해결** : 기존 사용하던 기술에서 빠르게 활용 가능한 방향으로, ScrollView 위에 3개의 CollectionView를 구성하여 각 CollectionView에 맞는 Cell을 바인딩하여 해결\n- **성과** : 프로젝트 진행 시 우선순위에 대해 고민하고, 우선순위에 맞는 작업 분배와 각 작업을 진행하는 방법에 대해 생각해보고 적용해볼 수 있게 됨\n\n```swift\nprivate func bind() {\n    let viewDidLoadTrigger = self.rx.methodInvoked(#selector(self.viewWillAppear(_:)))\n    \n    let input = HomeViewModel.Input(viewDidLoadTrigger: viewDidLoadTrigger,\n                                    menuButtonTap: homeView.menuCollectionView.rx.itemSelected,\n                                    popularLessonTap: homeView.popularCollectionView.rx.modelSelected(Post.self),\n                                    interestingLessonTap: homeView.interestingCollectionView.rx.modelSelected(Post.self))\n    let output = viewModel.transform(input: input)\n    \n    // 각 CollectionView에 바인딩할 Cell 구성\n    let menu = (id: HomeMenuCollectionViewCell.id,\n                cellType: HomeMenuCollectionViewCell.self)\n    let popular = (id: HomeLessonCollectionViewCell.id,\n                   cellType: HomeLessonCollectionViewCell.self)\n    let interesting = (id: HomeLessonCollectionViewCell.id,\n                       cellType: HomeLessonCollectionViewCell.self)\n\n    output.menuItems\n        .bind(to: homeView.menuCollectionView.rx.items(cellIdentifier: menu.id, cellType: menu.cellType)) { item, element, cell in\n            // ...\n        }\n        .disposed(by: disposeBag)\n    \n    output.popularLessonList\n        .bind(to: homeView.popularCollectionView.rx.items(cellIdentifier: popular.id, cellType: popular.cellType)) { item, element, cell in\n            // ...\n        }\n        .disposed(by: disposeBag)\n    \n    output.interestingLessonList\n        .bind(to: homeView.interestingCollectionView.rx.items(cellIdentifier: interesting.id, cellType: interesting.cellType)) { item, element, cell in\n            // ...\n        }\n        .disposed(by: disposeBag)\n}\n```\n\n\u003cbr /\u003e\n\n### 4. iamport-iOS, Then 패키지 충돌 오류\n- **원인** : iamport-iOS SDK Dependency에서 이미 가지고 있는 Then 라이브러리와 직접 설치한 Then 라이브러리의 버전이 달라 충돌 발생\n    - iamport-iOS에서 의존하는 Then 라이브러리는 버전 2.7.0\n    - 직접 설치한 Then 라이브러리는 버전 3.0.0 이상이기 때문에 서로 충돌\n- **해결** : Then 라이브러리는 major 버전 2에서도 문제없이 동작하기 때문에, 직접 설치한 라이브러리를 삭제하고 iamport-iOS에서 의존하고 있는 Then 라이브러리를 그대로 사용하여 해결\n- **성과** : SDK 개발 시 특정 라이브러리에 의존하게 되면 발생할 수 있는 문제와, 모듈 간의 의존성에 대해 고민하고 학습할 수 있게 됨 \n\n```swift=\n// iamport-iOS dependincies\ndependencies: [\n    .package(url: \"https://github.com/ReactiveX/RxSwift.git\", .upToNextMajor(from: \"6.0.0\")),\n    .package(name: \"RxBusForPort\", url: \"https://github.com/iamport/RxBus-Swift\", .upToNextMinor(from: \"1.3.0\")),\n    .package(url: \"https://github.com/Alamofire/Alamofire.git\", .upToNextMajor(from: \"5.4.0\")),\n    .package(url: \"https://github.com/devxoul/Then.git\", .upToNextMajor(from: \"2.7.0\")),\n    .package(url: \"https://github.com/nicklockwood/SwiftFormat\", from: \"0.50.4\")\n],\n```\n\n\u003cbr /\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdev-junehee%2Fall-for-lesson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdev-junehee%2Fall-for-lesson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdev-junehee%2Fall-for-lesson/lists"}