feat(main-home): 추천 홈 데이터 계층을 추가한다
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -279,5 +279,6 @@ xcuserdata
|
|||||||
|
|
||||||
.kiro/
|
.kiro/
|
||||||
.junie/
|
.junie/
|
||||||
|
.omo/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
|
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
|
||||||
|
|||||||
@@ -3060,6 +3060,68 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HomeRecommendation {
|
||||||
|
static var activityLive: String {
|
||||||
|
pick(ko: "라이브", en: "Live", ja: "ライブ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var activityAudio: String {
|
||||||
|
pick(ko: "오디오", en: "Audio", ja: "オーディオ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var activityCommunity: String {
|
||||||
|
pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var liveSectionTitle: String {
|
||||||
|
pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var activeCreatorSectionTitle: String {
|
||||||
|
pick(ko: "방금 활동한 크리에이터", en: "Recently active creators", ja: "最近活動したクリエイター")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var recentDebutCreatorSectionTitle: String {
|
||||||
|
pick(ko: "최근 데뷔한 크리에이터", en: "Recently debuted creators", ja: "最近デビューしたクリエイター")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var firstAudioContentSectionTitle: String {
|
||||||
|
pick(ko: "처음 만나는 오디오", en: "First audio to discover", ja: "初めて出会うオーディオ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var aiCharacterSectionTitle: String {
|
||||||
|
pick(ko: "AI 캐릭터", en: "AI characters", ja: "AIキャラクター")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var genreCreatorSectionTitle: String {
|
||||||
|
pick(ko: "장르의 크리에이터", en: "Creators by genre", ja: "ジャンルのクリエイター")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var cheerCreatorSectionTitle: String {
|
||||||
|
pick(ko: "최근 응원이 많은 크리에이터", en: "Recently cheered creators", ja: "最近応援が多いクリエイター")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var popularCommunitySectionTitle: String {
|
||||||
|
pick(ko: "인기 커뮤니티", en: "Popular community", ja: "人気コミュニティ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var followAll: String {
|
||||||
|
pick(ko: "모두 팔로우하기", en: "Follow all", ja: "すべてフォロー")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var followAllCompleted: String {
|
||||||
|
pick(ko: "모두 팔로우 완료", en: "All followed", ja: "すべてフォロー済み")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var more: String {
|
||||||
|
pick(ko: "더보기", en: "More", ja: "もっと見る")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var collapse: String {
|
||||||
|
pick(ko: "접기", en: "Collapse", ja: "閉じる")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum Explorer {
|
enum Explorer {
|
||||||
static var channel: String {
|
static var channel: String {
|
||||||
pick(ko: "채널", en: "Channel", ja: "チャンネル")
|
pick(ko: "채널", en: "Channel", ja: "チャンネル")
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FollowRecommendedCreatorsRequest: Encodable {
|
||||||
|
let creatorIds: [Int]?
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct HomeRecommendationResponse: Decodable {
|
||||||
|
let lives: [HomeLiveItem]
|
||||||
|
let banners: [HomeBannerItem]
|
||||||
|
let recentlyActiveCreators: [HomeActiveCreatorItem]
|
||||||
|
let recentDebutCreators: [HomeCreatorItem]
|
||||||
|
let firstAudioContents: [HomeFirstAudioContentItem]
|
||||||
|
let aiCharacters: [HomeAiCharacterItem]
|
||||||
|
let genreCreators: [HomeGenreCreatorGroupItem]
|
||||||
|
let cheerCreators: [HomeCreatorItem]
|
||||||
|
let popularCommunityPosts: [HomePopularCommunityPostItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeLiveItem: Decodable, Hashable {
|
||||||
|
let liveRoomId: Int?
|
||||||
|
let roomId: Int?
|
||||||
|
let creatorId: Int?
|
||||||
|
let creatorNickname: String?
|
||||||
|
let title: String?
|
||||||
|
let profileImageUrl: String?
|
||||||
|
let thumbnailUrl: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let beginDateTime: String?
|
||||||
|
let participantCount: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeBannerItem: Decodable, Hashable {
|
||||||
|
let bannerId: Int?
|
||||||
|
let type: String?
|
||||||
|
let title: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let link: String?
|
||||||
|
let eventId: Int?
|
||||||
|
let creatorId: Int?
|
||||||
|
let seriesId: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeActiveCreatorItem: Decodable, Hashable {
|
||||||
|
let creatorId: Int?
|
||||||
|
let creatorNickname: String?
|
||||||
|
let profileImageUrl: String?
|
||||||
|
let activityType: RecommendedActivityType?
|
||||||
|
let activityAt: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeCreatorItem: Decodable, Hashable {
|
||||||
|
let creatorId: Int?
|
||||||
|
let creatorNickname: String?
|
||||||
|
let nickname: String?
|
||||||
|
let profileImageUrl: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let description: String?
|
||||||
|
let followerCount: Int?
|
||||||
|
let cheerCount: Int?
|
||||||
|
let debutDate: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeFirstAudioContentItem: Decodable, Hashable {
|
||||||
|
let contentId: Int?
|
||||||
|
let creatorId: Int?
|
||||||
|
let creatorNickname: String?
|
||||||
|
let title: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let coverImageUrl: String?
|
||||||
|
let releaseDate: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeAiCharacterItem: Decodable, Hashable {
|
||||||
|
let characterId: Int?
|
||||||
|
let name: String?
|
||||||
|
let description: String?
|
||||||
|
let profileImageUrl: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let chatCount: Int?
|
||||||
|
let originalTitle: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomeGenreCreatorGroupItem: Decodable, Hashable {
|
||||||
|
let genreId: Int?
|
||||||
|
let genreName: String?
|
||||||
|
let title: String?
|
||||||
|
let creators: [HomeCreatorItem]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomePopularCommunityPostItem: Decodable, Hashable {
|
||||||
|
let postId: Int?
|
||||||
|
let creatorId: Int?
|
||||||
|
let creatorNickname: String?
|
||||||
|
let creatorProfileImageUrl: String?
|
||||||
|
let content: String?
|
||||||
|
let imageUrl: String?
|
||||||
|
let price: Int?
|
||||||
|
let existOrdered: Bool?
|
||||||
|
let likeCount: Int?
|
||||||
|
let commentCount: Int?
|
||||||
|
let createdAt: String?
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum RecommendedActivityType: Decodable, Hashable {
|
||||||
|
case live
|
||||||
|
case audio
|
||||||
|
case community
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let code = try container.decode(String.self)
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case "LIVE", "LIVE_REPLAY":
|
||||||
|
self = .live
|
||||||
|
case "AUDIO":
|
||||||
|
self = .audio
|
||||||
|
case "COMMUNITY":
|
||||||
|
self = .community
|
||||||
|
default:
|
||||||
|
self = .unknown(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .live:
|
||||||
|
return I18n.HomeRecommendation.activityLive
|
||||||
|
case .audio:
|
||||||
|
return I18n.HomeRecommendation.activityAudio
|
||||||
|
case .community:
|
||||||
|
return I18n.HomeRecommendation.activityCommunity
|
||||||
|
case .unknown:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift
Normal file
47
SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
enum MainHomeApi {
|
||||||
|
case getRecommendations
|
||||||
|
case followRecommendedCreators(request: FollowRecommendedCreatorsRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainHomeApi: TargetType {
|
||||||
|
var baseURL: URL {
|
||||||
|
return URL(string: BASE_URL)!
|
||||||
|
}
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .getRecommendations:
|
||||||
|
return "/api/v2/home/recommendations"
|
||||||
|
|
||||||
|
case .followRecommendedCreators:
|
||||||
|
return "/api/v2/home/recommendations/creators/follow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: Moya.Method {
|
||||||
|
switch self {
|
||||||
|
case .getRecommendations:
|
||||||
|
return .get
|
||||||
|
|
||||||
|
case .followRecommendedCreators:
|
||||||
|
return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: Moya.Task {
|
||||||
|
switch self {
|
||||||
|
case .getRecommendations:
|
||||||
|
return .requestPlain
|
||||||
|
|
||||||
|
case .followRecommendedCreators(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers: [String: String]? {
|
||||||
|
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
import CombineMoya
|
||||||
|
import Combine
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
final class MainHomeRepository {
|
||||||
|
private let api = MoyaProvider<MainHomeApi>()
|
||||||
|
|
||||||
|
func getRecommendations() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getRecommendations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func followRecommendedCreators(request: FollowRecommendedCreatorsRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.followRecommendedCreators(request: request))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
### Phase 1: 구현 기준과 데이터 계층 준비
|
### Phase 1: 구현 기준과 데이터 계층 준비
|
||||||
|
|
||||||
- [ ] **Task 1.1: 구현 전 구조 확인**
|
- [x] **Task 1.1: 구현 전 구조 확인**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 확인: `SodaLive/Sources/V2/Main/MainView.swift`
|
- 확인: `SodaLive/Sources/V2/Main/MainView.swift`
|
||||||
- 확인: `SodaLive/Sources/V2/Component/SectionTitle.swift`
|
- 확인: `SodaLive/Sources/V2/Component/SectionTitle.swift`
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
- 기대 결과: `.home` placeholder 위치, 기존 `HomeApi`, `I18n` 구조를 확인할 수 있다.
|
- 기대 결과: `.home` placeholder 위치, 기존 `HomeApi`, `I18n` 구조를 확인할 수 있다.
|
||||||
- 수동 확인: 신규 추천 API를 기존 `HomeApi.swift`에 추가하지 않아야 한다.
|
- 수동 확인: 신규 추천 API를 기존 `HomeApi.swift`에 추가하지 않아야 한다.
|
||||||
|
|
||||||
- [ ] **Task 1.2: 추천 API 모델 작성**
|
- [x] **Task 1.2: 추천 API 모델 작성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift`
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Models/FollowRecommendedCreatorsRequest.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Models/FollowRecommendedCreatorsRequest.swift`
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
- 기대 결과: 세 모델/enum 정의가 검색된다.
|
- 기대 결과: 세 모델/enum 정의가 검색된다.
|
||||||
- 수동 확인: PRD의 응답 필드명이 Swift 모델에 누락 없이 반영되어야 한다.
|
- 수동 확인: PRD의 응답 필드명이 Swift 모델에 누락 없이 반영되어야 한다.
|
||||||
|
|
||||||
- [ ] **Task 1.3: I18n 문구 추가**
|
- [x] **Task 1.3: I18n 문구 추가**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
- 기대 결과: 홈 추천 I18n enum과 핵심 문구가 검색된다.
|
- 기대 결과: 홈 추천 I18n enum과 핵심 문구가 검색된다.
|
||||||
- 수동 확인: 신규 사용자 노출 문자열이 하드코딩 View 텍스트로 흩어지지 않아야 한다.
|
- 수동 확인: 신규 사용자 노출 문자열이 하드코딩 View 텍스트로 흩어지지 않아야 한다.
|
||||||
|
|
||||||
- [ ] **Task 1.4: 신규 API와 Repository 작성**
|
- [x] **Task 1.4: 신규 API와 Repository 작성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift`
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift`
|
||||||
@@ -389,3 +389,26 @@
|
|||||||
- Swift 코드 구현
|
- Swift 코드 구현
|
||||||
- Xcode 프로젝트 포함 여부 확인
|
- Xcode 프로젝트 포함 여부 확인
|
||||||
- 빌드/기능 검증
|
- 빌드/기능 검증
|
||||||
|
|
||||||
|
### 2026-06-02 Phase 1 구현 완료
|
||||||
|
|
||||||
|
- 목적: Phase 1 범위인 구현 전 구조 확인, 추천 API 모델, I18n 문구, 신규 API/Repository 작성
|
||||||
|
- 수행 내용:
|
||||||
|
- `SodaLive/Sources/V2/Main/MainView.swift`의 `.home` placeholder 유지 상태와 기존 `HomeApi`, `HomeTabRepository`, `I18n` 구조 확인
|
||||||
|
- `SodaLive/Sources/V2/Main/Home/Models`에 `HomeRecommendationResponse`, `FollowRecommendedCreatorsRequest`, `RecommendedActivityType` 추가
|
||||||
|
- `SodaLive/Sources/I18n/I18n.swift`에 `I18n.HomeRecommendation` 문구 추가
|
||||||
|
- `SodaLive/Sources/V2/Main/Home/Repository`에 `MainHomeApi`, `MainHomeRepository` 추가
|
||||||
|
- 신규 Swift 파일 5개를 `SodaLive.xcodeproj/project.pbxproj`의 `SodaLive`, `SodaLive-dev` Sources에 포함
|
||||||
|
- 검증:
|
||||||
|
- `rg "case \.home|MainPlaceholderTabView|enum HomeApi|enum I18n" SodaLive/Sources/V2/Main SodaLive/Sources/Home SodaLive/Sources/I18n/I18n.swift` 실행, `.home` placeholder와 기존 `HomeApi`, `I18n` 구조 확인
|
||||||
|
- `rg "struct HomeRecommendationResponse|struct FollowRecommendedCreatorsRequest|enum RecommendedActivityType" SodaLive/Sources/V2/Main/Home/Models` 실행, 모델 정의 검색 확인
|
||||||
|
- `rg "enum HomeRecommendation|activityLive|followAllCompleted|collapse" SodaLive/Sources/I18n/I18n.swift` 실행, 신규 I18n 문구 검색 확인
|
||||||
|
- `rg "/api/v2/home/recommendations|followRecommendedCreators|getRecommendations" SodaLive/Sources/V2/Main/Home/Repository` 실행, 신규 API 경로와 repository 메서드 검색 확인
|
||||||
|
- `rg "/api/v2/home/recommendations" SodaLive/Sources/Home` 실행, 검색 결과 없음 확인
|
||||||
|
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` 확인
|
||||||
|
- `git diff --check` 실행, 출력 없이 성공 확인
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -list` 실행, 별도 테스트 타깃 없음 확인
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED` 확인
|
||||||
|
- 아직 수행하지 않은 작업:
|
||||||
|
- Phase 2 이후 ViewModel, UI 컴포넌트, 홈 탭 연결
|
||||||
|
- 테스트 타깃이 없어 Phase 1 전용 RED/GREEN 단위 테스트는 추가하지 않음
|
||||||
|
|||||||
Reference in New Issue
Block a user