Skip to content

Commit

Permalink
merge #372: README 업데이트 및 이미지 캐싱 개선
Browse files Browse the repository at this point in the history
[merge] README 업데이트 및 이미지 캐싱 개선
  • Loading branch information
0inn authored Dec 14, 2023
2 parents dcaf973 + 781d17f commit 8aec978
Show file tree
Hide file tree
Showing 23 changed files with 134 additions and 47 deletions.
84 changes: 63 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,80 @@
# <img align="left" src="https://github.com/boostcampwm2023/iOS07-traveline/assets/74968390/4e4c84da-d6da-445c-8328-21828a4c162a" width="120px"> 🌎 traveline
> 여행 일정을 타임라인을 **기록**하고 **공유**하는 서비스 Traveline 입니다.
나만의 여행을 공유하고 다양한 여행을 만나보아요 :)
# 🌎 traveline
> 여행 일정을 타임라인으로 **기록**하고 **공유**하는 **여행 SNS 서비스**입니다.
나만의 여행을 공유하고 다양한 여행들을 만나보아요 :)

<br>

![기능 소개 페이지](https://github.com/boostcampwm2023/iOS07-traveline/assets/51712973/f39271ee-26a8-41d2-b18d-32d44f79fd43)

### 주요 기능
**나랑 비슷한 취향과 환경을 가진 다양한 여행들을 만나고 나만의 여행 계획을 짜보아요 !**
</br>

#### 나랑 비슷한 취향과 환경을 가진 다양한 여행들을 만나고 나만의 여행 계획을 짜보아요 !
- 지역, 기간, 테마 등 총 8 가지의 필터를 통해 내가 원하는 여행을 찾을 수 있어요.
- 키워드 검색을 통해 원하는 여행을 바로 탐색할 수 있어요.
- 매 날짜마다 시간 순으로 기록된 여행들을 통해 보다 쉽게 일정을 확인할 수 있어요.
- 지도로 보기 기능을 통해 어떤 장소에 머물렀는지 지도로 한눈에 볼 수 있어요.

### Traveline 팀 소개
<br>

#### Traveline 팀 소개
|S009 김영인|S013 김태현|S041 홍기웅|J048 박경미|J170 황정민|
|:-:|:-:|:-:|:-:|:-:|
|<img src="https://avatars.githubusercontent.com/u/74968390?v=4" width=150>|<img src="https://user-images.githubusercontent.com/51712973/280571628-e1126b86-4941-49fc-852b-9ce16f3e0c4e.jpg" width=150>|<img src="https://avatars.githubusercontent.com/u/91725382?s=400&u=29b8023a56a09685aaab53d4eb0dd556254cd902&v=4" width=150>|<img src="https://github.com/boostcampwm2023/iOS07-Trapture/assets/74968390/76bfffde-8ebc-445d-8f3a-7c21288ae386" width=150>|<img src="https://github.com/boostcampwm2023/iOS07-Trapture/assets/74968390/3f5281e2-d233-49d2-b836-be2a56f93096" width=150>|
|[@0inn](https://github.com/0inn)|[@kth1210](https://github.com/kth1210)|[@otoolz](https://github.com/otoolz)|[@kmi0817](https://github.com/kmi0817)|[@yaongmeow](https://github.com/yaongmeow)|
|iOS|iOS|iOS|BE|BE|

## 프로젝트 구조
### 🍎 iOS
<br>

### 💽 BE
## 프로젝트 구조
|iOS|![프로젝트 구조도](https://github.com/boostcampwm2023/iOS07-traveline/assets/74968390/d4477585-eac2-481c-baf7-80d394eacaae)|
|:-:|:-:|
|BE|![image](https://github.com/boostcampwm2023/iOS07-traveline/assets/62174395/79d2c0a6-4e8c-4547-acd6-a2b4ea95ce68)|

<br>

## 🍎 iOS 기술 스택
## iOS 기술 스택

#### MVVM + CleanArchitecture
- 현재 서비스 기획 간 복잡한 아키텍처의 필요성을 느끼지 못했고, 작업 간 로직 분리의 용이함을 위해 선택했습니다.
- 또한 여러 명이 동시에 개발해야하기 때문에 CleanArchitecture를 활용해 역할 분리를 확실히 해두어 코드의 통일성을 유지했습니다.
- Repository 패턴을 통해 Data Source를 캡슐화할 수 있었고, RepositoryMock을 이용해 서버 개발 상황에 관계없이 개발을 진행할 수 있었습니다.
#### Combine
- Third-party인 RxSwift에 비해 시간, 공간적 성능이 우수하기 때문에 First-party인 Combine을 선택했습니다.
- 데이터 스트리밍을 조작하고 구독하는 것에 최적화된 Combine을 UI Binding에 활용했습니다.
#### Swift Concurrency
- Concurrency를 활용해 단발성 비동기 응답인 네트워크 로직을 보다 직관적으로 처리해주기 위해 선택했습니다.
- Repository - Network의 흐름을 간결하게 작성할 수 있었습니다.
#### MapKit
- 지도 위에 마커를 표시하고 정보를 보여주기 위해 사용했습니다.
- 위치 좌표 정보만 서버에서 받아오면 되기 때문에 다른 Third-party 지도를 사용할 필요가 없다고 생각해 애플 프레임워크인 MapKit을 선택했습니다.
#### Keychain
- 로그인 이후 토큰 등 민감한 사용자 정보를 안전하게 저장하고 관리하기 위해 사용했습니다.

### MVVM + CleanArchitecture
### Combine
### Swift Concurrency
### MapKit
### Keychain
<br>

## 💽 BE 기술 스택
## BE 기술 스택

#### NestJS
#### TypeORM
#### MySQL
#### Swagger
#### NCP
#### Server
#### VPC
#### AI APIs
- Papago Translation
- GreenEye
#### Docker
#### Docker Hub
#### Domain
#### Nginx
#### GitHub Actions
#### AWS - SES (Simple Email Service)

<br>

## 🔥 우리의 도전들
### 🍎 iOS
### iOS
#### - [단방향 플로우 ViewModel 구현기](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BiOS%5D-%EB%8B%A8%EB%B0%A9%ED%96%A5-%ED%94%8C%EB%A1%9C%EC%9A%B0-ViewModel-%EA%B5%AC%ED%98%84%EA%B8%B0)
#### - [Concurrency와 Combine의 공존](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BiOS%5D-Concurrency%EC%99%80-Combine%EC%9D%98-%EA%B3%B5%EC%A1%B4)
#### - [traveline의 이미지 관리하기](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BiOS%5D-traveline%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0)
Expand All @@ -48,11 +85,16 @@
#### - [MapKit으로 타임라인 보여주기](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BiOS%5D-MapKit%EC%9C%BC%EB%A1%9C-%ED%83%80%EC%9E%84%EB%9D%BC%EC%9D%B8-%EB%B3%B4%EC%97%AC%EC%A3%BC%EA%B8%B0)
#### - [작업을 서로 바꿔서 해보자!](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BiOS%5D-%EC%9E%91%EC%97%85%EC%9D%84-%EC%84%9C%EB%A1%9C-%EB%B0%94%EA%BF%94%EC%84%9C-%ED%95%B4%EB%B3%B4%EC%9E%90!)

### 💽 BE


### BE
#### - [생애 첫 CI CD 도전기](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-%EC%83%9D%EC%95%A0-%EC%B2%AB-CI-CD-%EB%8F%84%EC%A0%84%EA%B8%B0)
#### - [@nestjs/swagger로 이미지 업로드 form을 어떻게 만들지?](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-@nestjs-swagger%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-form%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%A7%80%3F)
#### - [클라이언트에서만 발생하는 413 Content Too Large 에러?](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%97%90%EC%84%9C%EB%A7%8C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-413-Content-Too-Large-%EC%97%90%EB%9F%AC%3F)
#### - [트랜잭션은 쿼리문만 다루지, 스토리지는 신경 쓰지 않아요](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%80-%EC%BF%BC%EB%A6%AC%EB%AC%B8%EB%A7%8C-%EB%8B%A4%EB%A3%A8%EC%A7%80,-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80%EB%8A%94-%EC%8B%A0%EA%B2%BD-%EC%93%B0%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94)
#### - [애플 로그인 구현 과정](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-애플-로그인-구현-과정)
#### - [이메일 서비스 적용 과정](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-이메일-서비스-적용-과정)
#### - [로그인 로그아웃 처리](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%5BBE%5D-로그인-로그아웃-처리)

<br>

|📚 문서|[Wiki](https://github.com/boostcampwm2023/iOS07-traveline/wiki)|[팀 노션](https://spiky-rat-16e.notion.site/6b9791faac7e4b9d9a31d225ce8cd157?pvs=4)|[그라운드 룰](https://github.com/boostcampwm2023/iOS07-Trapture/wiki/%E2%9C%A8-%ED%8C%80-%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%A3%B0)|[컨벤션](https://github.com/boostcampwm2023/iOS07-Trapture/wiki/%F0%9F%93%81-%EC%BB%A8%EB%B2%A4%EC%85%98)|[회의록](https://www.notion.so/bd676cad762c4cffa7b081c65939b0c5?v=76fe42efa9f1497b98764bf47ff47598&pvs=4)|[기획/디자인](https://www.figma.com/file/RrmfjBTxuLMAYRiXrbKQSW/traveline?type=design&node-id=2%3A2&mode=design&t=AD0PpylqwYoldl8g-1)|
|📚 문서|[Wiki](https://github.com/boostcampwm2023/iOS07-traveline/wiki)|[팀 노션](https://spiky-rat-16e.notion.site/6b9791faac7e4b9d9a31d225ce8cd157?pvs=4)|[그라운드 룰](https://github.com/boostcampwm2023/iOS07-traveline/wiki/%ED%8C%80-%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%A3%B0)|[컨벤션](https://github.com/boostcampwm2023/iOS07-traveline/wiki/GitHub-%EC%BB%A8%EB%B2%A4%EC%85%98)|[회의록](https://www.notion.so/bd676cad762c4cffa7b081c65939b0c5?v=76fe42efa9f1497b98764bf47ff47598&pvs=4)|[기획/디자인](https://www.figma.com/file/RrmfjBTxuLMAYRiXrbKQSW/traveline?type=design&node-id=2%3A2&mode=design&t=AD0PpylqwYoldl8g-1)|
|:-:|:-:|:-:|:-:|:-:|:-:|:--:|
3 changes: 0 additions & 3 deletions iOS/traveline/Sources/Core/Cache/ImageCacheRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ final class ImageCacheRepositoryImpl: ImageCacheRepository {

init(fileManager: FileManager) {
self.fileManager = fileManager

// TODO: - 캐시 정책
memoryCache.countLimit = 50
}

// MARK: - Functions
Expand Down
4 changes: 2 additions & 2 deletions iOS/traveline/Sources/Core/Cache/TLImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ final class TLImageCache {
imageCacheRepository.fetch(urlString)
}

func store(_ data: Data, urlString: String) {
imageCacheRepository.store(data, cacheKey: urlString)
func store(_ data: Data, imagePath: String) {
imageCacheRepository.store(data, cacheKey: imagePath)
}

}
15 changes: 11 additions & 4 deletions iOS/traveline/Sources/Core/Extension/UIImageView+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@ import UIKit
extension UIImageView {

/// urlString으로부터 이미지를 Load 합니다.
/// imagePath를 Key로 사용합니다.
/// 캐시에 존재한다면 캐시에서 가져오고, 없으면 다운로드합니다.
/// - Parameter urlString: 이미지 URL 문자열
func setImage(from urlString: String?) {
/// - Parameters:
/// - urlString: 이미지 URL 문자열
/// - imagePath: 이미지 캐싱 키, nil 시에는 다운로드만 진행
func setImage(from urlString: String?, imagePath: String?) {
guard let urlString else { return }

if let cachedImageData = TLImageCache.shared.fetch(urlString) {
if let imagePath,
let cachedImageData = TLImageCache.shared.fetch(imagePath) {
image = UIImage(data: cachedImageData)
return
}

Task {
guard let downloadImageData = await TLImageDownloader.shared.download(key: self, urlString: urlString) else { return }
TLImageCache.shared.store(downloadImageData, urlString: urlString)
image = UIImage(data: downloadImageData)

if let imagePath {
TLImageCache.shared.store(downloadImageData, imagePath: imagePath)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct PostingResponseDTO: Decodable {
let title: String
let createdAt: String
let thumbnail: String?
let thumbnailPath: String?
let period: String
let headcount: String?
let budget: String?
Expand All @@ -28,7 +29,7 @@ struct PostingResponseDTO: Decodable {

enum CodingKeys: String, CodingKey {
case createdAt = "created_at"
case id, title, thumbnail, period, headcount, budget, location, season, vehicle, theme, withWho, writer, likeds
case id, title, thumbnail, thumbnailPath, period, headcount, budget, location, season, vehicle, theme, withWho, writer, likeds
}
}

Expand All @@ -39,9 +40,11 @@ extension PostingResponseDTO {
return .init(
id: id,
imageURL: thumbnail ?? Literal.empty,
imagePath: thumbnailPath ?? Literal.empty,
title: title,
profile: .init(
imageURL: writer.avatar ?? Literal.empty,
imagePath: writer.avatarPath ?? Literal.empty,
name: writer.name
),
like: Int(likeds) ?? 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct TimelineDetailResponseDTO: Decodable {
let day: Int
let description: String
let image: String?
let imagePath: String?
let coordX: Double?
let coordY: Double?
let date: String
Expand All @@ -38,6 +39,7 @@ extension TimelineDetailResponseDTO {
day: day,
description: description,
imageURL: image,
imagePath: imagePath,
coordX: coordX,
coordY: coordY,
date: date,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct TimelineResponseDTO: Decodable {
let title: String
let description: String
let image: String?
let imagePath: String?
let coordX: Double?
let coordY: Double?
let place: String
Expand All @@ -31,6 +32,7 @@ extension TimelineListResponseDTO {
.init(
detailId: dto.id,
thumbnailURL: dto.image,
imagePath: dto.imagePath,
title: dto.title,
place: dto.place,
content: dto.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import Foundation
struct UserResponseDTO: Codable {
let name: String
let avatar: String?
let avatarPath: String?
}

extension UserResponseDTO {
func toDomain() -> Profile {
return .init(
imageURL: avatar ?? "",
imagePath: avatarPath ?? "",
name: name
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ struct WriterDTO: Decodable {
let id: String
let name: String
let avatar: String?
let avatarPath: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class UserRepositoryMock: UserRepository {

let mockData: Profile = .init(
imageURL: "https://avatars.githubusercontent.com/u/91725382?s=400&u=29b8023a56a09685aaab53d4eb0dd556254cd902&v=4",
imagePath: "https://avatars.githubusercontent.com/u/91725382?s=400&u=29b8023a56a09685aaab53d4eb0dd556254cd902&v=4",
name: "hongki"
)
return mockData
Expand All @@ -32,6 +33,7 @@ final class UserRepositoryMock: UserRepository {

return .init(
imageURL: "https://avatars.githubusercontent.com/u/91725382?s=400&u=29b8023a56a09685aaab53d4eb0dd556254cd902&v=4",
imagePath: "https://avatars.githubusercontent.com/u/91725382?s=400&u=29b8023a56a09685aaab53d4eb0dd556254cd902&v=4",
name: "hongki"
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ final class TLInfoView: UIView {
// MARK: - Functions

func setupData(item: TravelListInfo) {
thumbnailImageView.setImage(from: item.imageURL)
thumbnailImageView.setImage(from: item.imageURL, imagePath: item.imagePath)
thumbnailImageView.isHidden = item.imageURL.isEmpty
profileImageView.setImage(from: item.profile.imageURL)
profileImageView.setImage(from: item.profile.imageURL, imagePath: item.profile.imagePath)
nameLabel.setText(to: item.profile.name)
likeCountLabel.setText(to: "\(item.like)")
titleLabel.setText(to: item.title)
Expand Down
2 changes: 2 additions & 0 deletions iOS/traveline/Sources/Domain/Model/Profile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import Foundation

struct Profile: Hashable {
let imageURL: String
let imagePath: String
let name: String

static let empty: Profile = .init(
imageURL: Literal.empty,
imagePath: Literal.empty,
name: Literal.empty
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Foundation

enum ProfileSample {
static func make() -> Profile {
.init(imageURL: "https://avatars.githubusercontent.com/u/74968390?v=4", name: "0inn")
.init(
imageURL: "https://avatars.githubusercontent.com/u/74968390?v=4",
imagePath: "https://avatars.githubusercontent.com/u/74968390?v=4",
name: "0inn"
)
}
}
21 changes: 14 additions & 7 deletions iOS/traveline/Sources/Domain/Model/Sample/TravelListSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ enum TravelListSample {
return
.init(id: "1",
imageURL: "",
imagePath: "",
title: "부산여행",
profile: Profile(imageURL: "", name: "영인"),
profile: Profile(imageURL: "", imagePath: "", name: "영인"),
like: 10,
isLiked: false,
tags: TravelListSample.makeTags()
Expand All @@ -34,48 +35,54 @@ enum TravelListSample {
return [
.init(id: "1",
imageURL: "",
imagePath: "",
title: "부산여행",
profile: Profile(imageURL: "", name: "영인"),
profile: Profile(imageURL: "", imagePath: "", name: "영인"),
like: 10,
isLiked: false,
tags: TravelListSample.makeTags()
),
.init(id: "2",
imageURL: "",
imagePath: "",
title: "속초여행",
profile: Profile(imageURL: "", name: "0inn"),
profile: Profile(imageURL: "", imagePath: "", name: "0inn"),
like: 20,
isLiked: true,
tags: TravelListSample.makeTags()
),
.init(id: "3",
imageURL: "",
imagePath: "",
title: "제주도여행",
profile: Profile(imageURL: "", name: "youngin"),
profile: Profile(imageURL: "", imagePath: "", name: "youngin"),
like: 30,
isLiked: true,
tags: TravelListSample.makeTags()
),
.init(id: "4",
imageURL: "",
imagePath: "",
title: "제주도여행",
profile: Profile(imageURL: "", name: "youngin"),
profile: Profile(imageURL: "", imagePath: "", name: "youngin"),
like: 30,
isLiked: false,
tags: TravelListSample.makeTags()
),
.init(id: "5",
imageURL: "",
imagePath: "",
title: "제주도여행",
profile: Profile(imageURL: "", name: "youngin"),
profile: Profile(imageURL: "", imagePath: "", name: "youngin"),
like: 30,
isLiked: true,
tags: TravelListSample.makeTags()
),
.init(id: "6",
imageURL: "",
imagePath: "",
title: "제주도여행",
profile: Profile(imageURL: "", name: "youngin"),
profile: Profile(imageURL: "", imagePath: "", name: "youngin"),
like: 0,
isLiked: false,
tags: TravelListSample.makeTags()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ typealias TimelineCardList = [TimelineCardInfo]
struct TimelineCardInfo: Hashable {
let detailId: String
let thumbnailURL: String?
let imagePath: String?
let title: String
let place: String
let content: String
Expand All @@ -23,6 +24,7 @@ struct TimelineCardInfo: Hashable {
static let empty: Self = .init(
detailId: Literal.empty,
thumbnailURL: Literal.empty,
imagePath: Literal.empty,
title: Literal.empty,
place: Literal.empty,
content: Literal.empty,
Expand Down
Loading

0 comments on commit 8aec978

Please sign in to comment.