Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Projects/App/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{
"sourceLanguage" : "en",
"strings" : {
"%@" : {

},
"%@명 참여" : {

},
"w." : {

},
"Youtube Music" : {

},
"개인정보처리방침" : {

},
Expand All @@ -12,15 +24,30 @@
},
"다양한 사람들과 음악으로 연결되는\n특별한 순간을 경험해보세요" : {

},
"릴레이리스트가 완성되었어요 🎉" : {

},
"서비스 조건" : {

},
"에 동의하게 됩니다." : {

},
"위플리 TOP 100" : {

},
"위플리 인기 랭킹" : {

},
"위플리 차트에서 인기가 많은 가수들을 모아봤어요" : {

},
"최초 로그인은 계정을 생성하며," : {

},
"플리 완성까지" : {

},
"함께 만드는\n플레이리스트" : {

Expand Down
6 changes: 3 additions & 3 deletions Projects/App/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import SwiftUI

struct ContentView: View {
var body: some View {
LoginView(
store: Store(initialState: LoginFeature.State()) {
LoginFeature()
AppView(
store: Store(initialState: AppFeature.State()) {
AppFeature()
}
)
}
Expand Down
27 changes: 27 additions & 0 deletions Projects/App/Sources/Extensions/Color+Hex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI

extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64

Check failure on line 8 in Projects/App/Sources/Extensions/Color+Hex.swift

View workflow job for this annotation

GitHub Actions / Lint (SwiftFormat & SwiftLint)

Variable name 'b' should be between 2 and 50 characters long (identifier_name)

Check failure on line 8 in Projects/App/Sources/Extensions/Color+Hex.swift

View workflow job for this annotation

GitHub Actions / Lint (SwiftFormat & SwiftLint)

Variable name 'g' should be between 2 and 50 characters long (identifier_name)

Check failure on line 8 in Projects/App/Sources/Extensions/Color+Hex.swift

View workflow job for this annotation

GitHub Actions / Lint (SwiftFormat & SwiftLint)

Variable name 'r' should be between 2 and 50 characters long (identifier_name)

Check failure on line 8 in Projects/App/Sources/Extensions/Color+Hex.swift

View workflow job for this annotation

GitHub Actions / Lint (SwiftFormat & SwiftLint)

Variable name 'a' should be between 2 and 50 characters long (identifier_name)
switch hex.count {
case 3:
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6:
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8:
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
42 changes: 42 additions & 0 deletions Projects/App/Sources/Features/App/AppFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ComposableArchitecture
import Foundation

@Reducer
struct AppFeature {
@ObservableState
struct State: Equatable {
var isLoggedIn: Bool = false
var login: LoginFeature.State = .init()
var main: MainFeature.State = .init()
}

enum Action {
case login(LoginFeature.Action)
case main(MainFeature.Action)
}

var body: some ReducerOf<Self> {
Scope(state: \.login, action: \.login) {
LoginFeature()
}

Scope(state: \.main, action: \.main) {
MainFeature()
}

Reduce { state, action in
switch action {
// 로그인 성공 시 메인 화면으로 전환
case .login(.loginResponse(.success)):
state.isLoggedIn = true
return .none

case .login:
return .none

case .main:
return .none
}
}
}
}
36 changes: 36 additions & 0 deletions Projects/App/Sources/Features/App/AppView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ComposableArchitecture
import SwiftUI

struct AppView: View {
let store: StoreOf<AppFeature>

var body: some View {
WithPerceptionTracking {
if store.isLoggedIn {
MainView(
store: store.scope(state: \.main, action: \.main)
)
} else {
LoginView(
store: store.scope(state: \.login, action: \.login)
)
}
}
}
}

#Preview("Logged Out") {
AppView(
store: Store(initialState: AppFeature.State()) {
AppFeature()
}
)
}

#Preview("Logged In") {
AppView(
store: Store(initialState: AppFeature.State(isLoggedIn: true)) {
AppFeature()
}
)
}
37 changes: 37 additions & 0 deletions Projects/App/Sources/Features/Main/Client/MainClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ComposableArchitecture
import Foundation

// MARK: - MainClient

struct MainClient {
var fetchMainData: @Sendable () async -> MainData
}

// MARK: - DependencyKey

extension MainClient: DependencyKey {
static let liveValue = MainClient(
fetchMainData: {
// TODO: Implement actual API call
try? await Task.sleep(nanoseconds: 500000000)
return .mock
}
)

static let testValue = MainClient(
fetchMainData: { .mock }
)

static let previewValue = MainClient(
fetchMainData: { .mock }
)
}

// MARK: - DependencyValues

extension DependencyValues {
var mainClient: MainClient {
get { self[MainClient.self] }
set { self[MainClient.self] = newValue }
}
}
180 changes: 180 additions & 0 deletions Projects/App/Sources/Features/Main/Client/MainMockData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Foundation

// MARK: - MainData Mock

extension MainData {
static let mock = MainData(
relayLists: RelayList.mockList,
chartSongs: ChartSong.mockList,
popularArtists: Artist.mockList,
recommendedPlaylists: Playlist.recommendedMockList,
themedPlaylists: Playlist.themedMockList,
youtubeVideos: YouTubeVideo.mockList,
banners: Banner.mockList,
chartUpdateTime: "6월 23일 오전 7시 업데이트"
)
}

// MARK: - RelayList Mock

extension RelayList {
static let mockList: [RelayList] = [
RelayList(
id: "1",
title: "손님들이 물어보는\n카페 BGM",
participantCount: 3012,
firstSong: Song(
id: "1",
title: "Radical Optimism",
artist: "Dua Lipa",
albumName: "Album",
releaseYear: "2024",
albumArtURL: nil
),
backgroundImageURL: nil,
remainingSeconds: 130215
),
RelayList(
id: "2",
title: "비 오는 날 듣기 좋은\n감성 플레이리스트",
participantCount: 2456,
firstSong: nil,
backgroundImageURL: nil,
remainingSeconds: 0
),
]
}

// MARK: - ChartSong Mock

extension ChartSong {
static let mockList: [ChartSong] = [
ChartSong(
id: "1",
rank: 1,
song: Song(
id: "s1",
title: "Small girl (feat. 도경수 (D.O)",
artist: "이영지",
albumName: nil,
releaseYear: nil,
albumArtURL: nil
)
),
ChartSong(
id: "2",
rank: 2,
song: Song(
id: "s2",
title: "Supernova",
artist: "aespa",
albumName: nil,
releaseYear: nil,
albumArtURL: nil
)
),
ChartSong(
id: "3",
rank: 3,
song: Song(
id: "s3",
title: "How Sweet",
artist: "NewJeans",
albumName: nil,
releaseYear: nil,
albumArtURL: nil
)
),
ChartSong(
id: "4",
rank: 4,
song: Song(
id: "s4",
title: "해야 (HEYA)",
artist: "IVE (아이브)",
albumName: nil,
releaseYear: nil,
albumArtURL: nil
)
),
ChartSong(
id: "5",
rank: 5,
song: Song(
id: "s5",
title: "소나기",
artist: "이클립스 (ECLIPSE)",
albumName: nil,
releaseYear: nil,
albumArtURL: nil
)
),
]
}

// MARK: - Artist Mock

extension Artist {
static let mockList: [Artist] = [
Artist(id: "a1", name: "BIGBANG (빅뱅)", profileImageURL: nil),
Artist(id: "a2", name: "비투비", profileImageURL: nil),
Artist(id: "a3", name: "윤하(Younha/ユンナ)", profileImageURL: nil),
Artist(id: "a4", name: "엔플라잉(N.Flying)", profileImageURL: nil),
]
}

// MARK: - Playlist Mock

extension Playlist {
static let recommendedMockList: [Playlist] = [
Playlist(id: "p1", title: "끈적달달한 체리위스키를 머금은 힙합 R&B", coverImageURL: nil),
Playlist(id: "p2", title: "노을처럼 번지는 아날로그 무드", coverImageURL: nil),
Playlist(id: "p3", title: "추위를 녹이는\n음색의 보이스", coverImageURL: nil),
]

static let themedMockList: [Playlist] = [
Playlist(id: "t1", title: "초여름 청량한 케이팝 댄스", coverImageURL: nil),
Playlist(id: "t2", title: "청량함 가득 여름 국힙", coverImageURL: nil),
Playlist(id: "t3", title: "뼛속까지 청량해지는 K-pop", coverImageURL: nil),
]
}

// MARK: - YouTubeVideo Mock

extension YouTubeVideo {
static let mockList: [YouTubeVideo] = [
YouTubeVideo(
id: "y1",
title: "Falling - 로이킴 [더 시즌즈-최정훈의 밤의공원]",
channelName: "KBS Kpop",
thumbnailURL: nil
),
YouTubeVideo(
id: "y2",
title: "ZICO (지코) 'SPOT! (feat. JENNIE)' Official MV",
channelName: "HYBE LABELS",
thumbnailURL: nil
),
]
}

// MARK: - Banner Mock

extension Banner {
static let mockList: [Banner] = [
Banner(
id: "b1",
title: "실시간 통합 순위를 한 눈에!",
subtitle: "오직 위플리에서만",
imageURL: nil,
actionURL: nil
),
Banner(
id: "b2",
title: "감성 가득한 일상 보러가기",
subtitle: "위플리 인스타그램 OPEN!",
imageURL: nil,
actionURL: nil
),
]
}
Loading
Loading