드디어 프로젝트 1차 배포를 완료하였다. 물론 앱 심사를 진행 중이지만 Thread Sanitizer으로부터 아무런 경고도 받지 못했고, 실기기에서도 테스트하여 MVP기능에는 큰 문제가 없다고 판단하여 앱을 빌드하고 심사를 요청했다. 그리고 심사 승인까지 하루정도 걸린 것 같다.
링크는 아래와 같다 😌
앱 기능
1. 백준 아이디 등록 (Solved.ac에 등록이 되어야 함)
아래와 같이 백준 아이디를 입력하게 되면, 따로 비밀번호 입력없이 아이디의 정보를 조회할 수 있다. 약간 OP.GG 사이트와 비슷하다고 보면 된다.
2. 백준 프로필 확인
아래와 같이 아이디를 입력하게 되면 백준 프로필을 확인할 수 있다. 아래는 내가 푼 상위 50문제, 내가 푼 문제 난이도 분포를 그래프 형태로 볼 수 있다.
3. 각 항목을 클릭하게 되면 상세 정보도 확인 가능
각 항목들을 클릭하게 된다면 그 항목이 어떠한 것을 설명하는지, 상세 정보도 확인이 가능하다.
4. 위젯 기능
위젯을 등록해 앱을 들어가지 않고도 프로필을 쉽게 확인이 가능하다. (추후 위젯에 새로고침 버튼 넣을 예정)
5. 기기 대응
아이패드에서도 큰 문제없이 볼 수 있다. 하지만 아이패드의 UI는 좀 더 디테일하게 업데이트할 예정이다.
파일 구조
파일구조는 아래와 같다. 크게는 Solver앱, SolverWidget앱이 있다. Solver앱에서는 크게 Presentation, Domain, Data로 나누었다. persistence의 경우 SwiftData를 활용해 유저의 정보를 저장하고 Store에서만 사용하여 Entity로 바꿔주기 때문에 별개의 레이어로 두었다.
데이터 흐름
Data흐름의 경우 최대한 단방향으로 이루어지게 하였다. 데이터 흐름의 경우 Solved.ac에 저장되어있는 데이터를 API를 사용하여 호출하여 DTO로 바꾸어 준다. 바꾸어준 DTO를 입맛에 맞게 Entity로 변경해 주었다. 그리고 이렇게 변환된 Entity를 앱에 표시한다.
SwiftData의 활용
어떠한 것을 앱에 저장하면 좋을까 생각했는데, 결국 디테일한 데이터는 최신정보가 아니면 의미 없다고 생각했다. 결국 기본적인 정보와 유저의 프로필 사진, 배지 사진처럼 중복해서 불러온다면 계속 url을 통해 이미지 데이터를 요청해야 하는데, 똑같은 url주소라면 이미 가지고 있던 데이터를 사용하고 이미지 데이터 요청을 생략할 수 있게 SwiftData를 구성했다.
또한 SwiftData를 활용하여 Widget에 쉽게 실시간 SwiftData에 저장된 데이터를 표시할 수 있게 하였다. toDomain을 통해 Entity로 변경가능하게 만들었는데, Convertible 프로토콜을 따로 만들어 명확하게 어떠한 기능을 하는지도 명시해주면 좋을 것 같다는 생각을 했다.
@Model
final class User {
@Attribute(.unique) var id: String
var badgeId: String?
var profileImageUrl: String?
var solvedCount: Int
var tier: Int
var rating: Int
var userClass: Int
var classDecoration: String
var maxStreak: Int
var rank: Int
@Relationship(deleteRule: .cascade, inverse: \Profile.user) var profile: Profile?
@Relationship(deleteRule: .cascade, inverse: \Badge.user) var badge: Badge?
var totalUserCount: Int?
...
}
@Model
final class Profile {
var user: User?
var imageUrl: String
var image: Data?
...
func toDomain() -> ProfileEntity {
ProfileEntity(
imageUrl: imageUrl,
image: image
)
}
}
@Model
final class Badge {
var user: User?
...
var imageUrl: String
var image: Data?
...
}
Entity
Entity의 경우 최대한 어떠한 프로토콜, 연관성이 없게 최대한 담백하게 struct로만 구현하였다.
import Foundation
struct UserEntity {
var id: String
var badgeId: String?
var profileImageUrl: String?
var solvedCount: Int
var tier: Int
var rating: Int
var userClass: Int
var classDecoration: String
var maxStreak: Int
var rank: Int
}
DTO
DTO의 경우 아래의 toDomain 함수를 통해 Entity로 변경이 가능하게 했다.
protocol DTO {
associatedtype T
func toDomain() -> T
}
예를 들면 아래와 같이 Decodable, DTO 프로토콜을 준수하여 API를 사용하여 데이터를 쉽게 가져올 수 있게 했다.
import Foundation
struct ProblemDTO: Decodable, DTO {
var level: Int
var total: Int
var solved: Int
var partial: Int
var tried: Int
func toDomain() -> ProblemEntity {
ProblemEntity(
level: level,
total: total,
solved: solved,
tried: tried
)
}
}
앱 구조
Clean Architecture을 최대한 반영하여 만들었다.
추후 수정사항
Repository부분에서 이상함을 느꼈다. 최대한 추상화시켜서 Check, Fetch의 기능을 하는 Repository를 구현했는데, UseCase에서 직접적으로 각 Repository를 사용하기 때문에 아래의 의존성 규칙에 어긋난다고 할 수 있다... 이럴 때 해결방법이 Domain 레이어에 Repository 프로토콜을 두어 결합을 느슨하게 만든다고 한다.
'→ Solver' 카테고리의 다른 글
[Project-Solver] 프로젝트 2차 배포 (v1.2.0) (0) | 2024.07.08 |
---|---|
[Project-Solver] Data race 오류 기록 (0) | 2024.07.07 |
[Project-Solver] SwiftData 문제 기록 (0) | 2024.04.25 |
[Project-Solver] 앱 기본 설계 (2) | 2024.04.22 |
[Project-Solver] 아키텍처 설계 (0) | 2024.04.21 |