2023년 10월 10일 ~ 2023년 11월 17일 (6주)
iDo는 카테고리를 통해 좀 더 쉽게 같은 취미를 가진 사람들과 어울릴 수 있습니다.
내성적인 사람을 포함한 2030세대 모두가 어려움 없이 같이 취미를 공유한다는 즐거움을 느낄 수 있도록 기회를 제공하는 것이 목표입니다.
-
공통
- 로그인 유저 데이터 동기화
- 가입 유저 데이터 동기화
- 이미지 캐싱 작업
-
로그인/회원가입 페이지
- 아이디 및 비밀번호 비교
- 이메일 유효성 검사 (중복 가입 방지)
- 이메일을 이용한 인증번호
- 비밀번호 유효성 검사 (영문자, 숫자, 특수기호 필수)
-
홈 페이지
- 가입한 모임 목록 표시
- 가입한 모임으로 이동
-
카테고리 페이지
- 9가지 카테고리 중 1가지 선택
-
모임 페이지
- 모임 커버 이미지, 모임 이름, 모임 소개 표시
- 가입 멤버 목록
- 모임장 표시
- 프로필 이미지 클릭 시, 해당 유저 프로필 확인
- 모임 생성
- 모임 생성 시, 3:2 비율로 자르기
-
게시글 페이지
- 제목, 내용, 이미지, 작성 시간 표시
- 게시글 추가/삭제/수정 기능
- 댓글 추가/삭제/수정 기능
- 프로필 이미지 클릭 시, 해당 유저 프로필 확인
- 게시글 신고하기
- 댓글 신고하기
-
마이 프로필 페이지
- 나의 프로필 조회
- 프로필 수정
- 로그아웃, 서비스 탈퇴
홍준영 | 김도현 | 강지훈 | 한동연 | 이애라 |
---|---|---|---|---|
☀️ 리더 | ️🌙 부리더 | 🛠️ 개발자 | 🛠️ 개발자 | 🛠️ 개발자 |
게시글 목록/게시글 생성 및 수정 | 게시글 상세/댓글 | 회원가입 | 모임 생성/수정 | 홈 화면 |
회원탈퇴 | 이미지 캐싱 | 로그아웃 | 이미지 유효성 검사 기능 | 마이 프로필 화면 |
신고기능 | 신고기능 | 카테고리 | 이미지 편집 | 상단 로고 추가 |
-
Firebase
로그인 및 회원가입 진행 시유저의 데이터를 가져오고 비교하기 위해
Authentication에 데이터 동기화를 위해서 사용
모임/게시글/댓글 등각각 콘텐츠의 CRUD 기능을 위한 데이터를 활용하기 위해
Realtime에 데이터 동기화를 위해서 사용
콘텐츠, 유저와 관련된이미지를 관리하고 캐싱하기 위해
Storage에 이미지 저장을 위해서 사용 -
SnapKit
코드 기반 UI 작업의 효율성
을 높이기 위해서 사용Auto Layout
을 쉽게 설정하기 위해 사용 -
Tabman
상단 탭바 추가
를 위해 사용 -
TOCropViewController
이미지 비율을 3:2로 편집
을 쉽게 하기 위해 사용
- 원인 : 각 페이지마다 데이터를 관리하는 Manager 클래스를 각각 인스턴스화 시켜서 사용
private let firebaseManager = FirebaseManager()
- 해결 : 초기화 시, 상위 페이지이의 Manager 클래스 인스턴스를 전달 받아서 사용
var firebaseManager: FirebaseManager init(firebaseManager: FirebaseManager) { self.firebaseManager = firebaseManager super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
- 원인 : 유저가 콘텐츠를 즐기거나, 화면과 상호작용을 할 때 마다 사용자의 정보를 불러오고 있음. 즉, 앱을 사용할 때 사용자와 함께 이동하는 정보 저장 요소가 없음
- 해결 : 로그인회원가입 시 생성된 유저정보를 싱글톤으로 관리
final class MyProfile { static let shared = MyProfile() private var firebaseManager: MyProfileUpdateManager! private var ref: DatabaseReference = Database.database().reference() private var fileCache: ProfileImageCache = ProfileImageCache() var myUserInfo: MyUserInfo? private init() {} . . . }
// 사용 예시 DispatchQueue.main.async { guard let nickName = MyProfile.shared.myUserInfo?.nickName else { return } self.instructions.text = "\(nickName)님, 카테고리를 선택해보세요!" }
- 원인 : 데이터 처리가 완료되는 시점을 알 수 없음, 순차적으로 진행이 되게 하는 로직이 없음
- 해결 : 데이터 처리 함수에
completion
을 추가해서 데이터 처리가 완료된 시점에 화면 전환이 이루어지게 함func createNoticeBoard(title: String, content: String, completion: @escaping (Bool) -> Void) { let ref = Database.database().reference().child("noticeBoards").child(club.id) let newNoticeBoardID = ref.childByAutoId().key ?? "" . . . self.uploadImages(noticeBoardID: newNoticeBoardID, imageList: self.newSelectedImage) { success, imageURLs in if success { . . . completion(success) } } else { completion(false) } } }
// 사용 예시 // 새로운 메모 작성 @objc func finishButtonTappedNew() { navigationItem.rightBarButtonItem?.isEnabled = false if isTitleTextViewEdited, isContentTextViewEdited { guard let newTitleText = createNoticeBoardView.titleTextView.text else { return } guard let newContentText = createNoticeBoardView.contentTextView.text else { return } firebaseManager.createNoticeBoard(title: newTitleText, content: newContentText) { success in if success { self.navigationController?.popViewController(animated: true) print("게시판 생성 성공") } else { self.navigationItem.rightBarButtonItem?.isEnabled = true print("게시판 생성 실패") } } } else { navigationItem.rightBarButtonItem?.isEnabled = true } }
- 원인 : 데이터는 Manager 클래스에서 관리하고, 뷰는 ViewController에서 관리함
- 해결 : Delegate 패턴을 사용하여, 데이터 관련 함수가 호출될 때 해당 TableView reload
protocol FirebaseManagerDelegate: AnyObject { func reloadData() } class FirebaseManager { weak var delegate: FirebaseManagerDelegate? . . . { if success { self.addMyNoticeBoard(noticeBoard: newNoticeBoard) self.noticeBoards.insert(newNoticeBoard, at: 0) self.delegate?.reloadData() } } . . }
// 사용 예시 - ViewController에서 해당 protocol 상속 extension NoticeBoardViewController: FirebaseManagerDelegate { func reloadData() { selectView() noticeBoardView.noticeBoardTableView.reloadData() } }
- 원인 : 이미지와 버튼의 크기가 다름, 이미지를 강제로 컴포넌트에 맞추기 때문에 이미지가 3:2 비율이 아니라면 부자연스럽게 늘어나거나, 축소됨
- 해결 : 이미지 버튼 사이즈를 3:2로 변경, TOCropViewController 라이브러리 사용하여 이미지 추가 시, 이미지 편집
let cropViewController = TOCropViewController(croppingStyle: .default, image: selectedImage) cropViewController.delegate = self cropViewController.customAspectRatio = CGSize(width: 3, height: 2) // 비율 3:2 cropViewController.aspectRatioLockEnabled = true // 비율 선택 잠금 cropViewController.resetAspectRatioEnabled = false // 비율 리셋 막음 cropViewController.aspectRatioPickerButtonHidden = true // 비율 변경 토글 히든 picker.dismiss(animated: true) { self.present(cropViewController, animated: true, completion: nil) }
- 원인 : 사용자가 보는 이미지에 비해 매우 큰 사이즈의 이미지를 저장하고 불러오기 때문, Firebase Storage는 따로 캐싱작업을 해주지 않음
- 해결 : compressionQuality 를 사용하여 이미지를 압축하여 저장, storage에 metadata에 있는 md5hash값과 로컬에 있는 이미지 데이터를 md5hash로 변환하여 비교하여 다를 경우 서버에서 이미지를 가져와 캐싱된 이미지를 변경함
if let image = profileImageButton.image(for: .normal) { imageData = image.jpegData(compressionQuality: 0.5) // 이미지 품질 }
extension Data { var md5Hash: String { let hash = Insecure.MD5.hash(data: self) return Data(hash).base64EncodedString() } }
//사용 예시 if let localDataHash = cacheImage.pngData()?.md5Hash, let storageDataHash = metadata?.md5Hash, localDataHash == storageDataHash { return }
- 원인 : 기기 별로 화면의 크기가 다름, 오토레이아웃을 정확한 수치로 지정
// 높이를 수치로 설정 meetingDescriptionTextView.snp.makeConstraints { make in make.top.equalTo(countMeetingNameLabel.snp.bottom).offset(Constant.margin4) make.centerX.equalTo(scrollView) make.left.right.equalTo(scrollView).inset(Constant.margin4) make.height.equalTo(160) }
- 해결 : lessThanOrEqualTo과 greaterThanOrEqualTo 이용해서 레이아웃 설정
// 높이의 최대, 최소 지정 meetingDescriptionTextView.snp.makeConstraints { make in make.top.equalTo(countMeetingNameLabel.snp.bottom).offset(Constant.margin4) make.centerX.equalTo(scrollView) make.left.right.equalTo(scrollView).inset(Constant.margin4) make.height.lessThanOrEqualTo(160) make.height.greaterThanOrEqualTo(100) }