Seta는 손쉽게 세트리스트를 찾아 볼 수 있고, 유저가 사용하는 음악플랫폼으로 세트리스트를 플레이리스트로 변환시켜 손쉽게 예습을 할 수 있게 하는 앱 입니다.
- iOS 5명, 디자인 2명, PM 1명
- iOS 17.0+
- 가수와 세트리스트 검색 기능
- 세트리스트를 Apple Music으로 옮기는 기능
- OCR기능을 지원하는 음악앱을 위한 OCR용 이미지 추출 기능
- SNS에 공유하기 위한 이미지 추출, 공유 기능
- 세트리스트 북마크, 아티스트 찜하기 등의 저장 기능
- SwiftUI
- Tuist
- MVVM
- Combine
- URLSession
- MusicKit
- Node.js
- SwiftData의 SwiftDataManager를 만들어 코드의 재사용성을 높임.
public final class SwiftDataManager: ObservableObject {
public var modelContext: ModelContext?
public init(modelContext: ModelContext? = nil) { self.modelContext = modelContext }
// MARK: - Save SwiftData func
public func save() {
do {
try modelContext?.save()
} catch {
print(error.localizedDescription)
}
// ....
}
1. List의 onMove로 구현된 순서 이동이 저장이 안되는 이슈, 순서를 저장하는 ordered Property를 생성, 순서를 변경할때 ordered를 서로 변경하여 순서 저장
public func addLikeArtist(name: String,
country: String,
alias: String,
mbid: String,
gid: Int,
imageUrl: String?,
songList: [Titles]) {
let descriptor = FetchDescriptor<LikeArtist>()
let likeArtist = try? modelContext?.fetch(descriptor)
var max = 0
guard let likeArtist = likeArtist else { return }
for i in 0..<likeArtist.count {
if max < likeArtist[i].orderIndex {
max = likeArtist[i].orderIndex
}
}
let newLikeArtist = LikeArtist(artistInfo: SaveArtistInfo(name: name,
country: country,
alias: alias,
mbid: mbid,
gid: gid,
imageUrl: imageUrl ?? "",
songList: songList),
orderIndex: max+1)
modelContext?.insert(newLikeArtist)
self.save()
}
2. ObservableObject의 인스턴스를 명시적으로 생성하면, 상위View가 다시 그려질때마다 다시 ObservableObject를 재생성 하므로, 성능문제가 발생할 수 있음. 따라서 StateObject로 변경
@ObservedObject var onboardingViewModel = OnboardingViewModel()
@StateObject var onboardingViewModel = OnboardingViewModel()
var parsing_artists = new Set();
async function crawlingArtist(database) {
const korea_url = "https://kworb.net/spotify/country/kr_daily_totals.html";
await request(korea_url, function (error, response, html) {
if (!error) {
var $ = cheerio.load(html);
$("tbody tr").each(function (index, element) {
var artistElement = $(element).find("td:eq(0) a");
var artistName = artistElement.contents().first().text().trim();
parsing_artists.add(artistName);
});
}
fetchdata(database);
});
}
}
async function fetchTags(value) {
const kpopGenres = [
"k-pop",
"Kpop",
"Korean",
"Korean Pop",
"Kpop Star",
"korean indie",
"korean rock",
];
// ....
const jpopJrockGenres = [
"j-pop",
"japanese",
"JPop",
"J-rock",
"J-urban",
"J-Punk",
];
try {
const response = await axios.get(
`https://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist=${value}&api_key=0f54e196c4a83ed95d87c8ee18c3fdcd&format=json`
);
const data = response.data.artist.tags.tag;
const tagNames = data.map((tag) => tag.name);
var returnTags = new Set();
for (const tag of tagNames) {
if (kpopGenres.includes(tag)) {
returnTags.add("K-Pop");
} else if (popGenres.includes(tag)) {
returnTags.add("Pop");
} else if (hipHopRapGenres.includes(tag)) {
returnTags.add("Hip-Hop");
} else if (rnbSoulGenres.includes(tag)) {
returnTags.add("R&B");
} else if (rockAlternativeGenres.includes(tag)) {
returnTags.add("Rock");
} else if (metalGenres.includes(tag)) {
returnTags.add("Metal");
} else if (electronicGenres.includes(tag)) {
returnTags.add("Electronic");
} else if (countryFolkGenres.includes(tag)) {
returnTags.add("Country/Folk");
} else if (jpopJrockGenres.includes(tag)) {
returnTags.add("J-Pop");
} else {
continue;
}
}
if (returnTags.has("K-Pop") && returnTags.has("Pop")) {
returnTags.delete("Pop");
}
return Array.from(new Set(returnTags));
} catch (error) {
console.error(`Error fetching data for ${value}:`, error.message);
}
}
module.exports = fetchTags;
📦Projects
┣ 📂App
┃ ┣ 📂Resources
┃ ┣ 📂Sources
┃ ┣ 📂Support
┣ 📂Core
┃ ┣ 📂Sources
┃ ┃ ┣ 📂Model
┃ ┃ ┃ ┣ 📂ArchivedConcertInfo
┃ ┃ ┃ ┣ 📂ArtistInfoModel
┃ ┃ ┃ ┣ 📂LikeArtist
┃ ┃ ┃ ┣ 📂SearchHistory
┃ ┃ ┗ 📂Service
┃ ┃ ┃ ┣ 📂SearchHistoryDataManager
┃ ┃ ┃ ┣ 📂SwiftDataManager
┣ 📂Feature
┃ ┣ 📂Scenes
┃ ┃ ┣ 📂ArchiveScene
┃ ┃ ┃ ┣ 📂Component
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂ArtistScene
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂MainScene
┃ ┃ ┃ ┣ 📂Component
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂OnboardingScene
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂SearchScene
┃ ┃ ┃ ┣ 📂Component
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂SetlistScene
┃ ┃ ┃ ┣ 📂Component
┃ ┃ ┃ ┃ ┣ 📂CaptureSetlist
┃ ┃ ┃ ┣ 📂View
┃ ┃ ┃ ┗ 📂ViewModel
┃ ┃ ┣ 📂SettingScene
┃ ┃ ┃ ┗ 📂View
┣ 📂UI
┃ ┣ 📂Resources
┃ ┃ ┣ 📂Colors.xcassets
┃ ┣ 📂Sources
┗ ┗ ┗ 📂Extensions
뷰 | 이미지 |
---|---|
온보딩 뷰 | |
메인 뷰 | |
서치 뷰 | |
보관함 뷰 | |
아티스트 뷰 | |
세트리스트 뷰 |