- ๐ ํ๋ก์ ํธ ๊ธฐ๊ฐ
- โญ๏ธ ํ๋ก์ ํธ ์๊ฐ
- ๐ ๊ตฌํ ๊ธฐ๋ฅ
- ๐ฉ๐ปโ๐ป Contributors
- ๐ ๏ธ Tech Stack
- ๐น ์ฌ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- โ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ์ด์
- ๐ซ ํธ๋ฌ๋ธ ์ํ
2023๋ 10์ 10์ผ ~ 2023๋ 11์ 17์ผ (6์ฃผ)
์ฑ ๋ฐฐํฌ, ํ์ด์ด๋ฒ ์ด์ค ์ฐ๋, ์ด๋ฏธ์ง ๋ค๋ฃจ๊ธฐ
-
๊ณตํต
- ์ด๋ฏธ์ง ์บ์ฑ ์์
- ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ์์
- ํ์ด ํ๋ก๊ทธ๋๋ฐ ๋์
-
ํ ํ์ด์ง
- ๊ฐ์ ํ ๋ชจ์ ๋ชฉ๋ก ํ์
-
๋ชจ์ ํ์ด์ง
- ๊ฐ์ ๋ฉค๋ฒ ๋ชฉ๋ก
- ๋ชจ์์ฅ ํ์
-
๊ฒ์๊ธ ํ์ด์ง
- ์ ๋ชฉ, ๋ด์ฉ, ์ด๋ฏธ์ง, ์์ฑ ์๊ฐ ํ์
- ๊ฒ์๊ธ ์ถ๊ฐ/์ญ์ /์์ ๊ธฐ๋ฅ
- ๋๊ธ ์ถ๊ฐ/์ญ์ /์์ ๊ธฐ๋ฅ
- ํ๋กํ ์ด๋ฏธ์ง ํด๋ฆญ ์, ํด๋น ์ ์ ํ๋กํ ํ์ธ
- ๊ฒ์๊ธ ์ ๊ณ ํ๊ธฐ
- ๋๊ธ ์ ๊ณ ํ๊ธฐ
ํ์ค์ | ๊น๋ํ | ๊ฐ์งํ | ํ๋์ฐ | ์ด์ ๋ผ |
---|---|---|---|---|
โ๏ธ ๋ฆฌ๋ | ๏ธ๐ ๋ถ๋ฆฌ๋ | ๐ ๏ธ ๊ฐ๋ฐ์ | ๐ ๏ธ ๊ฐ๋ฐ์ | ๐ ๏ธ ๊ฐ๋ฐ์ |
๊ฒ์๊ธ ๋ชฉ๋ก/๊ฒ์๊ธ ์์ฑ ๋ฐ ์์ | ๊ฒ์๊ธ ์์ธ/๋๊ธ | ํ์๊ฐ์ | ๋ชจ์ ์์ฑ/์์ | ํ ํ๋ฉด |
ํ์ํํด | ์ด๋ฏธ์ง ์บ์ฑ | ๋ก๊ทธ์์ | ์ด๋ฏธ์ง ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ๋ฅ | ๋ง์ด ํ๋กํ ํ๋ฉด |
์ ๊ณ ๊ธฐ๋ฅ | ์ ๊ณ ๊ธฐ๋ฅ | ์นดํ ๊ณ ๋ฆฌ | ์ด๋ฏธ์ง ํธ์ง | ์๋จ ๋ก๊ณ ์ถ๊ฐ |
-
Firebase
๋ก๊ทธ์ธ ๋ฐ ํ์๊ฐ์ ์งํ ์์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ ๋น๊ตํ๊ธฐ ์ํด
Authentication์ ๋ฐ์ดํฐ ๋๊ธฐํ๋ฅผ ์ํด์ ์ฌ์ฉ
๋ชจ์/๊ฒ์๊ธ/๋๊ธ ๋ฑ๊ฐ๊ฐ ์ฝํ ์ธ ์ CRUD ๊ธฐ๋ฅ์ ์ํ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ๊ธฐ ์ํด
Realtime์ ๋ฐ์ดํฐ ๋๊ธฐํ๋ฅผ ์ํด์ ์ฌ์ฉ
์ฝํ ์ธ , ์ ์ ์ ๊ด๋ จ๋์ด๋ฏธ์ง๋ฅผ ๊ด๋ฆฌํ๊ณ ์บ์ฑํ๊ธฐ ์ํด
Storage์ ์ด๋ฏธ์ง ์ ์ฅ์ ์ํด์ ์ฌ์ฉ -
SnapKit
์ฝ๋ ๊ธฐ๋ฐ UI ์์ ์ ํจ์จ์ฑ
์ ๋์ด๊ธฐ ์ํด์ ์ฌ์ฉAuto Layout
์ ์ฝ๊ฒ ์ค์ ํ๊ธฐ ์ํด ์ฌ์ฉ -
Tabman
์๋จ ํญ๋ฐ ์ถ๊ฐ
๋ฅผ ์ํด ์ฌ์ฉ -
TOCropViewController
์ด๋ฏธ์ง ๋น์จ์ 3:2๋ก ํธ์ง
์ ์ฝ๊ฒ ํ๊ธฐ ์ํด ์ฌ์ฉ
๋ฐ์ดํฐ๊ฐ ์ฒ๋ฆฌ๋๊ธฐ ์ ์, ํ๋ฉด ์ ํ์ด ๋๋์ ์ํ๋ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ฌ์ง์ง ์์
- ์์ธ : ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋๋ ์์ ์ ์ ์ ์์, ์์ฐจ์ ์ผ๋ก ์งํ์ด ๋๊ฒ ํ๋ ๋ก์ง์ด ์์
- ํด๊ฒฐ : ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํจ์์
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 } }
- ์์ธ : ์ฌ์ฉ์๊ฐ ๋ณด๋ ์ด๋ฏธ์ง์ ๋นํด ๋งค์ฐ ํฐ ์ฌ์ด์ฆ์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ๊ณ ๋ถ๋ฌ์ค๊ธฐ ๋๋ฌธ, Firebase Storage๋ ๋ฐ๋ก ์บ์ฑ์์ ์ ํด์ฃผ์ง ์์
- ํด๊ฒฐ : ์ด๋ฏธ์ง ์ฌ์ด์ฆ ์กฐ์ ์ ๊ตฌํํ์ฌ ์ ์ฅ, storage์ metadata์ ์๋ updated๊ฐ๊ณผ ๋ก์ปฌ์ ์๋ ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ updated ๋น๊ตํ์ฌ ๋ค๋ฅผ ๊ฒฝ์ฐ ์๋ฒ์์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ ์บ์ฑ๋ ์ด๋ฏธ์ง๋ฅผ ๋ณ๊ฒฝํจ
- ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ์์
extension UIImage {
func resizeImage(targetSize: CGSize) -> UIImage {
let size = self.size
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
var newSize: CGSize
if widthRatio > heightRatio {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
self.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
guard let smallImageData = image.resizeImage(targetSize: CGSize(width: 90, height: 90)).pngData(),
let mediumImageData = image.resizeImage(targetSize: CGSize(width: 480, height: 480)).pngData() else { return }
let datas = [smallImageData, mediumImageData]
imageUpload(datas: datas, uid: uid)
- ์ด๋ฏธ์ง ์บ์ฑ ์์
final class CacheImage {
var image: UIImage
var updated: Date?
init(image: UIImage, updated: Date?) {
self.image = image
self.updated = updated
}
}
var cacheImage: CacheImage? = nil
if let image = imageCache.object(forKey: storagePath as NSString) {
completion(.success(image.image))
cacheImage = image
}
let storage = Storage.storage().reference(withPath: storagePath)
storage.getMetadata { metadata, error in
if let error {
completion(.failure(error))
return
}
if let cacheImageUpdated = cacheImage?.updated,
let storageDataUpdated = metadata?.updated,
cacheImageUpdated == storageDataUpdated {
return
}
.
.
- ์์ธ : ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๊บผ๋ฒ์ ๊ฐ์ ธ์์ ์๊ธด ๋ฌธ์
- ํด๊ฒฐ : ํ์ด์ง ๋ค์ด์ ์ ๊ตฌํํ์ฌ ๋ฐ์ดํฐ๋ฅผ 10๊ฐ์ฉ ๋๋ ์ ๊ฐ์ ธ์ค๋๋ก ๊ตฌํ
var query = defaultRef.queryOrderedByKey()
if let lastClubId {
query = query.queryStarting(atValue: lastClubId)
}
query = query.queryLimited(toFirst: 10)
query.getData { error, datasnapshot in
.
.
// ์คํฌ๋กค๋ทฐ์์ ์ผ์ ๋์ด๊ฐ ๋์ด๊ฐ๋ฉด ์๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ก ๊ตฌํ
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height && !isLoading {
loadData()
}
}
- ์์ธ : ๊ธฐ๊ธฐ ๋ณ๋ก ํ๋ฉด์ ํฌ๊ธฐ๊ฐ ๋ค๋ฆ, ์คํ ๋ ์ด์์์ ์ ํํ ์์น๋ก ์ง์
// ๋์ด๋ฅผ ์์น๋ก ์ค์ 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) }