곡 리스트 한 가지뿐이던 전체 목록
Utakoto 곡 탭 아래쪽의 "전체 목록"은 지금까지 곡 리스트 한 모양뿐이었습니다. 좋아하는 아티스트 곡만 보고 싶으면 "필터" 시트를 열어 아티스트를 골라야 했고, 썸네일을 크게 보며 훑는 방법은 없었습니다.
행 하나하나도 빽빽했습니다. 행마다 담기/열기 버튼과 칩 네 종류가 붙어 제목이 들어갈 자리가 좁았습니다. 그런데 그 버튼은 행을 누르는 것과 같은 일을 했습니다. 둘 다 미리보기 시트를 열거나 곡 화면으로 갑니다. 곡을 눌렀을 때 뜨는 미리보기 시트에도 손볼 데가 모여 있었습니다. 제목과 아티스트가 한 줄이라 아티스트 이름이 잘렸고, 가사 글자가 24px 라 긴 줄은 넘어갔고, "전체 가사이 열려요"라는 조사 오류도 있었습니다.
오늘 intent 를 쓰고, 목업을 놓고 PO 승인을 받고, 같은 날 OTA 까지 냈습니다. 바뀐 것은 이렇습니다.
- 전체 목록 제목 아래에
곡 | 아티스트,리스트 | 2열두 전환이 있고, 이 머리는 스크롤해도 검색창 아래에 붙어 있습니다. - 곡 리스트 행에서 버튼을 뺐습니다. 칩은 JLPT, 아는 단어 비율, 노래방,
담음만 남겼습니다. - 아티스트 뷰는 팔로우한 아티스트를 먼저, 나머지는 곡 많은 순으로 보여 줍니다. 한 팀을 누르면 같은 자리에서 그 팀 곡만 보입니다.
- 리스트/2열 선택은 앱을 다시 열어도 기억합니다.
고정 머리와 사진이 없을 때
머리를 고정하는 데는 FlatList 의 stickyHeaderIndices 를 썼습니다. 목록 데이터의 첫 행을 머리(bar)로 두고, 그 행을 붙입니다. ListHeaderComponent(위쪽의 인기 곡·추천 섹션들)가 0번 칸을 차지하기 때문에 붙일 번호는 1입니다. 이렇게 하면 머리 행이 늘 데이터에 있으니 목록이 비었을 때 쓰던 ListEmptyComponent 가 더는 불리지 않습니다. 그래서 불러오는 중·실패·0건을 state 라는 행 하나로 만들어 머리 밑에 넣었습니다.
머리가 붙어 있는 동안 곡↔아티스트를 바꾸면 새 목록의 중간부터 보이게 됩니다. 이때는 머리 자리까지 스크롤을 되감도록 했습니다.
아티스트 사진은 공식 유튜브 채널의 프로필 이미지를 서버가 받아 둡니다. intent 의 기록으로는 운영 DB 102팀 중 88팀을 채웠고, 채널이 레이블 채널이나 팬 영상뿐인 14팀은 비어 있습니다. 그리고 앱은 이 필드를 주지 않는 예전 서버에서도 그려져야 합니다. 그래서 그림은 세 단계로 물러나게 했습니다.
export function ArtistAvatar({ artist, size }: { /* … */ }) {
const sources = [artist.profileImageUrl, artist.thumbnails?.[0]].filter((u): u is string => !!u);
const [failed, setFailed] = useState(0);
const uri = sources[failed];
// …
if (!uri) {
return (
<View style={box} className="items-center justify-center" accessibilityElementsHidden importantForAccessibility="no">
<Text className="font-jp-bold text-callout text-text-mid" style={{ includeFontPadding: false, textAlignVertical: 'center' }}>
{artist.name.trim().slice(0, 1)}
</Text>
</View>
);
}
return <Image key={uri} source={{ uri }} style={box} onError={() => setFailed((n) => n + 1)} /* … */ />;
}
공식 사진을 못 불러오면 failed 가 하나 올라가 최신 곡 썸네일로 넘어가고, 그것도 없으면 이름 첫 글자를 그립니다. 2열 카드는 그 아티스트의 최신 곡 썸네일 네 장을 모아 그리고, 썸네일이 하나도 없을 때만 사진 한 장을 씁니다.
미리보기 시트는 제목과 아티스트를 두 줄로 나눴고, 아티스트 이름을 누르면 시트가 닫히고 그 아티스트 곡 목록으로 갑니다. 가사 미리보기는 줄마다 한글 발음, 원문, 번역이 붙는 묶음인데, 원문 글자를 text-title3 역할로 줄였고 원곡으로 듣기 는 담기 버튼과 같은 크기로 쌓지 않고 가사 묶음 머리의 작은 버튼으로 옮겼습니다.
Utakoto 곡 화면(스토어 스크린샷)입니다. 미리보기 시트의 가사 묶음이 이 화면과 같은 발음·원문·뜻 구성을 세 줄만 보여 줍니다.
시트 위쪽 테두리도 지웠습니다. 위쪽에만 borderTopWidth 를 주면 선이 둥근 모서리를 따라가지 못해 모서리가 각져 보였습니다. 같은 모양이던 곡 요청 시트와 원곡 듣기 패널은 다음 커밋(21f7099)에서 같이 고쳤습니다.
- style={{ backgroundColor: colors.surface, borderTopWidth: 1, borderColor: colors.borderStrong }}
+ // 위쪽에만 테두리를 주면 선이 둥근 모서리를 따라가지 못해 각져 보인다 — 막(scrim) 위의 면으로만 구분한다.
+ style={{ backgroundColor: colors.surface }}
버튼을 빼도 되는 이유, 넣지 않은 것
담기 버튼을 빼면서 걸린 것은 담기 전에는 미리보기 시트를 거친다는 기존 규칙이었습니다. 행·카드를 누르면 안 담은 곡은 시트가 열리고 담은 곡은 곡 화면으로 가는 기존 동작(tapCatalog)을 그대로 쓰니, 버튼이 없어도 규칙은 지켜집니다. 버튼이 하던 일이 행 탭과 같았다는 것이 뺀 이유의 전부입니다.
넣지 않기로 한 것도 있습니다. 처음에는 아티스트 행에 JLPT 범위를 붙이려 했지만, 곡마다 단어 분포를 다 읽어야 해서 비용이 크다고 보고 6곡 같은 곡 수만 남겼습니다. 리스트/2열은 기억하지만 곡/아티스트 선택은 기억하지 않고 늘 곡으로 시작합니다. 이 차이의 이유는 spec 에 따로 적혀 있지 않습니다.
같은 출시 묶음에 가사·번역 제보 보상도 들어갔습니다. 오류 신고는 전부터 있었지만 신고해도 돌아오는 것이 결과 알림뿐이라, 틀린 가사를 가장 먼저 보는 학습자가 알려 줄 이유가 약했습니다. PO 결정은 확인된 제보 1건에 30P, 하루 상한 150P, 게스트도 받기, 같은 줄을 여러 명이 제보하면 고쳐지기 전에 제보한 사람 모두 각자 한 번씩입니다.
기록이 가장 분명하게 못 박아 둔 것은 지급 시점입니다. 접수만으로는 주지 않고, 검수에서 맞다고 확인돼 고침으로 닫힐 때만 줍니다. 아무 신고나 넣어 포인트를 모으는 것을 막기 위해서이고, 지급은 신고 id 를 기준으로 한 건에 한 번만 일어나게 했습니다. 금액 계산도 앱이 아니라 서버가 합니다. 앱은 서버가 준 금액으로 문구만 그립니다.
/**
* "확인되면" 으로 약속한다 — 접수하자마자 주는 것처럼 읽히면 안 된다. 서버가 금액을 안 주면(구 서버) null.
*/
export function reportRewardCopy(reward: number | null | undefined): string | null {
return typeof reward === 'number' && reward > 0 ? `제보가 확인돼 고쳐지면 ${reward}P를 드려요` : null;
}
서버가 reportReward 를 주지 않으면 신고 시트와 미리보기 시트 어디에도 이 줄이 나오지 않습니다.
확인한 것과 남은 것
catalog-browse-views 의 plan 에 적힌 검증은 이렇습니다. npx tsc --noEmit 통과, 바꾼 파일 eslint 통과, npx jest 749개 통과. iOS 시뮬레이터에서 로컬 백엔드로 고정 머리, 곡 리스트/2열, 아티스트 리스트/2열, 아티스트 곡 목록과 팔로우 막대, 미리보기 시트를 다크·라이트, 큰 글자 설정에서 봤다고 적혀 있습니다. 같은 plan 에 손으로 누르는 동작(전환·뒤로·시트의 아티스트 이름·원곡 듣기 펼침)과 Android 는 확인하지 못했다고 적혀 있습니다.
제보 보상 쪽 plan 은 서버의 vitest 와 모바일의 tsc·eslint·jest 에 체크가 돼 있고, "실기기에서 신고 → 어드민에서 고침 → 포인트 이력에 제보 보상 +30P" 항목은 아직 비어 있습니다.
발행 기록(1a4323a)에 따르면 백엔드를 먼저 배포하고, main 의 21f7099 로 OTA 6108218c(runtime 1.11.0, iOS·Android)를 냈습니다.
판정은 출시 14일 뒤에 합니다. 곡 탭을 본 사용자 중 둘째 곡을 담는 비율과 미리보기 대비 담기 전환이 떨어지지 않았는지, 그리고 새로 넣은 catalog_browse_changed·catalog_artist_opened 로 아티스트 뷰와 2열을 실제로 얼마나 쓰는지 봅니다. 제보 보상은 30일 뒤 주간 신고 수와 그중 '고침' 비율을 보고, 보상 때문에 틀린 신고가 늘어 검수가 무거워지지 않았는지 같이 봅니다.
Utakoto 는 J-POP·애니송 가사로 일본어를 배우는 앱입니다. 가사마다 후리가나·한국어 발음·뜻이 붙고, 노래 속 단어가 JLPT 단어장으로 모여 복습할 수 있습니다. iOS·Android 에서 무료입니다.