Wonbi
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 오픈마켓 그라운드룰 # 🤙 Ground Rule ## 🧑‍🤝‍🧑 Rules 매일 10시 시작 ## ⏰ 스크럼 - 당일 컨디션 공유, 특이사항 - 오늘 할일 공유 - 매일 정리하자! ## 📝 Commit convention (Karma Style) > feat: 새로운 기능 추가 fix: 버그 수정 docs: 문서 수정 refactor: 코드 리팩토링 style: 코드 포맷팅 (코드 변경이 없는 경우) test: 테스트 코드 작성 chore: 소스 코드를 건들지 않는 작업(빌드 업무 수정) merge: 메서드 혹은 타입 이름 from 사용자 asset: 에셋 추가 혹은 에셋 수정 design: 사용자 UI 디자인 변경 > ## ✏️ Code convention [Code convention](https://hackmd.io/357h50mcQlmbxAth6HwQHg) # Open Market Scrum #### 🙌 11/14 스크럼 - 오늘의 컨디션 - Gundy: So good ☀️ - Wonbi: 주말에 잘 쉬어서 좋다 - 특이사항 - Gundy: 2시부터 활동학습, 5시에 인터넷 수리기사(KT) 방문예정으로 자리 비움 - Wonbi: 2~5시 활동학습 - 오늘 할 일 - [x] 그라운드 룰 정하기 - [x] 코드 컨벤션 정하기 - [x] 일일 스크럼 진행하기 #### 🙌 11/15 스크럼 - 오늘의 컨디션 - Gundy: 졸릴지도 - Wonbi: 어제 너무 늦게잠 ㅠ - 특이사항 - Gundy: 배고플지도 - Wonbi: 잠? 죽어서 잔다 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] 프로젝트 파일 포크하고 클론 떠오기 - [x] step1-1, 1-2 진행하기 #### 🙌 11/16 스크럼 - 오늘의 컨디션 - Gundy: 어제 일찍 자서 So good일지도 - Wonbi: 넘 졸림... - 특이사항 - Gundy: 17시에 KT 인터넷 엔지니어 방문할지도 - Wonbi: 3시에 구로에 약속이 있습니다..! - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] 까먹지 않고 파일명 변경하기 - [x] step1-3 진행하기 #### 🙌 11/17 스크럼 - 오늘의 컨디션 - Gundy: So so 일지도 - Wonbi: 어제 너무 푹자서 좋아따 - 특이사항 - Gundy: 활동학습말곤 없을지도 - Wonbi: 2~5시 활동학습 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] 실제 네트워킹 해보기 - [x] PR 보내기 - [x] 리드미 작성하기 #### 🙌 11/21 스크럼 - 오늘의 컨디션 - Gundy: 평범? - Wonbi: 어제 엄청 힘든거 치고 무난한 컨디션..! - 특이사항 - Gundy: 딱히? 활동학습? - Wonbi: 활동학습 2~5시 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] PR Refactoring - [x] step2 진행하기 #### 🙌 11/22 스크럼 - 오늘의 컨디션 - Gundy: 목이 건조한 너낌 - Wonbi: 어제 너무 늦게 자서 피곤합니다.. - 특이사항 - Gundy: 배고플지도? - Wonbi: 없습니다! - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step2 진행하기 #### 🙌 11/23 스크럼 - 오늘의 컨디션 - Gundy: So~Good~ - Wonbi: 아 늦잠잠... ㅋㅋㅋ - 특이사항 - Gundy: 즈녁쉭사가 있사옵니다. - Wonbi: 없습니다 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step2 마무리하기 - [x] step-bonus 하기 #### 🙌 11/24 스크럼 - 오늘의 컨디션 - Gundy: race-condition - Wonbi: 무난합니다 - 특이사항 - Gundy: 활동학습정도? 저녁 7시 약속(월드컵 시청) - Wonbi: 활동학습 2~5시 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step2 PR 보내기 - [x] 활동학습 공부하기 #### 🙌 11/25 스크럼 - 오늘의 컨디션 - Gundy: 월드컵 보고 늦게 잤는데 의외의 숙면? - Wonbi: 아 또 늦잠잤음 ㅠㅠ - 특이사항 - Gundy: 아침에 주민센터가서 음식물 쓰레기 용기 받아옴 개꿀~ 공짜~ - Wonbi: 4시에 약속이 있습니다. - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] 리드미 작성하기 #### 🙌 11/28 스크럼 - 오늘의 컨디션 - Gundy: 목이 좀 건조한 너낌 - Wonbi: 빡빡한 주말일정을 끝내고 왔습니다 - 특이사항 - Gundy: 활동학습? - Wonbi: 2~5시 활동학습 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] PR Refactoring - [x] 활동학습 공부 #### 🙌 11/29 스크럼 - 오늘의 컨디션 - Gundy: 일찍 일어나는 새가 졸리다 - Wonbi: 아 너무 졸리다 - 특이사항 - Gundy: 특이사항이 없는게 특이사항 - Wonbi: 없습니다 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step3 진행 #### 🙌 12/1 스크럼 - 오늘의 컨디션 - Gundy: 12월의 추위를 얕보지 마라 - Wonbi: 어제 너무 힘들었는데 그래도 오늘 컨디션 낫배드 - 특이사항 - Gundy: 동활습학 있습니당 - Wonbi: 활동학습 2~5시 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step3 진행 #### 🙌 12/2 스크럼 - 오늘의 컨디션 - Gundy: 기침이 좀 잦아드는 것 같기도 하고... - Wonbi: 아침에 일어나는 건 매우 힘든 일이다... - 특이사항 - Gundy: 배고픔? - Wonbi: 없습니다! - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] 리드미데이 진행 - [x] step3 진행 #### 🙌 12/5 스크럼 - 오늘의 컨디션 - Gundy: 기침이 잦아든다는 착각을 하는 것도 잠시 그의 기침은 멈출 줄을 몰랐다. - Wonbi: 이사이슈로 오전이 날아가버림.. - 특이사항 - Gundy: 그래서 병원다녀옴 - Wonbi: 오전 이사로 인한 불참 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step3 진행 #### 🙌 12/6 스크럼 - 오늘의 컨디션 - Gundy: 그냥 그래요 안좋은듯 - Wonbi: 아침부터 역시 이사이슈로 정신이 없다 @_@ - 특이사항 - Gundy: 힘이없다 - Wonbi: 이사.. 침대가 들어왔다. - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step3 완료 - [x] PR 보내기 #### 🙌 12/7 스크럼 - 오늘의 컨디션 - Gundy: 어제보다는 나은듯 - Wonbi: 오늘따라 상쾌한 아침이다. 따듯한 커피가 생각나는 아침 컨디션 굿! - 특이사항 - Gundy: 흠...글쎄요 딱히? - Wonbi: 오늘 매우 바쁠 예정.. 사실 이번주 내내 개바쁨.. ㅠㅠ - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] PR 리팩토링 #### 🙌 12/8 스크럼 - 오늘의 컨디션 - Gundy: 사탕이 질립니다 - Wonbi: 무난무난한 컨디션입니다 - 특이사항 - Gundy: 활동학습? - Wonbi: 활동학습 2~5시 - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] PR 리팩토링 - [x] step4 진행 #### 🙌 12/9 스크럼 - 오늘의 컨디션 - Gundy: 병원에서 주사맞고옴 30대가 엉덩이에 주사맞다니 - Wonbi: 정신차려보니 9시반 - 특이사항 - Gundy: 모각코 갈 예정입니당 - Wonbi: 5시에 이사갑니다..!! 그전에 준비할 시간이 좀 필요해요 ㅠ - 오늘 할 일 - [x] 일일 스크럼 진행하기 - [x] step4 완료하기 - [x] PR 보내기 - [x] 리드미 작성하기 # Open-Market PR 오픈마켓 II [Step 2] Gundy, Wonbi @wonhee009 안녕하세요 라자냐! 드디어 오픈마켓의 마지막 스텝입니다! ## ✓ 진행단계 : II Step-2 ## 📝 STEP 진행 중 경험하고 배운 것 - UIAlertController 활용 ☑️ textFields 사용하기 ☑️ Action Sheet 사용하기 ☑️ preferredAction 사용하기 - 프로토콜 및 기본구현 활용하기 ☑️ 프로토콜로 공통기능 구현하고 채택하기 ## 💭 고민한 부분 ### viewWillAppear에서의 네트워킹 - 상품 상세 페이지에서 상품 수정 화면으로 이동 후 상품을 수정하면 다시 상품 상세 페이지로 돌아옵니다. - 기존 로직으로는 detailProduct를 첫 화면에서부터 넘겨받기 때문에 UI적으로 즉각적인 업데이트를 실행할 수 없었습니다. - 그렇기 때문에 상품 수정이 끝나면 첫 화면으로 바로 넘어가게끔 로직을 구현하는 것이 가장 쉬운 해결방법이라 생각했습니다. - 하지만 더 좋은 사용자 경험은 화면이 바로 업데이트 되는 것이라 생각해 ProductDetailViewController의 viewWillAppear에서 네트워킹을 진행하는 방향으로 리팩토링하여 바로 업데이트 할 수 있도록 하였습니다. ### collectionView의 Paging - 상품 상세 페이지에서의 이미지는 더욱 용량이 큰 원본 이미지입니다. - 이 이미지들을 하나씩 잘 확인할 수 있도록 화면을 가득 메우는 크기로 이미지를 표시합니다. - 또한 이미지별로 사진의 번호가 레이블로 표시됩니다. - 이러한 조건에서 더 명확한 UI/UX를 위해 Paging 기능을 사용하였습니다. ## ❓ 조언을 얻고 싶은 부분 ### 프로토콜에서 @objc 사용 - 수정화면과 등록화면을 위한 프로토콜을 만드는 중에, 버튼이나 AlertController의 액션을 위한 @objc 메서드를 정의할 수 없는 문제가 있었습니다. 이 문제를 해결해보기 위해 protocol내부 메서드에@objc접두사를 붙이거나, @objc protocol을 선언해보았지만 모두 실패했습니다. 구글링을 좀 해보니 @objc는 프로토콜에서 사용할 수 없다고 하던데, 혹시 이 부분에서 저희가 놓친 부분이나 관련해서 힌트를 좀 얻을 수 있을까요? ### JSON 파일의 병합 - 이전 코멘트에서 주신 질문들을 토대로 NewProduct 모델과 EditProduct 모델을 합쳐서 하나의 SendingProduct모델을 만들어 보았습니다. - 하지만 코멘트 주신 부분처럼 ProductResponse 모델을 만들고 뷰 컨트롤러 내부에 EditProduct이 네스티드 타입으로 들어가서 사용하는 부분은 저희가 잘 이해하지 못했습니다. - 저희가 이해한 방향은 내부 네트워킹을 ProductResponse로 진행한 후에 받아온 리스폰을 내부 EditProduct객체를 초기화 하는데 사용하는 방향이라 생각했습니다. - 하지만, NewProduct 모델과 EditProduct 모델은 인코딩을 위한 모델이어서 사용자를 통해 입력받은 값을 토대로 객체를 초기화 하기 때문에 이 방법을 적용하는 것이 맞지 않다고 생각했고, DetailProduct같은 경우, ProductResponse타입으로 만들어서 GET 해온 후에 다시 DetailProduct 객체로 만드는 방식은 필요없는 로직이 하나 추가된다는 생각이 들었습니다. - 혹시 저희가 잘못 이해한 부분이나 잘못 생각하는 부분이 있을까요? 오픈마켓 II [Step 1] Gundy, Wonbi @wonhee009 안녕하세요 라자냐! PR이 너무 늦어졌습니다.. ㅠㅠ 죄송합니다 ㅠㅠ 오픈마켓의 악명은 익히 들어 알고 있었지만 실제로 접해보니 악독하더구만요... ## ✓ 진행단계 : II Step-1 ## 📝 STEP 진행 중 경험하고 배운 것 - 모던 컬렉션 뷰 사용하기 ☑️ 커스터마이징 셀 구현하기 ☑️ 가로스크롤 컬렉션 뷰 구현하기 - UIImagePickerController 사용하기 ☑️ 사진첩에 접근하기 - UITextField, UITextView를 통해 사용자 입력받기 ☑️ placeholder 사용하기 ## 💭 고민한 부분 ### 키보드 사용시 화면 스크롤링 - 이번 프로젝트의 가이드라인에는 키보드가 작성중인 컨텐츠를 가리면 안된다는 조건이 있었습니다. 기기별로 저마다의 컨텐츠 위치가 다를텐데 이를 적절히 스크롤해주는 것이 쉽지 않았습니다. - 저희는 텍스트 필드를 입력중일 때는 imageCollectionView의 아랫부분으로, 텍스트 뷰를 입력중일 때는 productStackView의 아랫부분으로 오프셋을 정하여 적절한 위치로 화면이 이동하게 하였습니다. ### 코드 재사용 - 얼럿을 띄우는 메서드인 showAlert의 경우 action을 따로 분리하지 않고 코드를 재사용할 수 있도록 매개변수 message의 내용에 따라 `dismiss(animated: true)`할 수 있도록 조건문을 사용하였습니다. ### Entering data - Apple HIG에 따르면 여러 텍스트 필드 등을 입력해야 할 때 동적으로 값을 검증하고 미리 안내하는 것이 사용자에게 좋다고 안내합니다. 저희도 이를 지키기 위해 필수 입력값인 이름, 가격, 설명 등에 대해서 입력하지 않은 경우 빨간색 보더라인을 추가하는 것으로 시각적 효과를 주고 있습니다. > **Dynamically validate field values.** > People can get frustrated when they have to go back and correct mistakes after filling out a lengthy form. When you verify values as soon as people enter them — and provide feedback as soon as you detect a problem — you give them the opportunity to correct errors right away. For numeric data in particular, consider using a number formatter, which automatically configures a text field to accept only numeric values. You can also configure a formatter to display the value in a specific way, such as with a certain number of decimal places, as a percentage, or as currency. ### 이미지 추가와 삭제 - 이미지를 처음에는 추가하지만 추가된 이미지를 다시 클릭하면 삭제되도록 로직을 구성해, 사용자가 이미지를 잘못 선택했을 때 cnacel을 눌러 화면에서 벗어났다가 다시 들어오는 불편함이 없도록 하였습니다. ## ❓ 조언을 얻고 싶은 부분 ### 뷰 컨트롤러가 너무 방대함 - 코드를 작성하다 보니 뷰 컨트롤러가 400줄이 넘어가는 사태가 발생하였습니다.. MVC패턴의 단점이 뷰 컨트롤러가 너무 방대해진다는 것은 알고 있었지만, 그럼에도 이 패턴을 지키면서 뷰 컨트롤러를 가볍게 할 수 있는 방법이 있을까요? 저희가 생각 하기에는 뷰의 컴포넌트들을 따로 분리하여 구현하고 그 컴포넌트들을 구현한 뷰를 뷰컨의 뷰로 바꿔치기 하는 등의 방법을 생각했었습니다. ```swift class MainView: UIView { // 여기에 컴포넌트들이 들어가고 구현됩니다. // Label, stack, collection 등등.. } class ViewController: UIViewController { override func viewDidLoad() { // 여기서 뷰가 교체됩니다. view = MainView() } } ``` - 위와 같은 방법으로 뷰를 그리는 부분을 분리하는 방법을 생각했었습니다. - 이번 저희의 PR이 너무 늦어져서, 일단 먼저 PR을 보낸 후 라자냐의 조언을 얻고 step2에서 이 방식으로 리팩토링 해볼 계획입니다. - 이러한 방법이 괜찮은 방법일까요? 또, 방대해진 뷰 컨트롤러의 부담을 덜어주기 위한 다른 방법이나 트릭들이 있을지 궁금합니다! ### 텍스트 필드와 키보드 타입 - 숫자를 입력받는 곳에는 소수를 입력받는지 정수를 입력받는지에 따라 decimalPad와 numberPad로 구분하여 키보드를 제공하고 있습니다. - 하지만 블루투스 키보드 등을 연결하고 사용한다면 해당 텍스트 필드에서도 숫자 이외의 값을 입력할 수 있을 것입니다. - 이런 경우 사용자의 입력을 강제할 수 있도록 다른 조치를 취해야 할까요? ### 화면의 다른 부분 터치시 키보드 내리기 - 텍스트 필드나 텍스트 뷰를 edit하다가 키보드를 내릴 때 화면을 터치하는 것이 일반적으로 사용되는 UX라고 생각합니다. - view의 `touchesBegan`을 통해 `view.endEditing(true)`를 호출하여 키보드를 내리려고 했는데, imageCollectionView 부분을 터치하는 경우 키보드가 내려가지 않았습니다. - 해당 부분을 터치하더라도 키보드가 내려갈 수 있도록 탭 제스처를 추가했더니 셀을 선택할 수 없는 문제가 발생하였습니다. - hitTest를 수정하는 방향으로도 접근하여서 첫 번째 셀을 터치하면 셀이 터치가 되고, 컬렉션뷰의 배경을 터치하면 키보드가 내려가게끔 로직을 구현하였습니다. 하지만 이미지가 추가됨에 따라 셀이 늘어나자 해당 로직대로는 셀을 터치할 수가 없어서 결국 터치로 키보드를 내리지 않고 Done 버튼을 추가하는 것으로 문제를 해결하였습니다. - 이런 상황에서 터치로 키보드를 내리는 방법에 대한 조언을 얻고 싶습니다! 아래는 당시 구현한 코드...인것같습니다. ```swift extension UICollectionView { open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { var touchedCell = superview?.hitTest(point, with: event) visibleCells.forEach { cell in if let cell = cell.hitTest(point, with: event) { touchedCell = cell } } return touchedCell } } ``` ### 이미지를 용량 기준으로 리사이징하기 - 이번 프로젝트 조건 중, 이미지의 용량이 300KB가 넘으면 이를 리사이징하여 300이하가 되도록 구현하는 부분이 있었습니다. - 저희는 첫번째로, 이미지를 jpeg로 압축하여 이미지의 용량을 조절하는 방법을 구현해 보았었습니다. - 이 방법은, 이미지를 jpegData(compressionQuality:)메서드로 압축하되, 압축 퀄리티를 점점 낮춰서 용량이 300이하가 될 때까지 재귀하는 방법이었습니다. ```swift func compress() -> Data? { var compressingValue: Double = 1.0 while /*용량이 300이하일 때 까지*/ { if let data = self.jpegData(compressionQuality: compressingValue) { if /*data의 크기가 300 보다 크면*/ { compressingValue -= 0.1 } else { image = data } } } } ``` - 간단히 작성해본 그때의 코드입니다. 이 방법의 단점은, 이미지를 계속해서 데이터로 인코딩을 진행해야 해서 앱의 속도가 느려지고, 이미지의 용량이 너무 클 경우 `compressionQuality` 매개변수 값이 음수가 될 때까지 반복된다는 점이었습니다. - 이런 문제를 해결하기 위해, 이미지의 용량을 체크해서 300보다 클 경우, 300보다 얼만큼 큰지를 비율로 계산해 그 비율만큼 사진의 크기를 리사이징 하고 압축하는 방법을 생각했습니다. - 이 방법은, 이미지의 용량를 비율로 계산하여 클수록 이미지를 더 작게 만들어서 한번만 압축하는 방법이었습니다. ```swift func updateImage(image: UIImage?) { guard let dataSize = image?.jpegData(compressionQuality: 0.9)?.count else { return } let kbSize = Double(dataSize / 1024) plusImageView.isHidden = true if kbSize > 300 { // 여기서 300보다 클 경우 scale을 계산하여 리사이징을 진행합니다. let scale = floor(sqrt(300 / kbSize) * 10) / 10 productImageView.image = image?.resize(scale) } else { productImageView.image = image } } func resize(_ scale: Double) -> UIImage? { let target = CGRect(x: 0, y: 0, width: (self.size.width * scale), height: (self.size.height * scale)) // 이미지를 target을 기준으로 리사이징 하는 로직이 들어갑니다. return newImage } } ``` - 이 방법은 한번만 압축을 진행하기에 속도가 빨라 앱에 부담을 덜어주는 방법이었습니다. 하지만 이미지의 용량이 300보다 더 작아지는 경우도 생겼습니다. - 결과적으로는 두번째 방법을 채택하여 이미지를 리사이징 하게 되었습니다. 다만, 이는 이미지의 용량이 300에 딱 떨어지게 해서 업로드 가능한 최대 화질의 이미지가 아니기 때문에 완벽하게 요구사항을 만족하지 못했다 판단하였습니다. - 저희가 생각하지 못한 이미지를 용량을 기준으로 리사이징 하는 다른 방법이 또 있을까요? 그리고, 현업에서는 이러한 작업을 보통 어떻게 진행하는지 궁금합니다! 오픈마켓 [Step 2] Gundy, Wonbi @wonhee009 안녕하세요 라자냐! 이번 스텝은 처음 써보는 네트워크 작업에 더불어 처음 써보는 컬렉션뷰라서 생각보다 많이 헤맨 것 같습니다! 이상한 부분이 있다면 다 알려주세요! ## ✓ 진행단계 : Step-2 ## 📝 STEP 진행 중 경험하고 배운 것 - 컬렉션 뷰 사용하기 ☑️ 커스터마이징 셀 구현하기 ☑️ 리스트와 그리드 모양의 컬렉션 뷰 구현하기 - 이미지 비동기로 처리하기 ☑️ 이미지를 서버에서 파싱하는 과정을 비동기로 처리하기 ☑️ race condition 해결하기 - UISegmentedControl 사용하기 - 이미지 캐싱하기 ☑️ 서버를 통해서 전달받은 데이터를 로컬에 캐시하기 ## ⚒️ 코드 구현내용 <details> <summary>구현내용</summary> ### 구현 - Extension - DecodingError - `errorDescription`을 사용해 상황에 맞는 에러 메세지를 출력하도록 하였습니다. - Collection - `subscript`를 이용해 첨자 문법으로 값에 접근 시 런타임에러가 나지 않도록 하였습니다. - Int - 값이 0인지 확인하는 `isZero`, 숫자의 자리수를 체크하는 `decimal` 프로퍼티를 가지도록 하였습니다. - OpenMarket - Product - 이제 `Product`는 `Hashable`을 채택합니다. - ImageCacheManager - 이미지를 캐싱하기 위한 싱글톤 객체입니다. - Controller - ProductsViewController - 앱 실행시 나오는 첫 화면을 컨트롤 합니다. - 데이터를 파싱하고 이를 각 컬렉션 뷰에 전달합니다. - segmentedControl의 값이 바뀔 때 마다 각각의 컬렉션 뷰를 보여주도록 화면을 전환합니다. - AddProductViewController - 다음 스텝에서 추가될 새로운 상품을 등록하는 화면을 컨트롤합니다. - DummyData - ListCollectionViewCell - 리스트 형태의 컬렉션 뷰에서 사용하는 셀입니다. - 리스트 형태로 커스터마이징 된 셀을 그립니다. - GridCollectionViewCell - 그리드 형태의 컬렉션 뷰에서 사용하는 셀입니다. - 그리드 형태로 커스터마이징 된 셀을 그립니다. </details> ## 💭 고민한 부분 ### 코드로만 화면 구현하기 - 이번 프로젝트는 유독 스토리보드로 구현하기가 더 쉬웠을 것 같습니다. 하지만 양쪽 모두 할 줄 알아야하기 때문에 이번 프로젝트에서는 코드로만 화면을 구현하는 것을 목표로 했습니다. - 이 과정에서 초기 설정으로 필요한 부분들을 AppDelegate 및 SceneDelegate에 작성하여 진행했습니다. ### 안전한 Collection 사용을 위한 extension - API를 통해 데이터를 요청할 때 한 번에 20개씩 요청을 하고, 마지막에 가서는 20개보다 적게 응답이 오기 때문에 잘못된 인덱스에 접근할 가능성이 있습니다. - 이를 subscript 문법에 대한 메서드 추가로 안전하게 값에 접근하도록 하였습니다. ### 투명한 네비게이션 바 문제 해결하기 - 앱 구동시 네비게이션 바가 투명해져서 네비게이션 바 아이템들이 둥둥 떠다니는 모습이 되어버렸습니다. 이는 iOS가 15버전으로 업데이트 된 후 네비게이션 바가 확장되면서 생긴 문제였습니다. 시뮬레이터의 iOS버전은 15 였고, 시뮬레이터에서 네비게이션 바가 투명해 진 것입니다. - 저희가 원했던 방향성은 네비게이션 바가 항상 불투명하게 자리를 잡는 것이기 때문에 `UINavigationBarAppearance`의 `configureWithDefaultBackground()` 인스턴스 메서드를 사용해서 항상 불투명하게 나오도록 처리하였습니다. ### 이미지를 가져올 때 비동기로 인한 race-condition발생 문제 해결 - 이미지를 서버에서 가져오는 로직의 경우, 이미지의 크기가 커 불러오는 작업을 메인 스레드에서 진행하면 스크롤을 내릴 때 마다 화면이 버벅이게 되어 쾌적한 사용자 경험을 제공하지 못하게 됩니다. - 따라서 이 로직을 메인 스레드가 아닌 다른 스레드에서 진행하도록 하여 메인 스레드는 받아온 이미지를 띄우기만 하도록 로직을 짜보았습니다. - 문제는 이 작업을 비동기적으로 진행하다보니, 이미지를 불러오기 위한 `thumbnail`에 접근하는 과정에서 race-condition이 발생하는 것입니다. 때문에 스크롤을 빠르게 내릴 수록 이미지가 계속 바뀌는 상태가 되었습니다. - 이를 해결하기 위해 이미지를 불러오기 전에 셀이 가지고 있는 `product`와 디스패치 큐로 작업을 넘길 때 캡쳐한 `product`가 일치하는지 확인하는 로직을 추가하였고, `product`가 비교 가능한 상태가 될 수 있도록 `Hashable`을 채택하도록 처리하였습니다. ### 이미지 로딩중임을 사용자에게 알리기 - 이미지를 서버에서 가져오는 로직은 텍스트를 가져오는 것과는 다르게 시간이 걸리는 작업입니다. 따라서, 이미지의 파싱이 끝날 때 까지 사용자는 이미지가 없는 셀을 보다가 이미지가 나중에 나타나는 UI를 보게 될 것입니다. 이는 좋은 사용자 경험이 아니라 판단하여 이미지의 파싱이 끝날 때 까지 로딩중임을 알리는 시각적 정보가 필요하였습니다. - 그래서 이미지와 정확히 같은 위치에 `UIActivityIndicatorView`를 추가하여 이미지가 파싱되어 이미지뷰에 할당될 때까지 로딩중임을 알렸습니다. - 이미지가 할당된 후에는 `UIActivityIndicatorView`의 애니메이션을 멈추고 보이지 않게 바꾸도록 하였습니다. ### 이미지 캐싱하기 - 매번 셀을 dequeue할 때마다 이미지를 서버에서 불러오는 것은 자원 낭비가 너무 심하다 판단되었습니다. 한번 불러온 이미지를 캐싱하여 이 문제를 해결하고자 했습니다. - NSCache<NSString, UIImage>타입의 싱글턴 객체를 갖는 ImageCacheManager 클래스를 구현하였습니다. - 이를 통해 한 번 사용된 이미지는 캐싱하여 성능을 향상시킬 수 있도록 구현하였습니다. ## ❓ 조언을 얻고 싶은 부분 ### segmentedControl과 화면 전환 - 현재 저희는 두 개의 컬렉션 뷰를 가지고 서로 isHidden 상태를 전환하는 방식으로 화면을 변경하고 있습니다. 하나의 컬렉션 뷰를 가지고 레이아웃만 변경해서 화면을 바꾸는 것이 더 적절한 방법인지에 대한 라자냐의 생각이 궁금합니다. 또한 현재는 segmentedControl의 item이 2개지만 더 많은 경우에는 또 어떤 방법을 취하는 것이 좋을까요? ### 스크롤 동기화 - 저희는 segmentedControl을 통한 화면 전환에서 두 화면이 서로 같은 상품을 보여주도록 스크롤 동기화가 있으면 좋겠다고 생각했습니다. - 특정 인덱스패스로 스크롤 해주는 메서드를 사용하고자 cellForItemAt이 호출될 때, indexPath 전달인자를 뷰컨트롤러의 프로퍼티에 저장했다가 화면전환시 해당 인덱스패스로 스크롤하는 `scrollToItem(at:at:animated:)` 메서드를 호출해봤습니다. 하지만 이 방법은 가장 마지막에 반환된 cell에 대한 인덱스로 스크롤되기 때문에 스크롤 방향에 따라서 `at: UICollectionView.ScrollPosition`을 적절히 바꿔줘야 했고, 완벽하게 작동하지도 않았습니다. - 두 컬렉션뷰 레이아웃의 아이템 사이즈의 비율과 근사하게 contentOffset에 2.1을 곱하거나 나눠서 위치를 잡아주는 방법도 시도해봤습니다. 얼추 비슷하게 스크롤되긴 했습니다. - 하지만 더 적절하게 스크롤을 동기화 시키려면 화면에 보이는 첫 번째 셀의 인덱스 패스를 가지고 `scrollToItem(at:at:animated:)`에 `UICollectionView.ScrollPosition.top`와 함께 전달하는 것이 좋겠다고 생각했습니다. 헌데 저희는 화면에 보이는 첫 번째 셀의 인덱스 패스를 어떤 방법으로 알아낼 수 있는지 모르겠습니다. - 현재는 두 컬렉션 뷰의 스크롤을 동기화하지 않았습니다. - 첫 번째 셀의 인덱스 패스를 알아내는 방법이나, 더 좋은 스크롤 동기화에 대한 아이디어가 있다면 조언을 부탁드립니다! ### 모던 컬렉션 뷰사용 - 컬렉션 뷰를 구현하는 방법을 고민하면서, 처음에는 모던 컬렉션 뷰를 사용하여 구현하였습니다. 하지만, 모던 컬렉션 뷰가 어떻게 동작하는지 정확하게 이해하진 못했고, 그저 예제를 보면서 카피코딩만 하고 있는거 같아 모던 컬렉션 뷰가 아닌 기존 컬렉션 뷰로 구현하게 되었습니다. 이 부분은 좀 더 공부한 후에 적용시키자고 생각하였습니다. - 저희가 궁금한 것은 이 모던 컬렉션 뷰와 기존 컬렉션 뷰 둘 중에 어떤 방식이 현업에서 더 많이 사용되는 지 궁금합니다. 회사마다 다르겠지만 라자냐의 의견이 궁금합니다! ### available - 모던 컬렉션뷰를 사용해보았을 때 iOS 14버전부터 사용할 수 있다는 available이 뜨게 되었습니다. - 이런 경우 14버전보다 낮은 버전에서는 해당 기능을 사용할 수 없기 때문에 버전 분기처리를 통한 같은 기능 구현을 필요로 할텐데요! 저희가 궁금한 점은 버전 분기처리는 보통 몇 버전정도 이전까지 구현하는 것이 보편적인가 하는 것이었습니다. - 라자냐는 현재 몇 버전까지 구현해주는 것이 좋다고 생각하시나요? 오픈마켓 [Step 1] Gundy, Wonbi @wonhee009 안녕하세요 라자냐~ 첫 번째 PR을 보내게 되어서 기분이 좋자냐~ 여러부분 신경써서 구현하고자 하였습니다! 부족한 부분이나 잘못된 부분이 있다면 가차없는 피드백 부탁드립니다!! 잘 부탁 드립니다!! ## ✓ 진행단계 : Step-1 ## 📝 STEP 진행 중 경험하고 배운 것 - JSONParsing ☑️ DTO 생성 - Networking 구현 ☑️ URLSession을 활용한 서버와의 데이터 통신 ☑️ 각 네트워킹 요소를 프로토콜을 이용하여 추상화 - Test Double 작성 ☑️ 테스트를 위한 객체 생성 ☑️ Unit Test 진행 ## ⚒️ 코드 구현내용 <details> <summary>구현내용</summary> ### 구현 - Network - HttpMethod - HttpMethod를 나타내는 열거형 타입입니다. - NetworkRequest - 네트워킹을 위한 URL과 Request를 가지고 이를 구현하기 위한 필수 프로퍼티를 선언하는 프로토콜입니다. - NetworkManager - 네트워크에서 데이터를 가져와 오류를 처리하고 데이터를 파싱해주는 객체입니다. - URLSessionProtocol - DIP적용을 위해 `dataTask`메서드를 정의하는 프로토콜입니다. - 이 프로토콜을 채택하면 `dataTask`메서드의 로직을 구현해주어야 합니다. - URLSessionDataTaskProtocol - `URLSessionProtocol`의 `dataTask`메서드에서 반환하는 타입을 지정하는 프로토콜입니다. - 이 프로토콜을 채택하면 `resume`메서드의 로직을 구현해주어야 합니다. - Extension - JSONDecoder - 제네릭 타입과 데이터를 받아 디코딩하는 타입 메서드를 추가하였습니다. - String - `"yyyy-MM-dd'T'HH:mm:ss"`의 형식의 문자열을 `Date`타입의 값으로 변경시켜주는 메서드를 추가하였습니다. - OpenMarket - Product - `Codable`을 채택하는 DTO입니다. - ProductList - `Codable`을 채택하는 DTO입니다. - HealthCheckerRequest - NetworkRequest를 채택하고, Application HealthChekcer를 리퀘스트하기위한 프로퍼티를 갖고 있는 구조체입니다. - ProductListRequest - NetworkRequest를 채택하고, 상품 리스트 조회를 리퀘스트하기위한 프로퍼티를 갖고 있는 구조체입니다. - ProductDetailRequest - NetworkRequest를 채택하고, 상품 상세 조회를 리퀘스트하기위한 프로퍼티를 갖고 있는 구조체입니다. - OpenMarketTests - products - 테스트를 위한 Mock JSON데이터입니다. - DataLoader - Mock JSON데이터를 코드로 연결시켜 data를 생성해주는 클래스입니다. - DummyData - 실제 네트워킹이 아닌 테스트를 진행할 때 반환할 데이터 구조체입니다. - MockURLSession - 테스트를 위해 실제 네트워킹이 아니라 `DummyData`를 사용하는 클래스입니다. - MockURLSessionDataTask - 테스트를 위해 실제 네트워킹 테스트가 아닌 `DummyData`를 반환하는 클래스 입니다. ### Unit Test 작성 - JSONDecoder, DTO - JSONParsingTests - NetworkManager - NetworkManagerTests - OpenMarketNetworkRequest - NetworkRequestTests </details> ## 💭 고민한 부분 ### 테스트용 JSON 파일과 서버 API 문서 - 이번 프로젝트 안내페이지에서 제공하는 테스트용 JSON 파일과 앞으로 작업을 진행할 API의 JSON이 서로 CodingKey나 value의 형식이 달랐습니다. - 우선 테스트용 파일에 맞게 DTO를 구현했었는데, 실제 API Network를 진행해보면서 서로 다르다는 것을 알게 되었습니다. - 테스트의 취지가 네트워크가 없는 상황에서도 정상적으로 동작하는지를 검증하는 것이기 때문에 서버 API 문서의 데이터 형식에 맞추는 것이 적절하다고 생각해 테스트용 JSON 파일을 서버 API 문서의 데이터 형식에 맞게 수정하였습니다. ### DIP적용을 위한 Protocol 구현하기 - 실제로 네트워킹을 하지 않고, 정상적으로 fetch가 진행되는지 로직을 테스트 하기위해 `MockURLSession`이라는 test double객체를 만들고 `MockData` 객체를 반환하도록 로직을 구현해야 했습니다. - 이 과정에서 URLSession의 dataTask메서드에서 네트워킹 하는 부분을가로채 실제 네트워킹이 아니라 미리 만들어둔 `MockData` 객체를 반환하게 구현하여야 했습니다. - 실제 `URLSession`과 이런 `MockURLSession`은 다르게 작동해야 하므로, 프로토콜을 통해 의존성을 역전시켜 네트워크 매니저가 프로토콜을 바라보게 하여 테스트를 할 때는 `MockURLSession`을 주입시키고, 실제 네트워킹을 할 때는 `URLSession`을 주입시키는 방법으로 진행해 보았습니다. - 이 때 `URLSessionDataTask`타입도 Mock데이터로 만들어 주었는데, 이 타입의 초기화 구문이 iOS13버전 부터 더 이상 사용되지 않는(deprecated) 로직이어서 이 타입도 프로토콜을 채택하게 하여 구현 해 보았습니다. ## ❓ 조언을 얻고 싶은 부분 ### 재사용이 가능한 Request객체 구현하기 - 이번 프로젝트에서 네트워킹의 요소가 총 3가지 였습니다. - 그리고 실제 오픈마켓 서버에서도 GET메서드 말고도 POST, PATCH, DEL 등의 httpMethod를 사용할 수 있었습니다. - 이에 각각의 리퀘스트마다 URL주소가 달라졌었고, 이를 String으로 써주는건 코드의 재사용이 많고, 확장성에도 문제가 있어보인다 판단하였습니다. - 그래서 `NetworkRequest` 프로토콜을 만들고, 각 네트워킹 요소를 객체화하여 이를 채택하도록 하였습니다. - 이로써 URL을 String으로 직접 작성할 필요가 없이 프로토콜을 채택하고 그 프로토콜을 각자의 역할에 맞게 구현만 해주면 알아서 URL과 Request를 만들게 되었습니다. - 혹시 저희가 구현한 `NetworkRequest`에서 부족한 부분이나 잘못된 부분, 혹은 개선할 부분이 있을까요?? ### 각 파일의 위치가 적절한지 - 테스트를 위한 JSON 파일은 `OpenMarketTests` 내부에 위치시켰고, 범용적으로 네트워크에 사용할 수 있는 프로토콜이나 타입은 `OpenMarket` 외부에 `Network`라는 그룹을 따로 생성하여 관리하였습니다. Network 기능을 수행하지만 그 용도가 OpenMarket 프로젝트에만 사용될 `OpenMarketNetworkRequest` 등은 `OpenMarket` 내부에 위치시켰습니다. - 저희가 네트워크 통신 작업이 처음이다보니, 이런 방식으로 파일을 정리하는 것이 적절한지 조언을 얻고 싶습니다. ### BoringSSL에 대하여 - Step 1-2를 진행하면서 서버와 실제로 데이터를 주고 받을 때, `boringssl_metrics_log_metric_block_invoke(153)` 라는 메시지가 콘솔에 뜨게 되었습니다. - [boringssl_metrics_log_metric_block_invoke(151) Failed to log metrics](https://github.com/firebase/firebase-ios-sdk/issues/9262)의 내용 및 여러 게시글들을 찾아본 결과 기능에는 문제가 없고, 콘솔에 나타나기만 하는 메시지로 파악했습니다. - 스키마에서 OS_ACTIVITY_MODE를 disable로 바꿔주면 이 에러 메세지가 콘솔에서 나타나지 않지만, `NSLog`도 사라지는 문제가 있어서 이 방법은 적절한 방법이 아니라 판단하였습니다. - 또 다른 방법으로는 `xcrun simctl spawn booted log config --subsystem com.apple.network --category boringssl --mode level:off` 처럼 Xcode 설정을 건드리는 방법도 있었는데, 이는 Xcode의 설정을 건드리는 것이고, 어떤 사이드 이펙트가 발생할지 몰라, 적용하기에 적절하지 않다 판단하여 적용하지 않았습니다. - 혹시 이 메시지를 신경쓰지 않고 작업을 이어나가도 괜찮은지 여쭙고 싶습니다! ### Type for Diagram - AddProductViewController: UIViewController - FormatConverter - dateFormatter: DateFormatter - numberFormatter: NumberFormatter - date(from:) - number(from:) - GridCollectionViewCell: UICollectionViewCell - product: Product? - loadingView: UIActivityIndicatorView - productImage: UIImageView - productName: UILabel - price: UILabel - stock: UILabel - labelStackView: UIStackView - reuseIdenifier: String? - prepareForReuse() - setupCellConstraints() - configureCell(from:) - setupImage(from:) - setupPrice(from:) - setupStock(from:) - ListCollectionViewCell: UICollectionViewCell - product: Product? - loadingView: UIActivityIndicatorView - productImage: UIImageView - productName: UILabel - price: UILabel - stock: UILabel - disclosureImage: UIImageView - stockStackView: UIStackView - productStackView: UIStackView - reuseIdenifier: String? - prepareForReuse() - setupCellConstraints() - configureCell(from:) - setupImage(from:) - setupPrice(from:) - setupStock(from:) - ImageCacheManager - shared: NSCache<NSString, UIImage> - LayoutType - case list - case grid - index: Int - ProductsViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate - networkManager: NetworkManager - productList: [ProductList] - pageNumber: Int - isInfiniteScroll: Bool - listCollectionView: UICollectionView - gridCollectionView: UICollectionView - segmentedControl: UISegmentedControl - addProductButton: UIBarButtonItem - setupCollectionView() - makeLayout(_:) - fetchData(_:) - setupCollectionViewConstraints() - addTarget() - addNewProduct() - changeLayout(_:) - numberOfSections(in:) - collectionView(_:numberOfItemInSection:) - collectionView(_:cellForItemAt) - scrollViewDidScroll(_:) --- - Data+ - append(_:using:) - ImageCell: UICollectionViewCell - plusImageView: UIImageView - productImageView: UIImageView - configure() - EditProduct: Codable - productID: Int - name: String? - description: String? - stock: Int? - thumbnailID: Int? - discountedPrice: Double? - price: Double? - currency: Currency? - secret: String - CodingKeys: String, CodingKey - case stock - case productID = "product_id" - case name - NewProduct: Encodable - AddProductViewController: UIViewController - leftButton: UIBarButtonItem - rightButton: UIBarButtonItem - imageCollectionView: UICollectionView - dataSource: UICollectionViewDiffableDataCource<Int, Int>! - configureNavigationBar() - addTarget() - tapCancelButton() - tapDoneButton() - ... ---

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully