Compare commits

..

156 Commits

Author SHA1 Message Date
d13769861d .idea/deploymentTargetSelector.xml를 gitignore에 추가 2026-01-30 14:35:09 +09:00
5d15f74575 versionCode 218, versionName "1.48.0" 2026-01-29 02:05:30 +09:00
5f445872c8 linesdk R8 오류를 해결하기 위해 프로가드 내용 추가 2026-01-29 01:58:42 +09:00
d44853055a x 로그인 버튼 제거, 언어에 관계없이 Line 로그인 버튼 보이도록 수정 2026-01-28 23:19:20 +09:00
4ddee2b1c1 LINE 로그인 연동 추가
LINE 로그인 요청에 id token과 nonce를 전달함
2026-01-28 17:52:02 +09:00
6031638260 로그인 화면에 일본어 SNS 아이콘 표시 2026-01-28 13:44:39 +09:00
39f647d05a 디버그 용 빌드 앱 이름 적용 2026-01-28 11:55:02 +09:00
a1b464e864 versionCode 216, versionName 1.47.2 2026-01-26 11:38:35 +09:00
d657e8827a 영문 폰트(Inter), 일본어 폰트(Pretendard-jp) 추가 2026-01-22 23:02:58 +09:00
6cb89ef09f 폰트 이름 변경
pretendard_bold -> bold
pretendard_regular -> regular
pretendard_medium -> medium
pretendard_light -> light
2026-01-22 22:55:14 +09:00
00941d8082 gmarket_sans_light -> pretendard_light으로 폰트 변경 2026-01-22 22:49:54 +09:00
cf612fa0c7 gmarket_sans_medium -> pretendard_medium으로 폰트 변경 2026-01-22 22:46:55 +09:00
6d5018c3fd gmarket_sans_bold -> pretendard_bold로 폰트 변경 2026-01-22 22:43:17 +09:00
f333d301b8 fontFamily 미지정된 TextView/EditText에 android:fontFamily="@font/pretendard_medium" 추가 2026-01-22 22:31:15 +09:00
11c5d57c4e 인기 크리에이터 팔로우 해제 시 확인 다이얼로그 추가 2026-01-22 19:04:36 +09:00
f269044c69 라이브 예약 완료 화면의 날짜 표시 로직 수정
MakeLiveReservationResponse의 필드 변경 사항을 반영하여 날짜 표시
로직을 수정함. UTC 시간을 디바이스 타임존으로 변환하고
yyyy.MM.dd E hh:mm a 포맷으로 표시함.
2026-01-21 18:52:00 +09:00
b1075eee16 다국어 설정 시 날짜 포맷이 섞이는 버그 수정
앱 내 설정 언어와 디바이스 언어가 다를 때 날짜 포맷에 여러 언어가
섞여서 표시되는 문제를 해결하기 위해 앱 설정 언어를 명시적으로
적용하도록 수정. 래핑된 컨텍스트를 사용하여 리소스를 가져오고
날짜 변환 시에도 해당 로케일을 전달하도록 개선.
2026-01-21 16:52:39 +09:00
5adfecf689 라이브 상세 - 다국어 설정 시 날짜 포맷이 섞이는 버그 수정
앱 내 설정 언어와 디바이스 언어가 다를 때 날짜 포맷에 여러 언어가
섞여서 표시되는 문제를 해결하기 위해 앱 설정 언어를 명시적으로
적용하도록 수정. 래핑된 컨텍스트를 사용하여 리소스를 가져오고
날짜 변환 시에도 해당 로케일을 전달하도록 개선.
2026-01-21 16:39:36 +09:00
f44eacaf52 라이브 리스트, 상세, 예약 - 타임존을 적용해서 내려주는 beginDateTime을 제거하고 UTC 타임존이 적용된 beginDateTimeUtc를 사용하도록 수정 2026-01-21 15:48:25 +09:00
258f0036a6 versionCode 214 versionName 1.47.0 2026-01-20 14:05:03 +09:00
4d33d459ee 홈 인기 크리에이터 - 닉네임 UI 높이 조정 2026-01-20 01:50:01 +09:00
1cdbd92d35 콘텐츠 상세 미리듣기 버튼 변경 2026-01-20 01:47:18 +09:00
871603f1bb 콘텐츠 상세 - 미리듣기 미지원 안내 문구 간결하게 수정 2026-01-20 00:58:54 +09:00
032640b068 크리에이터 채널 - 팔로우/팔로잉 버튼 로컬 라이징 적용 2026-01-19 23:53:08 +09:00
2e7c3a5352 홈 - 크리에이터 랭킹 뱃지 변경 2026-01-19 19:59:37 +09:00
c78370ec2a 스플래시 화면 변경 2026-01-19 13:56:51 +09:00
6813a74c13 하트 애니메이션 재생 순서 유지 2026-01-16 14:38:18 +09:00
3980673322 versionCode 213, versionName 1.47.0
라이브 중 전체보기 그리드 뷰 3단에서 2단으로 변경
2026-01-14 12:01:51 +09:00
0b99235ec2 versionCode 212, versionName 1.46.0 2026-01-08 11:34:58 +09:00
906881dc9c 일본어 문자열을 조정한다 2026-01-07 16:41:40 +09:00
d5b6a3f2d6 라이브 상세 UTC 시작 시간 반영
라이브 상세 응답에 beginDateTimeUtc 필드를 사용해 로컬 시간으로 표시한다.
2026-01-06 11:14:34 +09:00
4b4e47d17c 라이브 상세 - 날짜 오류 수정 2025-12-31 18:58:54 +09:00
6e2fcc53a5 versionCode 210, versionName 1.46.0 2025-12-31 18:58:33 +09:00
dfaa3961bf 문자열 리소스 참조로 화면 문구 정리 2025-12-30 15:46:01 +09:00
1d002c4045 영문 UI 문구 개선 2025-12-30 15:42:52 +09:00
4e80d91c20 Merge branch 'main' into feature/i18n 2025-12-26 18:38:45 +09:00
2525811db4 versionCode 209, versionName 1.45.0 2025-12-26 15:41:37 +09:00
5583004515 스플래시 화면 변경 2025-12-26 15:17:43 +09:00
a4a11b5801 다국어 문구 표현 정비 2025-12-26 12:27:26 +09:00
9adc45095b 커뮤니티 게시글 상대 시간 표기 다국어 지원 2025-12-19 14:21:26 +09:00
c66fdf63db 크리에이터 채널 - 최신 콘텐츠 영역 잘리지 않도록 수정 2025-12-17 16:07:58 +09:00
23a98b399c 앱 내 다국어 설정 - 언어 설정 화면에서 RadioButton을 직접 탭할 때 여러 개가 동시에 체크되는 문제를 수정 2025-12-16 20:22:04 +09:00
0ddf416b9d AI 채팅 원작 상세 - 번역 데이터가 있으면 번역 데이터를 표시하도록 수정 2025-12-16 07:07:49 +09:00
f0a2e2c46f 시리즈 상세 - 번역 데이터가 있으면 번역 데이터를 표시하도록 수정 2025-12-16 03:33:01 +09:00
3bd8061a93 불필요한 코드 제거 2025-12-16 03:01:31 +09:00
b67a3fd0b4 API 별로 언어 코드를 쿼리 파라미터로 전송하는 코드 제거 2025-12-12 19:59:07 +09:00
e9df2bfa03 모든 API 요청에 Accept-Language 헤더 추가 2025-12-12 17:43:44 +09:00
a75a11c9f6 앱 내 다국어 언어설정 기능 추가 2025-12-12 14:39:00 +09:00
ebd557ff71 콘텐츠 전체보기 - 폰 언어 설정에 따라 번역 데이터를 조회하도록 수정 2025-12-12 05:58:25 +09:00
0854b76dfa 채팅 탭의 캐릭터 리스트 - 폰 언어 설정에 따라 번역 데이터를 조회하도록 수정 2025-12-12 01:56:37 +09:00
2e7b1ac09e 홈 탭 - 폰 언어 설정에 따라 번역 데이터를 조회하도록 수정 2025-12-12 01:43:34 +09:00
4e15557949 콘텐츠 상세 - 폰 언어 설정에 따라 번역 데이터를 조회하도록 수정 2025-12-11 23:04:23 +09:00
e2c7134f61 캐릭터 상세 - 폰 언어 설정에 따라 번역 데이터를 조회하도록 수정 2025-12-11 20:09:58 +09:00
6f67c4e8e1 versionCode 208, versionName 1.45.0 2025-12-11 20:08:59 +09:00
d8d6509668 Merge branch 'main' into feature/i18n 2025-12-09 19:57:57 +09:00
c9a1417d60 마이페이지 탭에 보이스 크리에이터 지원하기 배너 추가 2025-12-09 18:33:01 +09:00
6cb8616c00 스플래시 이미지 교체 2025-12-09 17:58:58 +09:00
26ad8c39dc 라이브 만들기 버튼 이미지에서 글자로 변경 2025-12-04 13:26:53 +09:00
f2f406d581 메뉴 문자열 리소스화 2025-12-04 00:06:03 +09:00
b4ced77d57 영어·일본어 번역 추가 2025-12-03 23:53:32 +09:00
5ee57b1b2f 레이아웃 문자열 리소스화 2025-12-03 23:46:04 +09:00
57e3edc24e 딥링크 로그 문자열 리소스화 2025-12-03 22:42:01 +09:00
fc984cef22 추천 채널 더보기 문자열 리소스화 2025-12-03 22:37:09 +09:00
81b6cbe655 Live 포함 레이아웃 문자열 리소스화 2025-12-03 22:25:27 +09:00
e838085291 팔로우 알림 문자열 리소스화 2025-12-03 22:03:00 +09:00
0f6ebbfa76 커뮤니티 댓글 문자열 리소스화 2025-12-03 20:42:16 +09:00
ae081994c3 커뮤니티 수정 문자열 리소스화 2025-12-03 20:37:34 +09:00
e67251c9ba 커뮤니티 작성 문자열 리소스화 2025-12-03 20:23:29 +09:00
270d697ce6 커뮤니티 전체보기 문자열 리소스화 2025-12-03 20:07:31 +09:00
a050a56c19 팔로워 리스트 문자열 리소스화 2025-12-03 20:03:18 +09:00
ab7f837bb4 팬톡 전체보기 문자열 리소스화 2025-12-03 19:58:44 +09:00
f2e682c3d3 후원랭킹 전체보기 문자열 리소스화 2025-12-03 19:51:21 +09:00
237d112dec UserProfile 문자열 리소스화
UserProfile 화면 및 어댑터 문구를 ko/en/ja 리소스로 정리
2025-12-03 19:38:26 +09:00
f8769e97f9 시리즈 상세 문자열 리소스화 2025-12-03 19:10:59 +09:00
5ef7896f1d 시리즈 회차 목록 문자열 리소스화 2025-12-03 18:52:00 +09:00
4a5627bf36 시리즈 전체 목록 문자열 리소스화 2025-12-03 18:44:18 +09:00
b521b79fe4 시리즈 메인 문자열 리소스화 2025-12-03 18:34:14 +09:00
35f95aa0f2 오디오 보관함 문자열 리소스화 2025-12-03 18:20:08 +09:00
de042abd79 콘텐츠 업로드 문자열 리소스화 2025-12-03 18:11:54 +09:00
ad1c58bbf5 플레이어 에러 문자열 리소스화 2025-12-03 17:40:43 +09:00
764ca0f892 오디오 댓글 문자열 리소스화
댓글/답글 화면 문구를 ko/en/ja 리소스로 정리했습니다.
2025-12-03 17:32:16 +09:00
1018b6426e 재생목록 수정 문자열 리소스화
수정 화면 레이블과 검증/오류 문구를 ko/en/ja 리소스로 정리했습니다.
2025-12-03 17:20:45 +09:00
c74503beca 재생목록 상세 문자열 리소스화
플레이리스트 상세 화면 문구를 ko/en/ja 리소스로 정리했습니다.
2025-12-03 17:07:15 +09:00
7988b8c63e 재생목록 생성 화면 문자열 리소스화 2025-12-03 15:54:47 +09:00
6ef19d53f4 오디오 콘텐츠 주문 화면 문자열 리소스화
주문 목록/확인 UI의 텍스트를 ko/en/ja 리소스로 이전
2025-12-03 15:32:41 +09:00
dc00fd0277 AudioContent 상세 문자열 리소스화
상세 화면과 다이얼로그의 하드코딩 텍스트를 문자열 리소스로 이동했습니다.

ko/en/ja 리소스 추가 및 기본 시간 표시, 토스트, 공유 문구를 리소스 키로 통일했습니다.
2025-12-03 15:03:23 +09:00
96d14356be 오디오 콘텐츠 신규/인기/테마 화면 문자열 리소스화 2025-12-03 13:52:12 +09:00
a3fb090593 오디오 콘텐츠 전체 화면 문자열 리소스화 2025-12-03 12:12:08 +09:00
5a4b833516 룰렛 설정/프리뷰 문자열 리소스화 2025-12-03 11:40:03 +09:00
82b09f2c63 메뉴 설정 화면 문자열 리소스화
메뉴 선택/저장 UI 텍스트를 문자열 리소스와 다국어 번역으로 교체함

메뉴 프리셋 선택/저장 토스트를 UiText 기반으로 공통 오류 메시지 사용
2025-12-03 11:17:50 +09:00
fd6a025de9 라이브 수정 화면 문자열 리소스로 정리
LiveRoomEdit 화면의 타이틀과 안내 문구를 리소스 키로 치환해 다국어를 적용함

유효성 검증 및 토스트 메시지를 UiText 기반 문자열 리소스로 교체함
2025-12-03 11:10:45 +09:00
99bc5f14f7 라이브 생성 화면 문자열 리소스화
라이브 생성 입력/검증/라벨 문자열을 ko/en/ja 리소스로 분리

토스트, 로딩, 태그 제한 문구를 리소스 기반으로 통일
2025-12-02 21:08:32 +09:00
ddc7b9a76f 라이브룸 문자열 포맷 수정 및 불필요 변수 제거
기부/비밀미션 전송 메시지의 포맷 타입을 문자열로 통일.
사용되지 않는 변수 정리 및 import 정리.
2025-12-02 20:39:03 +09:00
af03d4dafd 라이브룸 문자열 리소스화 마무리 2025-12-02 20:18:05 +09:00
09bc25f035 라이브룸 다이얼로그 문자열 리소스화 2025-12-02 19:15:29 +09:00
072b206035 라이브룸 상세/태그 문자열 리소스 보완 2025-12-02 19:06:22 +09:00
af04ff9bf7 라이브룸 상세/태그 다이얼로그 문자열 리소스화 2025-12-02 18:58:18 +09:00
6bd63fc751 라이브룸 하위 어댑터 문자열 리소스화 2025-12-02 18:44:50 +09:00
4ed6437ce3 라이브룸 화면 문자열 리소스화 2025-12-02 17:12:35 +09:00
b356591aba 라이브 예약 완료 화면 문자열 리소스화 2025-12-02 16:08:04 +09:00
00db1d7bfd LiveReservationStatus 문자열 리소스화 2025-12-02 15:54:13 +09:00
cc517eb4d3 LiveReservationAll 문자열 리소스화 2025-12-02 15:46:28 +09:00
4a8442cb33 LiveNowAll 문자열 리소스화 2025-12-02 15:35:47 +09:00
7023324920 쿠폰 등록 화면 문자열 리소스화 2025-12-02 15:23:19 +09:00
3cfab2c57b 캔 결제 화면 문자열 리소스화 2025-12-02 15:12:07 +09:00
4caaeff0f0 캔 충전 화면 문자열 리소스화 2025-12-02 14:39:30 +09:00
98a6fb6637 캔 내역 화면 문자열 리소스화 2025-12-02 14:22:07 +09:00
50edf85de5 고객센터 화면 문자열 리소스화
- 카카오톡 문의 문자열 리소스화
2025-12-02 14:16:07 +09:00
19b74b2671 포인트 내역 화면 문자열 리소스화 2025-12-02 14:14:33 +09:00
7b6d2cd782 고객센터 화면 문자열 리소스화 2025-12-02 14:08:56 +09:00
b457cf0b4d 관심사 태그 선택 문자열 리소스화 2025-12-02 13:50:09 +09:00
7f27f461f3 차단 목록 화면 문자열 리소스화 2025-12-02 13:45:56 +09:00
3909920a4c 닉네임 변경 화면 문자열 리소스화 2025-12-02 12:24:34 +09:00
5ff35b1da4 비밀번호 변경 화면 문자열 리소스화 2025-12-02 12:03:18 +09:00
90c71026da ProfileUpdateActivity 문자열 리소스화 2025-12-01 22:05:22 +09:00
707107328a AlarmSelectAudioContentActivity 문자열 리소스화 2025-12-01 21:51:00 +09:00
958244c9b2 AlarmListActivity 문자열 리소스화 2025-12-01 21:36:56 +09:00
adc2f759c4 AlarmActivity 문자열 리소스화 2025-12-01 21:27:28 +09:00
c3c1a83e97 SelectMessageRecipientActivity 문자열 리소스화 2025-12-01 18:50:24 +09:00
61fa6d5f74 VoiceMessageWriteFragment 문자열 리소스화 2025-12-01 18:44:52 +09:00
9782462345 VoiceMessageFragment 문자열 리소스화 2025-12-01 18:37:12 +09:00
480b47ee3d TextMessageDetailActivity 문자열 리소스화 2025-12-01 18:29:43 +09:00
2e528f8c7d TextMessageDetailActivity 문자열 리소스화 2025-12-01 18:11:41 +09:00
7e4202db1b TextMessageFragment 문자열 리소스화 2025-12-01 18:03:47 +09:00
09a8019c66 MessageActivity 문자열 리소스화 2025-12-01 18:00:55 +09:00
e44bd68152 신규 캐릭터 전체 문자열 리소스화 2025-12-01 17:49:55 +09:00
2066dbc716 Original 작품 상세 문자열 리소스화 2025-12-01 17:39:02 +09:00
73b2eba1f8 Original 탭 인증 문자열 리소스로 치환 2025-12-01 17:26:11 +09:00
101c396ac2 댓글 목록 문자열 리소스화
댓글/답글 UI, 시간 포맷, 오류 문구 다국어 적용
2025-12-01 17:15:59 +09:00
3cf24c2ab6 캐릭터 상세 문자열 리소스화
CharacterDetail/갤러리 탭 다국어 리소스 추가

UiText로 오류 메시지 지역화 처리
2025-12-01 17:00:26 +09:00
4e0e6708e6 Character 탭 문자열 리소스화 2025-12-01 16:45:22 +09:00
ebe5c342c9 ChatRoom 문자열 리소스화 2025-12-01 16:32:53 +09:00
37c1bc06d0 Talk 탭 빈 상태 문자열 리소스화 2025-12-01 15:43:34 +09:00
98209d0d5f 오디션 역할 상세 문자열 리소스화 2025-12-01 15:32:14 +09:00
159a5ae8b3 오디션 상세 문자열 리소스화 2025-12-01 15:19:01 +09:00
e9afa55aa0 오디션 문자열 리소스로 이관 2025-12-01 15:12:02 +09:00
e727658b24 FollowingCreator 문자열 리소스화 2025-12-01 14:59:21 +09:00
eb0aa9473f 설정·공지·이벤트 문자열 리소스화 2025-12-01 14:35:55 +09:00
981859de1f FindPassword 문자열 리소스화
비밀번호 재설정 안내/버튼/토스트를 ko/en/ja 리소스로 추가

UI 메시지 클래스로 리소스 기반 토스트 처리
2025-12-01 14:06:25 +09:00
1889c6ae10 SignUp 문자열 리소스화
회원가입 검증/오류 문구를 ko/en/ja 리소스로 통합

UI 메시지 클래스를 추가해 토스트·필드 에러를 리소스 기반으로 표시
2025-12-01 14:02:10 +09:00
7e74bd1e4d Login 입력 오류 문구 리소스화
이메일·비밀번호 미입력 안내를 ko/en/ja 리소스로 추가

토스트 메시지를 리소스 기반 메시지 클래스로 전달
2025-12-01 13:56:14 +09:00
4d1e859bbf 공용 토스트 메시지로 unknown 오류를 통합
Toast 메시지를 공용 데이터 클래스로 정의합니다.

화면별 unknown 에러 문자열을 common_error_unknown으로 통일합니다.
2025-12-01 13:40:31 +09:00
492077ddb2 검색 화면 문자열 리소스화 2025-12-01 13:19:57 +09:00
bca527eca0 마이페이지 문자열 리소스화 2025-12-01 12:28:46 +09:00
707dc351ba 라이브 화면 문자열 리소스화 2025-12-01 12:20:15 +09:00
cc55a19e1d 메시지 탭 문자열 리소스화 2025-12-01 12:17:01 +09:00
8b215e553d 챗 탭 문자열 리소스화 2025-12-01 12:13:58 +09:00
af1679c92b 홈 화면 문자열 리소스화 2025-12-01 12:09:43 +09:00
41c11d763e 스플래시와 메인 문자열 리소스화 2025-12-01 11:43:11 +09:00
1efd968b09 사용하지 않는 OnBoarding 제거 2025-12-01 11:26:00 +09:00
ede2dc201c 커밋 메시지 규칙 스크립트 추가 및 가이드 정리
커밋 메시지 규칙 검증 스크립트를 추가하고 가이드를 정리한다.

제목 50자, 본문 72자, 빈 줄, 광고 금지 규칙을 준수한다.
2025-12-01 10:48:39 +09:00
2740522f05 fix(ui): 홈/메인 문자열 리소스화 및 영·일 번역 추가 2025-11-28 18:40:42 +09:00
6e3edd1e96 fix(auth): 로그인/회원가입 문자열 리소스화 및 영·일 번역 추가 2025-11-28 18:01:48 +09:00
0326fa89ea 커밋 메시지 구성 내용 추가 2025-11-28 17:59:52 +09:00
dcdf3b21bc 국/영/일 다국어 문자열 리소스 추가 2025-11-28 17:32:36 +09:00
748da3ec0c add new file AGENTS.md 2025-11-28 17:23:49 +09:00
627 changed files with 11701 additions and 5264 deletions

1
.gitignore vendored
View File

@@ -60,6 +60,7 @@ captures/
.idea/AndroidProjectSystem.xml
.idea/runConfigurations.xml
.idea/deploymentTargetSelector.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-23T14:41:22.468459Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=ce0917195d15ab39017e" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

8
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

18
AGENTS.md Normal file
View File

@@ -0,0 +1,18 @@
질문에 대한 답변과 설명은 한국어로 한다.
## Quality Assurance Guidelines
### Commit Standards
1. Write in Korean.
2. Use the present tense in the subject line (e.g., "Add feature" not "Added feature").
3. Keep the subject line to 50 characters or less.
4. Add a blank line between the subject and body.
5. Keep the body to 72 characters or less per line.
6. Within a paragraph, only break lines when the text exceeds 72 characters.
7. Describe changes to public API features and do not include implementation details such as package-private code.
8. Do not mention test code in commit messages.
9. Do not use any prefix (such as "fix:", "update:", "docs:", "feat:", etc.) in the subject line.
10. Do not start the subject line with a lowercase letter unless the first word is a function name or another identifier that is conventionally lowercase and there is a clear, justifiable reason for the exception. Otherwise, always start with an uppercase letter.
11. Do not include tool advertisements, branding, or promotional content in commit messages.
12. Use separate git commands to stage files before committing.
13. Always validate commits using `work/scripts/check-commit-message-rules.sh` and fix until validation passes.

View File

@@ -63,8 +63,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 35
versionCode 204
versionName "1.44.0"
versionCode 218
versionName "1.48.0"
}
buildTypes {
@@ -83,6 +83,7 @@ android {
buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon"'
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995539"'
manifestPlaceholders = [
URISCHEME : "voiceon",
APPLINK_HOST : "voiceon.onelink.me",
@@ -109,6 +110,7 @@ android {
buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"'
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"'
buildConfigField 'String', 'APPSCHEME', '"voiceon-test"'
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995582"'
manifestPlaceholders = [
URISCHEME : "voiceon-test",
APPLINK_HOST : "voiceon-test.onelink.me",
@@ -234,6 +236,11 @@ dependencies {
implementation 'com.github.orbitalsonic:Sonic-Water-Wave-Animation:2.0.1'
// Line
implementation("com.linecorp.linesdk:linesdk:5.6.1") {
exclude group: "org.jetbrains.kotlin", module: "kotlin-android-extensions-runtime"
}
// ----- Test dependencies -----
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.20.0'

View File

@@ -243,3 +243,5 @@
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }
-dontwarn com.linecorp.linesdk.BR

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">VoiceOn-Test</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ボイスオン-テスト</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">보이스온-테스트</string>
</resources>

View File

@@ -63,6 +63,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:targetApi="31">
<activity
android:name=".main.DeepLinkActivity"
@@ -109,6 +110,7 @@
<activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".audio_content.all.AudioContentAllActivity" />
<activity android:name=".settings.language.LanguageSettingsActivity" />
<activity
android:name=".user.signup.SignUpActivity"
android:windowSoftInputMode="stateVisible" />
@@ -160,7 +162,6 @@
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".message.MessageActivity" />
<activity android:name=".onboarding.OnBoardingActivity" />
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />

View File

@@ -15,8 +15,10 @@ import com.kakao.sdk.common.KakaoSdk
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.di.AppDI
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
import tech.notifly.Notifly
@@ -36,6 +38,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SodaLiveApplicationHolder.init(this)
SharedPreferenceManager.init(applicationContext)
ImageLoaderProvider.init(applicationContext)
@@ -108,8 +111,13 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
logUtmInFirebase()
}
DeepLinkResult.Status.NOT_FOUND -> Logger.d("딥링크를 찾을 수 없습니다.")
DeepLinkResult.Status.ERROR -> Logger.d("딥링크 처리 중 오류 발생: ${deepLinkResult.error}")
DeepLinkResult.Status.NOT_FOUND -> Logger.d(
getString(R.string.deeplink_not_found)
)
DeepLinkResult.Status.ERROR -> Logger.d(
getString(R.string.deeplink_error, deepLinkResult.error)
)
}
}
}

View File

@@ -38,6 +38,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
private var userId: Long = 0
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private val allCategoryLabel by lazy { getString(R.string.audio_content_label_all) }
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
@@ -53,7 +54,11 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
super.onCreate(savedInstanceState)
if (userId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
getString(R.string.screen_audio_content_error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}
@@ -64,7 +69,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "콘텐츠 전체보기"
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
audioContentAdapter = AudioContentAdapter {
@@ -75,7 +80,9 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
activityResultLauncher.launch(intent)
}
categoryAdapter = AudioContentCategoryAdapter {
categoryAdapter = AudioContentCategoryAdapter(
allCategoryLabel = allCategoryLabel
) {
viewModel.selectCategory(it, userId = userId)
}
@@ -259,7 +266,7 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
if (it.isNotEmpty()) {
binding.rvCategory.visibility = View.VISIBLE
val items = it as MutableList<GetCategoryListResponse>
items.add(0, GetCategoryListResponse(0, "전체"))
items.add(0, GetCategoryListResponse(0, allCategoryLabel))
categoryAdapter.addItems(items = items)
} else {
binding.rvCategory.visibility = View.GONE

View File

@@ -66,7 +66,8 @@ class AudioContentAdapter(
} else {
binding.tvPrice.visibility = View.VISIBLE
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.text =
binding.root.context.getString(R.string.audio_content_price_free)
binding.tvPrice.setCompoundDrawables(null, null, null, null)
} else {
binding.tvPrice.text = item.price.moneyFormat()

View File

@@ -1,11 +1,13 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.category.CategoryApi
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.settings.ContentType
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -70,7 +72,10 @@ class AudioContentRepository(
authHeader = token
)
fun getAudioContentDetail(audioContentId: Long, token: String) = api.getAudioContentDetail(
fun getAudioContentDetail(
audioContentId: Long,
token: String
) = api.getAudioContentDetail(
id = audioContentId,
timezone = TimeZone.getDefault().id,
authHeader = token
@@ -150,7 +155,8 @@ class AudioContentRepository(
fun getContentRanking(
page: Int,
size: Int,
sortType: String = "매출",
sortType: String = SodaLiveApplicationHolder.get()
.getString(R.string.screen_home_sort_revenue),
token: String
) = api.getContentRanking(
page = page - 1,

View File

@@ -6,9 +6,11 @@ import com.google.gson.annotations.SerializedName
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.category.GetCategoryListResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
class AudioContentViewModel(private val repository: AudioContentRepository) : BaseViewModel() {
@@ -80,7 +82,8 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
@@ -94,7 +97,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
if (onFailure != null) {
onFailure()
}
@@ -134,7 +140,8 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
@@ -144,7 +151,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)

View File

@@ -35,6 +35,7 @@ class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
private var isFree: Boolean = false
private var isPointOnly: Boolean = false
private val allThemeLabel by lazy { getString(R.string.screen_home_theme_all) }
override fun onCreate(savedInstanceState: Bundle?) {
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
@@ -56,9 +57,9 @@ class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = when {
isPointOnly -> "포인트 대여 전체"
isFree -> "무료 콘텐츠 전체"
else -> "콘텐츠 전체보기"
isPointOnly -> getString(R.string.screen_audio_content_all_title_point_only)
isFree -> getString(R.string.screen_audio_content_all_title_free)
else -> getString(R.string.screen_audio_content_title)
}
binding.toolbar.tvBack.setOnClickListener { finish() }
@@ -75,9 +76,10 @@ class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
}
private fun setupTheme() {
themeAdapter = HomeContentThemeAdapter {
themeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
adapter.addItems(emptyList())
viewModel.selectTheme(it, isFree = isFree, isPointOnly = isPointOnly)
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
viewModel.selectTheme(theme, isFree = isFree, isPointOnly = isPointOnly)
}
binding.rvTheme.layoutManager = LinearLayoutManager(
@@ -169,7 +171,9 @@ class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
}
viewModel.themeListLiveData.observe(this) {
themeAdapter.addItems(it)
val themes = mutableListOf(allThemeLabel)
themes.addAll(it)
themeAdapter.addItems(themes)
}
viewModel.itemsLiveData.observe(this) { list ->

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.home.AudioContentMainItem
class AudioContentAllViewModel(
@@ -35,7 +37,7 @@ class AudioContentAllViewModel(
private var page = 1
private val size = 20
private var isLast = false
private var selectedTheme = "전체"
private var selectedTheme = ""
fun reset() {
page = 1
@@ -46,6 +48,7 @@ class AudioContentAllViewModel(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null
) {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getAudioContentActiveThemeList(
isFree = isFree,
@@ -57,15 +60,12 @@ class AudioContentAllViewModel(
.subscribe(
{
if (it.success && it.data != null) {
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
_themeListLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
}
@@ -74,7 +74,7 @@ class AudioContentAllViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
}
)
)
@@ -86,6 +86,7 @@ class AudioContentAllViewModel(
) {
if (_isLoading.value == true || isLast) return
_isLoading.value = true
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getAllAudioContents(
@@ -94,11 +95,7 @@ class AudioContentAllViewModel(
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
sortType = _sortLiveData.value!!,
theme = if (selectedTheme == "전체") {
null
} else {
selectedTheme
},
theme = selectedTheme.ifBlank { null },
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -115,7 +112,7 @@ class AudioContentAllViewModel(
_isLoading.value = false
}, { t ->
_isLoading.value = false
_toastLiveData.postValue(t.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(t.message ?: unknownError)
})
)
}

View File

@@ -11,6 +11,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
@@ -32,6 +33,7 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
private lateinit var newContentThemeAdapter: HomeContentThemeAdapter
private lateinit var newContentAdapter: AudioContentNewAllAdapter
private val allThemeLabel by lazy { getString(R.string.screen_home_theme_all) }
private var isFree: Boolean = false
@@ -47,16 +49,16 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = if (isFree) {
"새로운 무료 콘텐츠"
getString(R.string.screen_audio_content_new_all_title_free)
} else {
"새로운 단편"
getString(R.string.screen_audio_content_new_all_title)
}
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvNotice.text = if (isFree) {
"※ 최근 2주간 등록된 새로운 콘텐츠 입니다."
getString(R.string.screen_audio_content_new_all_notice_free)
} else {
"※ 최근 2주간 등록된 새로운 단편 입니다."
getString(R.string.screen_audio_content_new_all_notice)
}
setupNewContentTheme()
@@ -65,10 +67,11 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
@SuppressLint("NotifyDataSetChanged")
private fun setupNewContentTheme() {
newContentThemeAdapter = HomeContentThemeAdapter {
newContentThemeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
newContentAdapter.clear()
newContentAdapter.notifyDataSetChanged()
viewModel.selectTheme(it, isFree = isFree)
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
viewModel.selectTheme(theme, isFree = isFree)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
@@ -171,7 +174,9 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
viewModel.themeListLiveData.observe(this) {
newContentThemeAdapter.addItems(it)
val themes = mutableListOf(allThemeLabel)
themes.addAll(it)
newContentThemeAdapter.addItems(themes)
}
viewModel.newContentListLiveData.observe(this) {

View File

@@ -68,7 +68,7 @@ class AudioContentNewAllAdapter(
binding.tvCan.text = item.price.moneyFormat()
} else {
binding.ivCan.visibility = View.GONE
binding.tvCan.text = "무료"
binding.tvCan.text = context.getString(R.string.audio_content_price_free)
}
binding.tvTime.text = item.duration

View File

@@ -8,7 +8,9 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AudioContentNewAllViewModel(
private val repository: AudioContentRepository
@@ -41,15 +43,12 @@ class AudioContentNewAllViewModel(
fun getNewContentList(isFree: Boolean = false) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getNewContentAllOfTheme(
isFree = isFree,
theme = if (selectedTheme == "전체") {
""
} else {
selectedTheme
},
theme = selectedTheme,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
@@ -71,9 +70,7 @@ class AudioContentNewAllViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
}
@@ -82,7 +79,7 @@ class AudioContentNewAllViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
}
)
)
@@ -90,6 +87,7 @@ class AudioContentNewAllViewModel(
}
fun getThemeList() {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
@@ -97,15 +95,12 @@ class AudioContentNewAllViewModel(
.subscribe(
{
if (it.success && it.data != null) {
val themeList = listOf("전체").union(it.data).toList()
_themeListLiveData.postValue(themeList)
_themeListLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
}
@@ -114,7 +109,7 @@ class AudioContentNewAllViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
}
)
)

View File

@@ -8,6 +8,7 @@ import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.base.BaseActivity
@@ -37,7 +38,7 @@ class AudioContentRankingAllActivity : BaseActivity<ActivityAudioContentRankingA
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.toolbar.tvBack.text = "인기 콘텐츠"
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_ranking_title)
adapter = AudioContentRankingAllAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)

View File

@@ -43,7 +43,7 @@ class AudioContentRankingAllAdapter(
binding.tvNickname.text = item.creatorNickname
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.text = context.getString(R.string.audio_content_price_free)
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.white))
binding.tvPrice.setCompoundDrawables(null, null, null, null)
binding.tvPrice.setPadding(

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class AudioContentRankingAllViewModel(
private val repository: AudioContentRepository
@@ -37,11 +39,14 @@ class AudioContentRankingAllViewModel(
private var pageSize = 10
private var isLast = false
private var selectedSort = "매출"
private var selectedSort =
SodaLiveApplicationHolder.get().getString(R.string.screen_home_sort_revenue)
fun getAudioContentRanking() {
if (!_isLoading.value!! && !isLast && page <= 5) {
_isLoading.value = true
val unknownError =
SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getContentRanking(
page = page,
@@ -69,15 +74,13 @@ class AudioContentRankingAllViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
}
)
)
@@ -85,6 +88,7 @@ class AudioContentRankingAllViewModel(
}
fun getAudioContentRankingSortType() {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getContentRankingSortType(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
@@ -97,15 +101,13 @@ class AudioContentRankingAllViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
}
)
)

View File

@@ -38,7 +38,7 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
if (themeId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
getString(R.string.screen_audio_content_error_invalid_request_retry),
Toast.LENGTH_LONG
).show()

View File

@@ -8,7 +8,9 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AudioContentAllByThemeViewModel(
private val repository: AudioContentRepository
@@ -61,7 +63,8 @@ class AudioContentAllByThemeViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
@@ -71,7 +74,10 @@ class AudioContentAllByThemeViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)

View File

@@ -1,7 +1,5 @@
package kr.co.vividnext.sodalive.audio_content.box
import android.os.Handler
import android.os.Looper
import android.widget.LinearLayout
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
@@ -37,14 +35,14 @@ class AudioContentBoxActivity : BaseActivity<ActivityAudioContentBoxBinding>(
}
private fun setupToolbar() {
binding.toolbar.tvBack.text = "내 보관함"
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_box_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
}
private fun setupTabs() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("구매목록"))
tabs.addTab(tabs.newTab().setText("재생목록"))
tabs.addTab(tabs.newTab().setText(R.string.screen_audio_content_box_tab_orders))
tabs.addTab(tabs.newTab().setText(R.string.screen_audio_content_box_tab_playlists))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {

View File

@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemContentCategoryBinding
class AudioContentCategoryAdapter(
private val allCategoryLabel: String,
private val onClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCategoryAdapter.ViewHolder>() {
@@ -24,7 +25,7 @@ class AudioContentCategoryAdapter(
fun bind(item: GetCategoryListResponse) {
if (
item.category == selectedCategory ||
(selectedCategory == "" && item.category == "전체")
(selectedCategory.isEmpty() && item.category == allCategoryLabel)
) {
binding.tvCategory.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_3bb9f1

View File

@@ -99,9 +99,12 @@ class AudioContentCommentAdapter(
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.text = if (item.replyCount > 0) {
"답글 ${item.replyCount}"
context.getString(
R.string.audio_content_comment_reply_count_format,
item.replyCount
)
} else {
"답글 쓰기"
context.getString(R.string.audio_content_comment_write_reply)
}
if (

View File

@@ -106,9 +106,9 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
title = getString(R.string.audio_content_comment_delete_title),
desc = getString(R.string.audio_content_comment_delete_message),
confirmButtonTitle = getString(R.string.screen_audio_content_detail_delete),
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
@@ -116,7 +116,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
},

View File

@@ -6,7 +6,9 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AudioContentCommentListViewModel(
private val repository: AudioContentCommentRepository
@@ -27,6 +29,17 @@ class AudioContentCommentListViewModel(
val totalCommentCount: LiveData<Int>
get() = _totalCommentCount
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
private val noChangeMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.audio_content_comment_no_change)
private val inputRequiredMessage: String
get() = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_comment_input_required
)
var page = 1
private var isLast = false
private val size = 10
@@ -59,9 +72,7 @@ class AudioContentCommentListViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
if (onFailure != null) {
@@ -74,7 +85,7 @@ class AudioContentCommentListViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
if (onFailure != null) {
onFailure()
}
@@ -110,16 +121,14 @@ class AudioContentCommentListViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)
@@ -132,12 +141,12 @@ class AudioContentCommentListViewModel(
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
_toastLiveData.postValue(noChangeMessage)
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
_toastLiveData.postValue(inputRequiredMessage)
return
}
@@ -169,14 +178,14 @@ class AudioContentCommentListViewModel(
isLast = false
getCommentList(audioContentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = it.message ?: unknownErrorMessage
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -112,9 +112,9 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "댓글 삭제",
desc = "삭제하시겠습니까?",
confirmButtonTitle = "삭제",
title = getString(R.string.audio_content_comment_delete_title),
desc = getString(R.string.audio_content_comment_delete_message),
confirmButtonTitle = getString(R.string.screen_audio_content_detail_delete),
confirmButtonClick = {
viewModel.modifyComment(
commentId = it,
@@ -122,7 +122,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}

View File

@@ -6,7 +6,9 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AudioContentCommentReplyViewModel(
private val repository: AudioContentCommentRepository
@@ -23,6 +25,17 @@ class AudioContentCommentReplyViewModel(
val commentList: LiveData<List<GetAudioContentCommentListItem>>
get() = _commentList
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
private val noChangeMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.audio_content_comment_no_change)
private val inputRequiredMessage: String
get() = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_comment_input_required
)
var page = 1
private var isLast = false
private val size = 10
@@ -53,9 +66,7 @@ class AudioContentCommentReplyViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
if (onFailure != null) {
@@ -68,7 +79,7 @@ class AudioContentCommentReplyViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
if (onFailure != null) {
onFailure()
}
@@ -104,16 +115,14 @@ class AudioContentCommentReplyViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)
@@ -126,12 +135,12 @@ class AudioContentCommentReplyViewModel(
isActive: Boolean? = null
) {
if (comment == null && isActive == null) {
_toastLiveData.postValue("변경사항이 없습니다.")
_toastLiveData.postValue(noChangeMessage)
return
}
if (comment != null && comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
_toastLiveData.postValue(inputRequiredMessage)
return
}
@@ -163,14 +172,14 @@ class AudioContentCommentReplyViewModel(
isLast = false
getCommentReplyList(parentCommentId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = it.message ?: unknownErrorMessage
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@@ -31,7 +32,10 @@ class AudioContentDeleteDialog(
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?"
dialogView.tvTitle.text = activity.getString(
R.string.screen_audio_content_detail_delete_confirm_message,
title
)
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
@@ -43,7 +47,9 @@ class AudioContentDeleteDialog(
} else {
Toast.makeText(
activity,
"동의하셔야 삭제할 수 있습니다.",
activity.getString(
R.string.screen_audio_content_detail_delete_consent_required
),
Toast.LENGTH_LONG
).show()
}

View File

@@ -42,6 +42,7 @@ import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
@@ -94,8 +95,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 0
binding.ivPlayOrPause.setImageResource(0)
binding.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00"
binding.ivPlayOrPause.visibility = View.GONE
binding.llPreview.visibility = View.GONE
binding.tvTotalDuration.text = getString(
R.string.screen_audio_content_detail_time_total_default
)
binding.tvCurrentDuration.text =
getString(R.string.screen_audio_content_detail_time_default)
binding.rlPreviewAlert.visibility = View.GONE
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
@@ -109,7 +115,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
getString(R.string.screen_audio_content_error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}
@@ -148,7 +158,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvBack.text = "콘텐츠 상세"
binding.tvBack.text = getString(R.string.screen_audio_content_detail_title)
binding.tvBack.setOnClickListener { finish() }
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
binding.ivMenu.setOnClickListener {
@@ -292,9 +302,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
LayoutInflater.from(this)
) { can, message, _ ->
if (can <= 0) {
showToast("1캔 이상 후원하실 수 있습니다.")
showToast(getString(R.string.screen_audio_content_detail_donation_minimum))
} else if (message.isBlank()) {
showToast("함께 보낼 메시지를 입력하세요.")
showToast(
getString(R.string.screen_audio_content_detail_donation_empty_message)
)
} else {
donation(can, message)
}
@@ -355,13 +367,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
SodaDialog(
this@AudioContentDetailActivity,
layoutInflater,
"고정 한도 도달",
"이 콘텐츠를 고정하시겠어요? " +
"채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다." +
"이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.",
"확인",
getString(R.string.screen_audio_content_detail_pin_limit_title),
getString(R.string.screen_audio_content_detail_pin_limit_message),
getString(R.string.confirm),
{ viewModel.pinContent(audioContentId) },
"취소",
getString(R.string.cancel),
{}
).show(screenWidth)
}
@@ -446,7 +456,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
)
title = it.title
title = it.translated?.title ?: it.title
setupCreatorArea(it.creator)
setupMosaicArea(it.isMosaic)
setupPlayArea(it)
@@ -694,28 +704,31 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
binding.tvUnit.text = if (SharedPreferenceManager.userId == 17958L) {
"원으로"
getString(R.string.screen_audio_content_detail_purchase_unit_won)
} else {
"캔으로"
getString(R.string.screen_audio_content_detail_purchase_unit_can)
}
when (response.purchaseOption) {
PurchaseOption.BOTH -> {
binding.tvStrPurchaseOrRental.text = " 구매하기"
binding.tvStrPurchaseOrRental.text =
getString(R.string.screen_audio_content_detail_action_buy)
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_3bb9f1
)
}
PurchaseOption.BUY_ONLY -> {
binding.tvStrPurchaseOrRental.text = " 소장하기"
binding.tvStrPurchaseOrRental.text =
getString(R.string.screen_audio_content_detail_action_keep)
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_59548f
)
}
PurchaseOption.RENT_ONLY -> {
binding.tvStrPurchaseOrRental.text = " 대여하기"
binding.tvStrPurchaseOrRental.text =
getString(R.string.screen_audio_content_detail_action_rental)
binding.llPurchase.setBackgroundResource(
R.drawable.bg_round_corner_5_3_548f7d
)
@@ -754,10 +767,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.flSoldOut.visibility = View.GONE
binding.tvSoldOutBig.visibility = View.GONE
binding.ivPlayOrPause.visibility = View.GONE
binding.llPreview.visibility = View.GONE
binding.ivSeekBackward10.visibility = View.GONE
binding.ivSeekForward10.visibility = View.GONE
binding.tvPreviewNo.visibility = View.GONE
binding.tvTotalDuration.text = " / ${response.duration}"
binding.llPreviewNo.visibility = View.GONE
binding.tvTotalDuration.text = getString(
R.string.screen_audio_content_detail_total_duration_format,
response.duration
)
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
!response.existOrdered &&
@@ -775,8 +792,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
!isAlertPreview ||
(response.isActivePreview && response.contentUrl.isNotBlank())
) {
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.ivPlayOrPause.setOnClickListener {
if (isAlertPreview) {
binding.llPreview.visibility = View.VISIBLE
} else {
binding.ivPlayOrPause.visibility = View.VISIBLE
}
val playClickAction = View.OnClickListener {
startService(
Intent(
applicationContext,
@@ -820,13 +842,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
)
}
binding.ivPlayOrPause.setImageResource(
if (!isAlertPreview) {
R.drawable.btn_audio_content_play
} else {
R.drawable.btn_audio_content_preview_play
}
)
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
binding.ivPlayOrPause.setOnClickListener(playClickAction)
binding.llPreview.setOnClickListener(playClickAction)
if (!isAlertPreview) {
binding.ivSeekForward10.visibility = View.VISIBLE
@@ -855,7 +873,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
}
} else if (response.releaseDate == null) {
binding.tvPreviewNo.visibility = View.VISIBLE
binding.llPreviewNo.visibility = View.VISIBLE
}
binding.ivPoint.visibility = if (response.isAvailableUsePoint) {
@@ -894,13 +912,13 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvRemainingTime.visibility = View.GONE
}
binding.tvTitle.text = response.title
binding.tvDetail.text = response.detail
binding.tvTitle.text = response.translated?.title ?: response.title
binding.tvDetail.text = response.translated?.detail ?: response.detail
binding.tvDetail.setOnClickListener { viewModel.toggleExpandDetail() }
if (response.tag.isNotBlank()) {
binding.tvTag.visibility = View.VISIBLE
binding.tvTag.text = response.tag
binding.tvTag.text = response.translated?.tags ?: response.tag
} else {
binding.tvTag.visibility = View.GONE
}
@@ -925,7 +943,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_pressed)
} else {
binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
SodaLiveApplicationHolder.get().getString(R.string.common_zero)
} else {
"${likeCount - 1}"
}
@@ -941,7 +959,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오 콘텐츠 공유")
val shareIntent = Intent.createChooser(
intent,
getString(R.string.screen_audio_content_detail_share_title)
)
startActivity(shareIntent)
}
}
@@ -1203,23 +1224,29 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (this@AudioContentDetailActivity.audioContentId == contentId) {
runOnUiThread {
if (changeUi != null && changeUi) {
binding.ivPlayOrPause.setImageResource(
if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause
if (isPlaying != null && isPlaying) {
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.llPreview.visibility = View.GONE
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_pause)
} else {
if (isAlertPreview) {
binding.ivPlayOrPause.visibility = View.GONE
binding.llPreview.visibility = View.VISIBLE
} else {
if (isAlertPreview) {
R.drawable.btn_audio_content_preview_play
} else {
R.drawable.btn_audio_content_play
}
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.llPreview.visibility = View.GONE
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
}
)
}
}
}
if (duration != null && duration > 0) {
binding.sbProgress.max = duration
binding.tvTotalDuration.text = " / ${Utils.convertDurationToString(duration)}"
binding.tvTotalDuration.text = getString(
R.string.screen_audio_content_detail_total_duration_format,
Utils.convertDurationToString(duration)
)
}
if (progress != null && progress > 0) {

View File

@@ -36,10 +36,11 @@ class AudioContentDetailMenuBottomSheetDialog(
if (isPin) {
dialog.ivPin.setImageResource(R.drawable.ic_pin_cancel)
dialog.tvPin.text = "내 채널에 고정 취소"
dialog.tvPin.text =
getString(R.string.screen_audio_content_detail_pin_cancel)
} else {
dialog.ivPin.setImageResource(R.drawable.ic_pin)
dialog.tvPin.text = "내 채널에 고정"
dialog.tvPin.text = getString(R.string.screen_audio_content_detail_pin)
}
dialog.llPin.setOnClickListener {

View File

@@ -1,15 +1,18 @@
package kr.co.vividnext.sodalive.audio_content.detail
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
@@ -52,7 +55,13 @@ class AudioContentDetailViewModel(
val isContentPlayLoopLiveData: LiveData<Boolean>
get() = _isContentPlayLoopLiveData
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
private fun getString(@StringRes resId: Int, vararg args: Any) =
SodaLiveApplicationHolder.get().getString(resId, *args)
fun getAudioContentDetail(
audioContentId: Long,
onFailure: (() -> Unit)? = null
) {
if (!isLoading.value!!) {
isLoading.value = true
}
@@ -73,7 +82,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -87,7 +96,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
if (onFailure != null) {
onFailure()
}
@@ -116,7 +125,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -125,7 +134,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -155,9 +164,13 @@ class AudioContentDetailViewModel(
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue(
if (orderType == OrderType.RENTAL) {
"대여가 완료되었습니다."
getString(
R.string.screen_audio_content_detail_order_rental_success
)
} else {
"구매가 완료되었습니다."
getString(
R.string.screen_audio_content_detail_order_keep_success
)
}
)
} else {
@@ -168,7 +181,7 @@ class AudioContentDetailViewModel(
}
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -177,7 +190,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -201,7 +214,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -211,7 +224,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -240,7 +253,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -250,7 +263,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -283,7 +296,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -291,7 +304,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -327,7 +340,7 @@ class AudioContentDetailViewModel(
if (it.success) {
_toastLiveData.postValue(
"삭제되었습니다."
getString(R.string.screen_audio_content_detail_delete_success)
)
onSuccess()
} else {
@@ -335,7 +348,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -343,7 +356,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -365,7 +378,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"신고가 접수되었습니다."
getString(R.string.screen_audio_content_detail_report_submitted)
)
}
@@ -374,7 +387,9 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("신고가 접수되었습니다.")
_toastLiveData.postValue(
getString(R.string.screen_audio_content_detail_report_submitted)
)
}
)
)
@@ -404,7 +419,10 @@ class AudioContentDetailViewModel(
if (it.success) {
SharedPreferenceManager.can -= can
_toastLiveData.postValue(
"${can.moneyFormat()}캔을 후원하였습니다."
getString(
R.string.screen_audio_content_detail_donation_success,
can.moneyFormat()
)
)
onSuccess()
} else {
@@ -412,7 +430,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -420,7 +438,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -440,14 +458,16 @@ class AudioContentDetailViewModel(
isLoading.value = false
if (it.success) {
_toastLiveData.postValue("고정되었습니다.")
_toastLiveData.postValue(
getString(R.string.screen_audio_content_detail_pin_success)
)
getAudioContentDetail(audioContentId)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -455,7 +475,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -475,14 +495,16 @@ class AudioContentDetailViewModel(
isLoading.value = false
if (it.success) {
_toastLiveData.postValue("해제되었습니다.")
_toastLiveData.postValue(
getString(R.string.screen_audio_content_detail_unpin_success)
)
getAudioContentDetail(audioContentId)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -490,7 +512,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)

View File

@@ -8,6 +8,7 @@ import android.view.WindowManager
import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentReportBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@@ -37,7 +38,13 @@ class AudioContentReportDialog(
alertDialog.dismiss()
confirmButtonClick(reason)
} else {
Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show()
Toast.makeText(
activity,
activity.getString(
R.string.screen_audio_content_detail_report_reason_required
),
Toast.LENGTH_LONG
).show()
}
}

View File

@@ -43,7 +43,8 @@ data class GetAudioContentDetailResponse(
@SerializedName("previousContent") val previousContent: OtherContentResponse?,
@SerializedName("nextContent") val nextContent: OtherContentResponse?,
@SerializedName("buyerList") val buyerList: List<ContentBuyer>,
@SerializedName("isAvailableUsePoint") val isAvailableUsePoint: Boolean
@SerializedName("isAvailableUsePoint") val isAvailableUsePoint: Boolean,
@SerializedName("translated") val translated: TranslatedContent?
)
@Keep
@@ -68,3 +69,10 @@ data class ContentBuyer(
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String
)
@Keep
data class TranslatedContent(
@SerializedName("title") val title: String?,
@SerializedName("detail") val detail: String?,
@SerializedName("tags") val tags: String?
)

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding
class AudioContentMainNewContentThemeAdapter(
@@ -24,8 +25,10 @@ class AudioContentMainNewContentThemeAdapter(
fun bind(theme: String) {
if (
theme == selectedTheme ||
(selectedTheme == "" && theme == "전체") ||
(selectedTheme == "" && theme == "매출")
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
.getString(R.string.audio_content_label_all)) ||
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
.getString(R.string.screen_home_sort_revenue))
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_3bb9f1

View File

@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -42,7 +43,12 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
super.onCreate(savedInstanceState)
val audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
SodaLiveApplicationHolder.get()
.getString(R.string.screen_audio_content_error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}

View File

@@ -7,9 +7,11 @@ import com.google.gson.Gson
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
@@ -111,7 +113,8 @@ class AudioContentModifyViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
@@ -125,7 +128,10 @@ class AudioContentModifyViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
if (onFailure != null) {
onFailure()
}
@@ -201,7 +207,8 @@ class AudioContentModifyViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
@@ -211,7 +218,8 @@ class AudioContentModifyViewModel(
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
@@ -221,12 +229,18 @@ class AudioContentModifyViewModel(
private fun validateData(): Boolean {
if (title != null && title!!.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.audio_content_upload_error_title_required)
)
return false
}
if (detail != null && (detail!!.isBlank() || detail!!.length < 5)) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.audio_content_upload_error_detail_min_length)
)
return false
}

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
@@ -17,7 +16,6 @@ import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBindin
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
@SuppressLint("SetTextI18n")
class AudioContentOrderConfirmDialog(
activity: Activity,
layoutInflater: LayoutInflater,
@@ -38,6 +36,7 @@ class AudioContentOrderConfirmDialog(
val dialogView = DialogAudioContentOrderConfirmBinding.inflate(layoutInflater)
init {
val context = dialogView.root.context
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
@@ -82,7 +81,10 @@ class AudioContentOrderConfirmDialog(
dialogView.tvPoint.visibility = View.GONE
dialogView.tvPlus.visibility = View.GONE
dialogView.ivCan.visibility = View.GONE
dialogView.tvCan.text = "${(price * 110).moneyFormat()}"
dialogView.tvCan.text = context.getString(
R.string.audio_content_order_price_won_format,
(price * 110).moneyFormat()
)
} else {
if (usablePoint > 0) {
dialogView.ivPoint.visibility = View.VISIBLE
@@ -111,19 +113,19 @@ class AudioContentOrderConfirmDialog(
}
}
if (SharedPreferenceManager.userId == 17958L) {
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?"
} else {
"콘텐츠를 소장하시겠습니까?"
}
} else {
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 금액이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 금액이 차감됩니다."
}
val noticeResId = when {
SharedPreferenceManager.userId == 17958L && orderType == OrderType.RENTAL ->
R.string.audio_content_order_confirm_notice_rental_simple
SharedPreferenceManager.userId == 17958L && orderType == OrderType.KEEP ->
R.string.audio_content_order_confirm_notice_keep_simple
orderType == OrderType.RENTAL ->
R.string.audio_content_order_confirm_notice_rental
else -> R.string.audio_content_order_confirm_notice_keep
}
dialogView.tvNotice.text = dialogView.root.context.getString(noticeResId)
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()

View File

@@ -1,11 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
@@ -28,23 +28,31 @@ class AudioContentOrderFragment(
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = requireContext()
if (SharedPreferenceManager.userId == 17958L) {
binding.tvKeepDate.text = "(이용기간 1년)"
binding.tvKeepDate.text =
context.getString(R.string.audio_content_order_keep_period_special)
binding.ivKeepCan.visibility = View.GONE
binding.ivRentalCan.visibility = View.GONE
} else {
binding.tvKeepDate.text = "(서비스 종료시까지)"
binding.tvKeepDate.text =
context.getString(R.string.audio_content_order_keep_period_default)
binding.ivKeepCan.visibility = View.VISIBLE
binding.ivRentalCan.visibility = View.VISIBLE
}
if (SharedPreferenceManager.userId == 17958L) {
binding.tvKeep.text = "${(price * 110).moneyFormat()}"
binding.tvRental.text = "${(ceil(price * 0.7).toInt() * 110).moneyFormat()}"
binding.tvKeep.text = context.getString(
R.string.audio_content_order_price_won_format,
(price * 110).moneyFormat()
)
binding.tvRental.text = context.getString(
R.string.audio_content_order_price_won_format,
(ceil(price * 0.7).toInt() * 110).moneyFormat()
)
} else {
binding.tvKeep.text = price.moneyFormat()
binding.tvRental.text = ceil(price * 0.7).toInt().moneyFormat()

View File

@@ -33,7 +33,7 @@ class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderList
}
private fun setupToolbar() {
binding.toolbar.tvBack.text = "콘텐츠 보관함"
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_order_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
}
}

View File

@@ -122,7 +122,7 @@ class AudioContentOrderListFragment : BaseFragment<FragmentAudioContentOrderList
}
viewModel.totalCount.observe(viewLifecycleOwner) {
binding.tvTotalCount.text = "$it"
binding.tvTotalCount.text = it.toString()
}
}
}

View File

@@ -5,8 +5,10 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentOrderListViewModel(
@@ -34,6 +36,7 @@ class AudioContentOrderListViewModel(
private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
if (_isLoading.value == false) {
_isLoading.value = true
compositeDisposable.add(
@@ -59,9 +62,7 @@ class AudioContentOrderListViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownError)
}
if (onFailure != null) {
@@ -73,7 +74,7 @@ class AudioContentOrderListViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownError)
if (onFailure != null) {
onFailure()
}

View File

@@ -263,7 +263,7 @@ class AudioContentPlayerFragment(
if (mediaController == null) {
Toast.makeText(
requireContext(),
"플레이어를 실행하지 못했습니다.\n다시 시도해 주세요.",
getString(R.string.audio_content_player_error_launch_failed),
Toast.LENGTH_LONG
).show()
dismiss()

View File

@@ -7,9 +7,9 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemPlaylistListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.R
class AudioContentPlaylistListAdapter(
private val onClickItem: (Long) -> Unit
@@ -20,7 +20,6 @@ class AudioContentPlaylistListAdapter(
inner class ViewHolder(
private val binding: ItemPlaylistListBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetPlaylistsItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
@@ -29,7 +28,10 @@ class AudioContentPlaylistListAdapter(
}
binding.tvTitle.text = item.title
binding.tvContentCount.text = "${item.contentCount}"
binding.tvContentCount.text = binding.root.context.getString(
R.string.audio_content_playlist_content_count,
item.contentCount
)
if (item.desc.isNotBlank()) {
binding.tvDesc.text = item.desc

View File

@@ -1,14 +1,16 @@
package kr.co.vividnext.sodalive.audio_content.playlist
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateActivity
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
@@ -43,6 +45,7 @@ class AudioContentPlaylistListFragment : BaseFragment<FragmentAudioContentPlayli
viewModel.getPlaylistList()
}
@OptIn(UnstableApi::class)
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
adapter = AudioContentPlaylistListAdapter { playlistId ->
@@ -107,7 +110,6 @@ class AudioContentPlaylistListFragment : BaseFragment<FragmentAudioContentPlayli
}
}
@SuppressLint("SetTextI18n")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
@@ -122,7 +124,8 @@ class AudioContentPlaylistListFragment : BaseFragment<FragmentAudioContentPlayli
}
viewModel.totalCountLiveData.observe(viewLifecycleOwner) {
binding.tvTotalCount.text = "${it}"
binding.tvTotalCount.text =
getString(R.string.audio_content_playlist_total_count, it)
}
viewModel.playlistLiveData.observe(viewLifecycleOwner) {

View File

@@ -5,8 +5,10 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class AudioContentPlaylistListViewModel(
private val repository: AudioContentPlaylistRepository
@@ -44,7 +46,8 @@ class AudioContentPlaylistListViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
@@ -52,7 +55,10 @@ class AudioContentPlaylistListViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.audio_content.playlist.create
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
@@ -11,10 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistCreateBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -39,7 +40,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
duration = item.duration ?: SodaLiveApplicationHolder.get()
.getString(R.string.screen_audio_content_upload_preview_start_time_default),
creatorNickname = item.creatorNickname,
creatorProfileUrl = ""
)
@@ -70,7 +72,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
}
override fun setupView() {
binding.tvBack.text = "새 재생목록 만들기"
binding.tvBack.text = getString(R.string.audio_content_playlist_create_title)
binding.tvSave.text = getString(R.string.audio_content_playlist_create_save)
binding.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
@@ -153,8 +156,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.length > 30) {
val truncated = it.take(30)
if (it.length > PLAYLIST_TITLE_MAX_LENGTH) {
val truncated = it.take(PLAYLIST_TITLE_MAX_LENGTH)
binding.etTitle.setText(truncated)
binding.etTitle.setSelection(truncated.length)
setTitle(truncated)
@@ -171,8 +174,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.length > 40) {
val truncated = it.take(40)
if (it.length > PLAYLIST_DESC_MAX_LENGTH) {
val truncated = it.take(PLAYLIST_DESC_MAX_LENGTH)
binding.etDesc.setText(truncated)
binding.etDesc.setSelection(truncated.length)
setDesc(truncated)
@@ -183,15 +186,26 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
)
}
@SuppressLint("SetTextI18n")
private fun setTitle(title: String) {
binding.tvTitleLength.text = "${title.length}/30"
binding.tvTitleLength.text = getString(
R.string.audio_content_playlist_create_length_format,
title.length,
PLAYLIST_TITLE_MAX_LENGTH
)
viewModel.title = title
}
@SuppressLint("SetTextI18n")
private fun setDesc(desc: String) {
binding.tvDescLength.text = "${desc.length}/40"
binding.tvDescLength.text = getString(
R.string.audio_content_playlist_create_length_format,
desc.length,
PLAYLIST_DESC_MAX_LENGTH
)
viewModel.desc = desc
}
private companion object {
private const val PLAYLIST_TITLE_MAX_LENGTH = 30
private const val PLAYLIST_DESC_MAX_LENGTH = 40
}
}

View File

@@ -4,10 +4,12 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class AudioContentPlaylistCreateViewModel(
private val repository: AudioContentPlaylistRepository
@@ -41,6 +43,8 @@ class AudioContentPlaylistCreateViewModel(
fun savePlaylist(onSuccess: () -> Unit) {
if (validate()) {
val unknownError =
SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
_isLoading.value = true
val contentIdAndOrderList = contentList.mapIndexed { index, item ->
PlaylistContentIdAndOrder(item.id, index + 1)
@@ -66,7 +70,7 @@ class AudioContentPlaylistCreateViewModel(
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownError
}
}
},
@@ -75,7 +79,7 @@ class AudioContentPlaylistCreateViewModel(
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownError
}
}
)
@@ -84,16 +88,27 @@ class AudioContentPlaylistCreateViewModel(
}
private fun validate(): Boolean {
if (title.isBlank() || title.length < 3) {
_toastLiveData.value = "제목을 3자 이상 입력하세요"
if (title.isBlank() || title.length < MIN_TITLE_LENGTH) {
_toastLiveData.value = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_playlist_create_title_min_length_error,
MIN_TITLE_LENGTH
)
return false
}
if (contentList.isEmpty()) {
_toastLiveData.value = "콘텐츠를 1개 이상 추가하세요"
_toastLiveData.value = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_playlist_create_content_min_count_error,
MIN_CONTENT_COUNT
)
return false
}
return true
}
private companion object {
private const val MIN_TITLE_LENGTH = 3
private const val MIN_CONTENT_COUNT = 1
}
}

View File

@@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
@@ -148,7 +149,7 @@ class PlaylistAddContentDialogFragment(
recyclerView.adapter = adapter
}
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show()
@@ -175,7 +176,10 @@ class PlaylistAddContentDialogFragment(
}
viewModel.totalCount.observe(viewLifecycleOwner) {
binding.tvContentCount.text = "${it}"
binding.tvContentCount.text = getString(
R.string.audio_content_playlist_total_count,
it
)
}
}
}

View File

@@ -173,8 +173,9 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
super.onCreate(savedInstanceState)
if (playlistId <= 0) {
showToast("잘못된 요청입니다.")
showToast(getString(R.string.screen_audio_content_error_invalid_request))
finish()
return
}
bindData()
@@ -283,16 +284,19 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
SodaDialog(
activity = this@AudioContentPlaylistDetailActivity,
layoutInflater = layoutInflater,
title = "재생 목록 삭제",
desc = "'${binding.tvTitle.text}'을 삭제하시겠습니까?",
confirmButtonTitle = "삭제",
title = getString(R.string.audio_content_playlist_detail_delete_title),
desc = getString(
R.string.audio_content_playlist_detail_delete_message,
binding.tvTitle.text
),
confirmButtonTitle = getString(R.string.screen_audio_content_detail_delete),
confirmButtonClick = {
viewModel.deletePlaylist(playlistId = playlistId) {
setResult(RESULT_OK)
finish()
}
},
cancelButtonTitle = "취소",
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}
@@ -319,8 +323,14 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
viewModel.detailResponseLiveData.observe(this) {
binding.tvDesc.text = it.desc
binding.tvTitle.text = it.title
binding.tvContentCount.text = " ${it.contentCount}"
binding.tvCreateDate.text = "만든 날짜 ${it.createdDate} "
binding.tvContentCount.text = getString(
R.string.audio_content_playlist_total_count,
it.contentCount
)
binding.tvCreateDate.text = getString(
R.string.audio_content_playlist_detail_created_date,
it.createdDate
)
adapter.updateItems(it.contentList)
this.contentList.clear()
this.contentList.addAll(it.contentList)

View File

@@ -7,6 +7,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentPlaylistDetailViewModel(
@@ -24,6 +26,14 @@ class AudioContentPlaylistDetailViewModel(
val detailResponseLiveData: LiveData<GetPlaylistDetailResponse>
get() = _detailResponseLiveData
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
private val deleteSuccessMessage: String
get() = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_playlist_detail_delete_success
)
fun getPlaylistDetail(playlistId: Long) {
_isLoading.value = true
compositeDisposable.add(
@@ -41,9 +51,7 @@ class AudioContentPlaylistDetailViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
}
_isLoading.value = false
@@ -51,7 +59,7 @@ class AudioContentPlaylistDetailViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)
@@ -69,15 +77,13 @@ class AudioContentPlaylistDetailViewModel(
.subscribe(
{
if (it.success) {
_toastLiveData.value = "삭제되었습니다."
_toastLiveData.value = deleteSuccessMessage
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
}
_isLoading.value = false
@@ -85,7 +91,7 @@ class AudioContentPlaylistDetailViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -13,11 +13,13 @@ import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -45,7 +47,8 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
duration = item.duration ?: SodaLiveApplicationHolder.get()
.getString(R.string.screen_audio_content_upload_preview_start_time_default),
creatorNickname = item.creatorNickname,
creatorProfileUrl = ""
)
@@ -70,8 +73,9 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
super.onCreate(savedInstanceState)
if (playlistId <= 0) {
showToast("잘못된 요청입니다.")
showToast(getString(R.string.screen_audio_content_error_invalid_request))
finish()
return
}
imm = getSystemService(
@@ -83,7 +87,6 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
}
override fun setupView() {
binding.tvBack.text = "재생목록 수정"
binding.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
@@ -207,13 +210,21 @@ class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlay
@SuppressLint("SetTextI18n")
private fun setTitle(title: String) {
binding.tvTitleLength.text = "${title.length}/30"
binding.tvTitleLength.text = getString(
R.string.audio_content_playlist_create_length_format,
title.length,
30
)
viewModel.title = title
}
@SuppressLint("SetTextI18n")
private fun setDesc(desc: String) {
binding.tvDescLength.text = "${desc.length}/40"
binding.tvDescLength.text = getString(
R.string.audio_content_playlist_create_length_format,
desc.length,
40
)
viewModel.desc = desc
}

View File

@@ -10,7 +10,9 @@ import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdA
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AudioContentPlaylistModifyViewModel(
private val repository: AudioContentPlaylistRepository
@@ -37,6 +39,9 @@ class AudioContentPlaylistModifyViewModel(
var desc: String = ""
private var playlistId: Long = 0
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun addContent(item: AudioContentPlaylistContent) {
contentList.add(item)
_contentListLiveData.value = contentList
@@ -85,7 +90,7 @@ class AudioContentPlaylistModifyViewModel(
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
}
},
@@ -94,7 +99,7 @@ class AudioContentPlaylistModifyViewModel(
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
}
)
@@ -104,12 +109,18 @@ class AudioContentPlaylistModifyViewModel(
private fun validate(): Boolean {
if (title.isBlank() || title.length < 3) {
_toastLiveData.value = "제목을 3자 이상 입력하세요"
_toastLiveData.value = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_playlist_create_title_min_length_error,
3
)
return false
}
if (contentList.isEmpty()) {
_toastLiveData.value = "콘텐츠를 1개 이상 추가하세요"
_toastLiveData.value = SodaLiveApplicationHolder.get().getString(
R.string.audio_content_playlist_create_content_min_count_error,
1
)
return false
}
@@ -136,9 +147,7 @@ class AudioContentPlaylistModifyViewModel(
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
_toastLiveData.postValue(unknownErrorMessage)
}
}
_isLoading.value = false
@@ -146,7 +155,7 @@ class AudioContentPlaylistModifyViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -38,7 +38,10 @@ class SeriesListAdapter(
}
binding.tvTitle.text = item.title
binding.tvSeriesContentCount.text = "${item.numberOfContent}"
binding.tvSeriesContentCount.text = binding.root.context.getString(
R.string.screen_home_series_episode_count,
item.numberOfContent
)
binding.tvPublishedDaysOfWeek.text = item.publishedDaysOfWeek
binding.tvNew.visibility = if (item.isNew) {

View File

@@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
@@ -57,11 +58,11 @@ class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text =
if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) {
"완결 시리즈"
getString(R.string.screen_series_main_completed_title)
} else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) {
"오직 보이스온에서만"
getString(R.string.voiceon_original_only)
} else {
"시리즈 전체보기"
getString(R.string.screen_series_main_title)
}
binding.toolbar.tvBack.setOnClickListener { finish() }

View File

@@ -6,8 +6,10 @@ import com.google.gson.annotations.SerializedName
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseViewModel() {
@@ -34,6 +36,8 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
var isLast = false
var page = 1
private val size = 20
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun getSeriesList() {
if (!_isLoading.value!! && !isLast) {
@@ -62,12 +66,7 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
_toastLiveData.value = it.message ?: unknownErrorMessage
}
_isLoading.value = false
@@ -75,7 +74,7 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -47,7 +47,7 @@ class SeriesContentAdapter(
} else if (item.isRented) {
binding.tvRented.visibility = View.VISIBLE
} else if (item.price > 0) {
binding.tvPrice.text = "${item.price}"
binding.tvPrice.text = item.price.toString()
binding.tvPrice.visibility = View.VISIBLE
} else {
binding.tvPriceFree.visibility = View.VISIBLE

View File

@@ -30,7 +30,11 @@ class SeriesContentAllActivity : BaseActivity<ActivitySeriesContentAllBinding>(
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
getString(R.string.screen_audio_content_error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}
@@ -45,9 +49,12 @@ class SeriesContentAllActivity : BaseActivity<ActivitySeriesContentAllBinding>(
val seriesTitle = intent.getStringExtra(Constants.EXTRA_SERIES_TITLE) ?: ""
binding.toolbar.tvBack.text = if (seriesTitle.isNotBlank()) {
"$seriesTitle - 전체회차 듣기"
getString(
R.string.screen_series_content_all_title_format,
seriesTitle
)
} else {
" 전체회차 듣기"
getString(R.string.screen_series_content_all_title_default)
}
binding.toolbar.tvBack.setOnClickListener { finish() }

View File

@@ -10,6 +10,8 @@ import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.R
class SeriesContentAllViewModel(private val repository: SeriesRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
@@ -33,6 +35,8 @@ class SeriesContentAllViewModel(private val repository: SeriesRepository) : Base
var page = 1
private var pageSize = 10
private var isLast = false
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun getSeriesContentList() {
if (!_isLoading.value!! && !isLast) {
@@ -59,18 +63,14 @@ class SeriesContentAllViewModel(private val repository: SeriesRepository) : Base
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
_toastLiveData.value = it.message ?: unknownErrorMessage
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -17,6 +17,7 @@ data class GetSeriesDetailResponse(
@SerializedName("writer") val writer: String?,
@SerializedName("studio") val studio: String?,
@SerializedName("publishedDate") val publishedDate: String,
@SerializedName("publishedDateUtc") val publishedDateUtc: String,
@SerializedName("creator") val creator: GetSeriesDetailCreator,
@SerializedName("rentalMinPrice") var rentalMinPrice: Int,
@SerializedName("rentalMaxPrice") var rentalMaxPrice: Int,
@@ -26,7 +27,9 @@ data class GetSeriesDetailResponse(
@SerializedName("keywordList") var keywordList: List<String>,
@SerializedName("publishedDaysOfWeek") var publishedDaysOfWeek: String,
@SerializedName("contentList") val contentList: List<GetSeriesContentListItem>,
@SerializedName("contentCount") val contentCount: Int
@SerializedName("contentCount") val contentCount: Int,
@SerializedName("translated") val translated: TranslatedSeries?,
@SerializedName("translatedGenre") val translatedGenre: String?
) : Parcelable {
@Parcelize
@Keep
@@ -38,3 +41,11 @@ data class GetSeriesDetailResponse(
@SerializedName("isNotify") var isNotify: Boolean
) : Parcelable
}
@Parcelize
@Keep
data class TranslatedSeries(
@SerializedName("title") val title: String,
@SerializedName("introduction") val introduction: String,
@SerializedName("keywords") val keywords: List<String>
) : Parcelable

View File

@@ -38,7 +38,11 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
getString(R.string.screen_audio_content_error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}
@@ -57,8 +61,14 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
private fun setupTab() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("").setTag("home"))
tabs.addTab(tabs.newTab().setText("작품소개").setTag("introduction"))
tabs.addTab(
tabs.newTab().setText(R.string.screen_series_detail_tab_home).setTag("home")
)
tabs.addTab(
tabs.newTab()
.setText(R.string.screen_series_detail_tab_introduction)
.setTag("introduction")
)
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
@@ -108,10 +118,13 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
}
viewModel.seriesDetailLiveData.observe(this) {
// GetSeriesDetailResponse에 추가한대로 번역 데이터가 있으면 번역데이터 표시
// 없으면 지금과 동일하게 원본 데이터 표시
setSeriesBg(it.coverImage)
setSeriesInfo(it)
setSeriesCreator(it.creator)
setSeriesKeywordChipList(it.keywordList)
// 번역 키워드가 있으면 번역 키워드 표시, 없으면 기존 키워드 표시
setSeriesKeywordChipList(it.translated?.keywords ?: it.keywordList)
changeFragment("home")
}
@@ -137,7 +150,7 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(applicationContext, R.font.gmarket_sans_medium)
chip.typeface = ResourcesCompat.getFont(applicationContext, R.font.medium)
binding.chipGroup.addView(chip)
}
}
@@ -150,9 +163,23 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = seriesDetail.title
binding.tvGenre.text = seriesDetail.genre
binding.tvPublishedDaysOfWeek.text = "${seriesDetail.publishedDaysOfWeek} 연재"
// 번역 데이터 사용 우선, 없으면 원본 사용
val displayTitle = seriesDetail.translated?.title?.takeIf { it.isNotBlank() }
?: seriesDetail.title
val displayGenre = seriesDetail.translatedGenre?.takeIf { it.isNotBlank() }
?: seriesDetail.genre
binding.tvTitle.text = displayTitle
binding.tvGenre.text = displayGenre
val publishedDays = if (seriesDetail.publishedDaysOfWeek == getString(R.string.day_random)) {
getString(R.string.day_random)
} else {
seriesDetail.publishedDaysOfWeek
}
binding.tvPublishedDaysOfWeek.text = getString(
R.string.screen_series_detail_published_days_format,
publishedDays
)
if (seriesDetail.isAdult) {
binding.tvAge19.visibility = View.VISIBLE

View File

@@ -5,8 +5,11 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAdapter
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllActivity
@@ -44,9 +47,12 @@ class SeriesDetailHomeFragment : BaseFragment<FragmentSeriesDetailHomeBinding>(
}
}
@SuppressLint("SetTextI18n")
@OptIn(UnstableApi::class)
private fun setContent() {
binding.tvTotalCount.text = "(${seriesDetailResponse!!.contentCount})"
binding.tvTotalCount.text = getString(
R.string.screen_series_detail_content_count_format,
seriesDetailResponse!!.contentCount
)
binding.llContentAll.setOnClickListener {
startActivity(
Intent(requireActivity(), SeriesContentAllActivity::class.java).apply {

View File

@@ -13,6 +13,9 @@ import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.FragmentSeriesDetailIntroductionBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntroductionBinding>(
FragmentSeriesDetailIntroductionBinding::inflate
@@ -38,8 +41,15 @@ class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntrod
super.onViewCreated(view, savedInstanceState)
if (seriesDetailResponse != null) {
setSeriesKeywordChipList(seriesDetailResponse!!.keywordList)
setSeriesIntroduction(seriesDetailResponse!!.introduction)
// 번역 데이터가 있으면 번역 데이터 우선 적용
setSeriesKeywordChipList(
seriesDetailResponse!!.translated?.keywords
?: seriesDetailResponse!!.keywordList
)
setSeriesIntroduction(
seriesDetailResponse!!.translated?.introduction
?: seriesDetailResponse!!.introduction
)
setSeriesPrice()
setSeriesInfo()
}
@@ -53,41 +63,63 @@ class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntrod
binding.tvRentalPrice.text = if (rentalMinPrice == rentalMaxPrice) {
if (rentalMaxPrice == 0) {
"무료(5일)"
getString(R.string.screen_series_detail_price_rental_free)
} else {
"$rentalMaxPrice(5일)"
getString(R.string.screen_series_detail_price_rental_single, rentalMaxPrice)
}
} else {
"${if (rentalMinPrice == 0) "무료" else rentalMinPrice} ~ ${rentalMaxPrice}캔 (5일)"
val minText = if (rentalMinPrice == 0) {
getString(R.string.audio_content_price_free)
} else {
rentalMinPrice.toString()
}
getString(
R.string.screen_series_detail_price_rental_range,
minText,
rentalMaxPrice
)
}
binding.tvPrice.text = if (minPrice == maxPrice) {
if (maxPrice == 0) {
"무료"
getString(R.string.audio_content_price_free)
} else {
"$maxPrice"
getString(R.string.screen_series_detail_price_keep_single, maxPrice)
}
} else {
"${if (minPrice == 0) "무료" else minPrice} ~ ${maxPrice}"
val minText = if (minPrice == 0) {
getString(R.string.audio_content_price_free)
} else {
minPrice.toString()
}
getString(R.string.screen_series_detail_price_keep_range, minText, maxPrice)
}
}
@SuppressLint("SetTextI18n")
private fun setSeriesInfo() {
binding.tvGenre.text = seriesDetailResponse!!.genre
// 장르는 번역 데이터가 있으면 번역 장르 사용
binding.tvGenre.text =
seriesDetailResponse!!.translatedGenre ?: seriesDetailResponse!!.genre
binding.tvIsAdult.text = if (seriesDetailResponse!!.isAdult) {
"19세 이상"
getString(R.string.screen_series_detail_age_19)
} else {
"전체연령가"
getString(R.string.screen_series_detail_age_all)
}
binding.tvPublishedDate.text = seriesDetailResponse!!.publishedDate
binding.tvPublishedDaysOfWeek.text =
if (seriesDetailResponse!!.publishedDaysOfWeek == "랜덤") {
seriesDetailResponse!!.publishedDaysOfWeek
// 공개일은 publishedDateUtc(UTC)를 단말 타임존으로 변환하여 표시
binding.tvPublishedDate.text =
formatPublishedDateUtc(seriesDetailResponse!!.publishedDateUtc)
val publishedDays =
if (seriesDetailResponse!!.publishedDaysOfWeek == getString(R.string.day_random)) {
getString(R.string.day_random)
} else {
seriesDetailResponse!!.publishedDaysOfWeek
}
binding.tvPublishedDaysOfWeek.text = getString(
R.string.screen_series_detail_published_days_format,
publishedDays
)
if (seriesDetailResponse!!.writer != null) {
binding.tvWriter.visibility = View.VISIBLE
@@ -110,6 +142,26 @@ class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntrod
}
}
private fun formatPublishedDateUtc(utcString: String): String {
return try {
val utcFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
val date = utcFormat.parse(utcString)
if (date != null) {
val localFormat = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).apply {
timeZone = TimeZone.getDefault()
}
localFormat.format(date)
} else {
// 파싱 실패 시 서버에서 제공한 기존 publishedDate로 폴백
seriesDetailResponse?.publishedDate ?: utcString
}
} catch (e: Exception) {
seriesDetailResponse?.publishedDate ?: utcString
}
}
private fun setSeriesIntroduction(introduction: String) {
binding.tvIntroduce.text = introduction
}
@@ -134,7 +186,7 @@ class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntrod
chip.setEnsureMinTouchTargetSize(false)
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(requireContext(), R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(requireContext(), R.font.gmarket_sans_medium)
chip.typeface = ResourcesCompat.getFont(requireContext(), R.font.medium)
binding.chipGroup.addView(chip)
}
}

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.user.UserRepository
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class SeriesDetailViewModel(
private val repository: SeriesRepository,
@@ -29,6 +31,8 @@ class SeriesDetailViewModel(
var seriesId = 0L
lateinit var seriesDetailResponse: GetSeriesDetailResponse
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun getSeriesDetail() {
_isLoading.value = true
@@ -46,18 +50,14 @@ class SeriesDetailViewModel(
seriesDetailResponse = it.data
_seriesDetailLiveData.value = seriesDetailResponse
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
_toastLiveData.value = it.message ?: unknownErrorMessage
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)
@@ -84,20 +84,14 @@ class SeriesDetailViewModel(
if (it.success && it.data != null) {
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
_toastLiveData.postValue(it.message ?: unknownErrorMessage)
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)

View File

@@ -14,16 +14,22 @@ class SeriesMainActivity : BaseActivity<ActivitySeriesMainBinding>(
private var currentTab = 0
override fun setupView() {
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.text = getString(R.string.screen_series_main_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
setupTabs()
}
private fun setupTabs() {
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(""))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("요일별"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("장르별"))
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_series_main_tab_home)
)
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_series_main_tab_day_of_week)
)
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_series_main_tab_genre)
)
// 탭 선택 리스너 설정
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {

View File

@@ -4,10 +4,12 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class SeriesMainByGenreViewModel(
private val repository: SeriesMainRepository
@@ -36,6 +38,9 @@ class SeriesMainByGenreViewModel(
val selectedGenreId: Long?
get() = _selectedGenreId
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun loadGenres() {
_isLoading.value = true
compositeDisposable.add(
@@ -59,12 +64,12 @@ class SeriesMainByGenreViewModel(
_genreListLiveData.value = emptyList()
}
} else {
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = it.message ?: unknownErrorMessage
}
},
{
_isLoading.value = false
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
)
)
@@ -108,12 +113,12 @@ class SeriesMainByGenreViewModel(
isLast = true
}
} else {
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = it.message ?: unknownErrorMessage
}
},
{
_isLoading.value = false
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
)
)

View File

@@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
@@ -61,7 +60,7 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
}
private fun setupDayOfWeekDay() {
val dayOfWeekAdapter = DayOfWeekAdapter(screenWidth = screenWidth) {
val dayOfWeekAdapter = DayOfWeekAdapter(requireContext()) {
adapter.clear()
viewModel.dayOfWeek = it
}
@@ -73,7 +72,7 @@ class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBind
false
) {
override fun canScrollVertically() = false
override fun canScrollHorizontally() = false
override fun canScrollHorizontally() = true
}
rvDayOfWeek.layoutManager = layoutManager

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
class SeriesMainDayOfWeekViewModel(
@@ -30,6 +32,9 @@ class SeriesMainDayOfWeekViewModel(
private var isLast = false
private val pageSize = 20
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
var dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM
set(newValue) {
if (field != newValue) {
@@ -67,17 +72,13 @@ class SeriesMainDayOfWeekViewModel(
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
_toastLiveData.value = it.message ?: unknownErrorMessage
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
)
)

View File

@@ -5,10 +5,12 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class SeriesMainHomeViewModel(
private val repository: SeriesMainRepository
@@ -35,6 +37,9 @@ class SeriesMainHomeViewModel(
val recommendSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _recommendSeriesLiveData
private val unknownErrorMessage: String
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
fun fetchData() {
_isLoading.value = true
@@ -52,19 +57,13 @@ class SeriesMainHomeViewModel(
_completedSeriesLiveData.value = data.completedSeriesList
_recommendSeriesLiveData.value = data.recommendSeriesList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
_toastLiveData.postValue(it.message ?: unknownErrorMessage)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(unknownErrorMessage)
}
)
)
@@ -83,17 +82,13 @@ class SeriesMainHomeViewModel(
if (it.success && it.data != null) {
_recommendSeriesLiveData.value = it.data
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
_toastLiveData.value = it.message ?: unknownErrorMessage
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = unknownErrorMessage
}
)
)

View File

@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.audio_content.upload
import android.Manifest
import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
@@ -32,6 +31,7 @@ import kr.co.vividnext.sodalive.common.ImagePickerCropper
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentUploadBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.dialog.SodaLiveTimePickerDialog
@@ -84,16 +84,17 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
getString(R.string.audio_content_upload_error_invalid_file),
Toast.LENGTH_SHORT
).show()
}
} else {
binding.tvSelectContent.text = "파일 선택"
binding.tvSelectContent.text =
getString(R.string.screen_audio_content_upload_select_file)
viewModel.contentUri = null
Toast.makeText(
this,
"파일 선택을 실패했습니다.\n다시 시도해 주세요.",
getString(R.string.audio_content_upload_error_select_file),
Toast.LENGTH_SHORT
).show()
}
@@ -177,11 +178,11 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
)
binding.tvServiceDate.text = if (SharedPreferenceManager.userId == 17958L) {
"※ 이용기간 : 대여(5일) | 소장(이용 기간 1년)"
getString(R.string.screen_audio_content_upload_service_period_one_year)
} else {
"※ 이용기간 : 대여(5일) | 소장(서비스 종료시까지)"
getString(R.string.screen_audio_content_upload_service_period_until_end)
}
binding.toolbar.tvBack.text = "콘텐츠 등록"
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_upload_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.llTheme.setOnClickListener {
if (themeFragment.isAdded) return@setOnClickListener
@@ -200,7 +201,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
selectAudioActivityResultLauncher.launch(
Intent.createChooser(
intent,
"Select Audio"
getString(R.string.audio_content_upload_select_audio_title)
)
)
}
@@ -235,11 +236,9 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
LiveDialog(
activity = this,
layoutInflater = layoutInflater,
title = "콘텐츠 업로드",
desc = "등록한 콘텐츠가 업로드 중입니다.\n" +
"콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" +
"이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.",
confirmButtonTitle = "확인",
title = getString(R.string.audio_content_upload_dialog_title),
desc = getString(R.string.audio_content_upload_dialog_desc),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = { finish() },
).show(screenWidth)
}
@@ -309,6 +308,13 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
timePicker.show()
}
viewModel.setReservationDate(
getString(R.string.screen_audio_content_upload_reservation_date_placeholder)
)
viewModel.setReservationTime(
getString(R.string.screen_audio_content_upload_reservation_time_placeholder)
)
}
private fun checkPermissions() {
@@ -332,8 +338,12 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
.check()
}
@SuppressLint("SetTextI18n")
private fun bindData() {
binding.tvNumberOfCharacters.text = getString(
R.string.screen_audio_content_upload_char_count_format,
binding.etDetail.text?.length ?: 0
)
compositeDisposable.add(
binding.etTitle.textChanges().skip(1)
.subscribeOn(Schedulers.io())
@@ -348,7 +358,10 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
binding.tvNumberOfCharacters.text = getString(
R.string.screen_audio_content_upload_char_count_format,
it.length
)
viewModel.detail = it.toString()
}
)
@@ -424,13 +437,21 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
viewModel.toastLiveData.observe(this) { toastMessage ->
toastMessage?.let {
val message = it.message ?: it.resId?.let(::getString)
message?.let { text ->
Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
}
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "콘텐츠를 업로드 하는 중입니다.")
loadingDialog.show(
screenWidth,
getString(R.string.screen_audio_content_upload_loading_message)
)
} else {
loadingDialog.dismiss()
}
@@ -647,7 +668,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private fun checkPriceFree() {
viewModel.price = 0
binding.etSetPrice.setText("0")
binding.etSetPrice.setText(SodaLiveApplicationHolder.get().getString(R.string.common_zero))
binding.llSetPrice.visibility = View.GONE
binding.llConfigPurchase.visibility = View.GONE
binding.tvTitleConfigKeep.visibility = View.GONE
@@ -876,7 +897,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private fun checkBoth() {
uncheckPurchaseOption()
binding.tvPriceTitle.text = "소장 가격"
binding.tvPriceTitle.text =
getString(R.string.screen_audio_content_upload_price_title_keep)
binding.ivBoth.visibility = View.VISIBLE
binding.tvBoth.setTextColor(
ContextCompat.getColor(
@@ -889,7 +911,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private fun checkBuyOnly() {
uncheckPurchaseOption()
binding.tvPriceTitle.text = "소장 가격"
binding.tvPriceTitle.text =
getString(R.string.screen_audio_content_upload_price_title_keep)
binding.ivBuyOnly.visibility = View.VISIBLE
binding.tvBuyOnly.setTextColor(
ContextCompat.getColor(
@@ -902,7 +925,8 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
private fun checkRentOnly() {
uncheckPurchaseOption()
binding.tvPriceTitle.text = "대여 가격"
binding.tvPriceTitle.text =
getString(R.string.screen_audio_content_upload_price_title_rent)
binding.ivRentOnly.visibility = View.VISIBLE
binding.tvRentOnly.setTextColor(
ContextCompat.getColor(

View File

@@ -11,7 +11,9 @@ import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
@@ -27,8 +29,8 @@ class AudioContentUploadViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
@@ -71,11 +73,11 @@ class AudioContentUploadViewModel(
val isActiveReservationLiveData: LiveData<Boolean>
get() = _isActiveReservationLiveData
private val _reservationDateLiveData = MutableLiveData("날짜를 선택해주세요")
private val _reservationDateLiveData = MutableLiveData("")
val reservationDateLiveData: LiveData<String>
get() = _reservationDateLiveData
private val _reservationTimeLiveData = MutableLiveData("시간을 설정해주세요")
private val _reservationTimeLiveData = MutableLiveData("")
val reservationTimeLiveData: LiveData<String>
get() = _reservationTimeLiveData
@@ -262,12 +264,16 @@ class AudioContentUploadViewModel(
}
if (coverImage == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_cover_required)
)
return
}
if (contentFile == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_content_required)
)
return
}
@@ -286,10 +292,12 @@ class AudioContentUploadViewModel(
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.postValue(
ToastMessage(message = it.message)
)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
}
@@ -299,7 +307,7 @@ class AudioContentUploadViewModel(
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
@@ -309,48 +317,64 @@ class AudioContentUploadViewModel(
private fun validateData(): Boolean {
if (title.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_title_required)
)
return false
}
if (detail.isBlank() || detail.length < 5) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_detail_min_length)
)
return false
}
if (theme == null) {
_toastLiveData.postValue("테마를 선택해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_theme_required)
)
return false
}
if (coverImageFile == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_cover_required)
)
return false
}
if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime!!.split(":")
if (startTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_preview_format)
)
return false
}
for (time in startTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_preview_format)
)
return false
}
}
val endTimeArray = previewEndTime!!.split(":")
if (endTimeArray.size != 3) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_preview_format)
)
return false
}
for (time in endTimeArray) {
if (time.length != 2) {
_toastLiveData.postValue("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_preview_format)
)
return false
}
}
@@ -359,7 +383,7 @@ class AudioContentUploadViewModel(
if (timeDifference < 15000) {
_toastLiveData.postValue(
"미리 듣기의 최소 시간은 15초 입니다."
ToastMessage(resId = R.string.audio_content_upload_error_preview_minimum)
)
return false
@@ -367,7 +391,9 @@ class AudioContentUploadViewModel(
} else {
if (previewStartTime != null || previewEndTime != null) {
_toastLiveData.postValue(
"미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다."
ToastMessage(
resId = R.string.audio_content_upload_error_preview_both_required
)
)
return false
@@ -375,12 +401,16 @@ class AudioContentUploadViewModel(
}
if (contentUri == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_content_required)
)
return false
}
if (!isPriceFreeLiveData.value!! && price < 5) {
_toastLiveData.postValue("콘텐츠의 최소금액은 5캔 입니다.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_minimum_price)
)
return false
}
@@ -388,7 +418,9 @@ class AudioContentUploadViewModel(
_isActiveReservationLiveData.value!! &&
(releaseDate.isBlank() || releaseTime.isBlank())
) {
_toastLiveData.postValue("예약날짜와 시간을 선택해주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.audio_content_upload_error_reservation_required)
)
return false
}

View File

@@ -91,7 +91,13 @@ class AudioContentThemeFragment(
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
it?.let { toastMessage ->
val message = toastMessage.message
?: toastMessage.resId?.let(::getString)
message?.let { text ->
Toast.makeText(requireActivity(), text, Toast.LENGTH_LONG).show()
}
}
}
viewModel.themeLiveData.observe(viewLifecycleOwner) {

View File

@@ -7,11 +7,13 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
class AudioContentThemeViewModel(private val repository: AudioContentRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private val _themeLiveData = MutableLiveData<List<GetAudioContentThemeResponse>>()
@@ -29,17 +31,19 @@ class AudioContentThemeViewModel(private val repository: AudioContentRepository)
_themeLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.postValue(ToastMessage(message = it.message))
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
)

View File

@@ -124,8 +124,13 @@ class AuditionFragment : BaseFragment<FragmentAuditionBinding>(
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
viewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
toastMessage?.let {
val message = it.message ?: it.resId?.let(::getString)
message?.let { text ->
Toast.makeText(requireActivity(), text, Toast.LENGTH_LONG).show()
}
}
}
viewModel.isLoading.observe(viewLifecycleOwner) {

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.audition
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@@ -47,18 +46,18 @@ class AuditionListAdapter(
inner class InProgressHeaderViewHolder(
private val binding: ItemAuditionListInProgressHeaderBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(totalCount: Int) {
binding.tvTotalCount.text = "${totalCount}"
binding.tvTotalCount.text =
binding.root.context.getString(R.string.screen_audition_total_count, totalCount)
}
}
inner class CompletedHeaderViewHolder(
private val binding: ItemAuditionListCompletedHeaderBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(totalCount: Int) {
binding.tvTotalCount.text = "${totalCount}"
binding.tvTotalCount.text =
binding.root.context.getString(R.string.screen_audition_total_count, totalCount)
}
}

View File

@@ -6,7 +6,9 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.user.UserRepository
@@ -14,8 +16,8 @@ class AuditionViewModel(
private val repository: AuditionRepository,
private val userRepository: UserRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
@@ -53,10 +55,10 @@ class AuditionViewModel(
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.postValue(ToastMessage(message = it.message))
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
}
@@ -64,7 +66,9 @@ class AuditionViewModel(
{
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
)

View File

@@ -13,6 +13,7 @@ import androidx.media3.common.util.UnstableApi
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import java.io.IOException
import kr.co.vividnext.sodalive.R
@OptIn(UnstableApi::class)
class AuditionApplicantMediaPlayerManager(
@@ -101,7 +102,11 @@ class AuditionApplicantMediaPlayerManager(
}
} catch (e: IOException) {
e.printStackTrace()
Toast.makeText(context, "콘텐츠를 재생하지 못했습니다.\n다시 시도해 주세요", Toast.LENGTH_SHORT).show()
Toast.makeText(
context,
context.getString(R.string.screen_audition_play_error),
Toast.LENGTH_SHORT
).show()
showLoadingDialog(false)
}

View File

@@ -11,6 +11,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.databinding.FragmentAuditionApplyDialogBinding
import kr.co.vividnext.sodalive.R
class AuditionApplyDialogFragment(
private val fileName: String,
@@ -59,13 +60,13 @@ class AuditionApplyDialogFragment(
if (phoneNumber.isBlank() || phoneNumber.length != 11) {
Toast.makeText(
activity,
"잘못된 연락처 입니다.\n다시 입력해 주세요.",
getString(R.string.dialog_audition_apply_error_invalid_phone),
Toast.LENGTH_LONG
).show()
} else if (!binding.tvAgree.isSelected) {
Toast.makeText(
activity,
"연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.",
getString(R.string.dialog_audition_apply_error_need_agreement),
Toast.LENGTH_LONG
).show()
} else {

View File

@@ -7,8 +7,6 @@ import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.resource.bitmap.CenterCrop
@@ -19,6 +17,7 @@ import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.databinding.ActivityAuditionDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -40,7 +39,7 @@ class AuditionDetailActivity : BaseActivity<ActivityAuditionDetailBinding>(
if (auditionId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
getString(R.string.screen_audition_error_invalid_request),
Toast.LENGTH_LONG
).show()
@@ -60,7 +59,7 @@ class AuditionDetailActivity : BaseActivity<ActivityAuditionDetailBinding>(
isOpenInformation = !isOpenInformation
if (isOpenInformation) {
binding.tvInformation.maxLines = Int.MAX_VALUE
binding.tvOpen.text = "접기"
binding.tvOpen.text = getString(R.string.screen_audition_detail_collapse)
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_top,
0,
@@ -69,7 +68,7 @@ class AuditionDetailActivity : BaseActivity<ActivityAuditionDetailBinding>(
)
} else {
binding.tvInformation.maxLines = 3
binding.tvOpen.text = "펼치기"
binding.tvOpen.text = getString(R.string.screen_audition_detail_expand)
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_bottom,
0,
@@ -125,8 +124,13 @@ class AuditionDetailActivity : BaseActivity<ActivityAuditionDetailBinding>(
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
viewModel.toastLiveData.observe(this) { toastMessage ->
toastMessage?.let {
val message = it.message ?: it.resId?.let(::getString)
message?.let { text ->
Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
}
}
}
viewModel.isLoading.observe(this) {

View File

@@ -60,11 +60,11 @@ class AuditionDetailRoleAdapter(
if (item.isComplete) {
binding.blackCover.visibility = View.VISIBLE
binding.tvStatus.text = "모집완료"
binding.tvStatus.text = context.getString(R.string.screen_audition_status_closed)
binding.tvStatus.setBackgroundResource(R.drawable.bg_round_corner_13_3_909090)
} else {
binding.blackCover.visibility = View.GONE
binding.tvStatus.text = "모집중"
binding.tvStatus.text = context.getString(R.string.screen_audition_status_open)
binding.tvStatus.setBackgroundResource(R.drawable.bg_round_corner_13_3_3bb9f1)
}

View File

@@ -7,13 +7,15 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
class AuditionDetailViewModel(
private val repository: AuditionRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
@@ -39,10 +41,10 @@ class AuditionDetailViewModel(
_auditionDetailLiveData.value = it.data
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.postValue(ToastMessage(message = it.message))
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
@@ -56,7 +58,9 @@ class AuditionDetailViewModel(
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
if (onFailure != null) {
onFailure()
}

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.audition.role
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
@@ -28,6 +29,7 @@ import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.databinding.ActivityAuditionRoleDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.RecordingVoiceFragment
@@ -66,14 +68,14 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
} else {
Toast.makeText(
this,
"잘못된 녹음 파일 입니다.\n다시 선택해 주세요.",
getString(R.string.screen_audition_invalid_audio_file),
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText(
this,
"잘못된 녹음 파일 입니다.\n다시 선택해 주세요.",
getString(R.string.screen_audition_invalid_audio_file),
Toast.LENGTH_SHORT
).show()
}
@@ -87,7 +89,7 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
if (auditionRoleId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
getString(R.string.screen_audition_error_invalid_request),
Toast.LENGTH_LONG
).show()
@@ -137,7 +139,7 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
isOpenInformation = !isOpenInformation
if (isOpenInformation) {
binding.tvInformation.maxLines = Int.MAX_VALUE
binding.tvOpen.text = "접기"
binding.tvOpen.text = getString(R.string.screen_audition_detail_collapse)
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_top,
0,
@@ -146,7 +148,7 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
)
} else {
binding.tvInformation.maxLines = 3
binding.tvOpen.text = "펼치기"
binding.tvOpen.text = getString(R.string.screen_audition_detail_expand)
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_bottom,
0,
@@ -162,9 +164,9 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
SodaDialog(
activity = this@AuditionRoleDetailActivity,
layoutInflater = layoutInflater,
title = "재지원 안내",
desc = "재지원 시 이전 지원 내역은 삭제되며 받은 투표수는 무효 처리됩니다.",
confirmButtonTitle = "확인",
title = getString(R.string.dialog_audition_reapply_title),
desc = getString(R.string.dialog_audition_reapply_desc),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = { showApplicationMethodDialog() }
).show(screenWidth)
} else {
@@ -174,9 +176,9 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
SodaDialog(
activity = this@AuditionRoleDetailActivity,
layoutInflater = layoutInflater,
title = "- 본인인증 -",
desc = "마이페이지에서 '본인인증'을 하고 다시 오디션에 지원해 주세요.",
confirmButtonTitle = "확인",
title = getString(R.string.dialog_audition_auth_title),
desc = getString(R.string.dialog_audition_auth_desc),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = {}
).show(screenWidth)
}
@@ -211,23 +213,23 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
SodaDialog(
activity = this@AuditionRoleDetailActivity,
layoutInflater = layoutInflater,
"[오디션 응원]",
"오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요.",
confirmButtonTitle = "확인",
getString(R.string.dialog_audition_vote_title),
getString(R.string.dialog_audition_vote_desc),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = {
isShowNotifyVote = false
},
descGravity = Gravity.CENTER
).show(screenWidth)
descGravity = Gravity.CENTER
).show(screenWidth)
}
},
onFailure = {
SodaDialog(
activity = this@AuditionRoleDetailActivity,
layoutInflater = layoutInflater,
"[오늘 응원 제한]",
"오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요.",
confirmButtonTitle = "확인",
getString(R.string.dialog_audition_vote_limit_title),
getString(R.string.dialog_audition_vote_limit_desc),
confirmButtonTitle = getString(R.string.confirm),
confirmButtonClick = {},
descGravity = Gravity.CENTER
).show(screenWidth)
@@ -307,7 +309,7 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
selectAudioActivityResultLauncher.launch(
Intent.createChooser(
intent,
"Select Audio"
getString(R.string.screen_audition_select_audio)
)
)
},
@@ -339,9 +341,15 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
auditionApplyDialogFragment.show(supportFragmentManager, auditionApplyDialogFragment.tag)
}
@SuppressLint("SetTextI18n")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
it?.let { toastMessage ->
val message = toastMessage.message ?: toastMessage.resId?.let(::getString)
message?.let { text ->
Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
}
}
}
viewModel.isLoading.observe(this) {
@@ -381,10 +389,10 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
if (roleDetail.isAlreadyApplicant) {
reApplication = true
binding.tvApplicant.text = "오디션 재지원"
binding.tvApplicant.text = getString(R.string.screen_audition_apply_again)
} else {
reApplication = false
binding.tvApplicant.text = "오디션 지원"
binding.tvApplicant.text = getString(R.string.screen_audition_apply)
}
}
@@ -394,7 +402,7 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
if (it > 0) {
binding.groupApplicant.visibility = View.VISIBLE
binding.tvApplicantCount.text = "$it"
binding.tvApplicantCount.text = " $it"
} else {
binding.groupNoApplicant.visibility = View.VISIBLE
}

View File

@@ -13,7 +13,9 @@ import kr.co.vividnext.sodalive.audition.applicant.ApplyAuditionRoleRequest
import kr.co.vividnext.sodalive.audition.applicant.GetAuditionRoleApplicantItem
import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.R
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
@@ -24,8 +26,8 @@ import java.io.File
import java.util.TimeZone
class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private val _isLoading = MutableLiveData(false)
@@ -90,9 +92,10 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
_auditionRoleDetailLiveData.value = roleDetailResponse.data!!
} else {
if (roleDetailResponse.message != null) {
_toastLiveData.value = roleDetailResponse.message
_toastLiveData.value = ToastMessage(message = roleDetailResponse.message)
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value =
ToastMessage(resId = R.string.common_error_unknown)
}
if (onFailure != null) {
@@ -113,9 +116,10 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
}
} else {
if (applicantListResponse.message != null) {
_toastLiveData.value = applicantListResponse.message
_toastLiveData.value = ToastMessage(message = applicantListResponse.message)
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value =
ToastMessage(resId = R.string.common_error_unknown)
}
if (onFailure != null) {
@@ -128,7 +132,9 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
if (onFailure != null) {
onFailure()
}
@@ -166,9 +172,10 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
_toastLiveData.value = ToastMessage(message = it.message)
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value =
ToastMessage(resId = R.string.common_error_unknown)
}
}
@@ -177,7 +184,9 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
)
@@ -186,7 +195,8 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
fun applyAudition(auditionRoleId: Long, phoneNumber: String, onSuccess: () -> Unit) {
if (audioFile == null) {
_toastLiveData.value = "잘못된 녹음 파일 입니다.\n다시 선택해 주세요."
_toastLiveData.value =
ToastMessage(resId = R.string.screen_audition_invalid_audio_file)
return
}
@@ -236,10 +246,10 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
_toastLiveData.postValue(ToastMessage(message = it.message))
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
}
@@ -248,7 +258,7 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
@@ -304,11 +314,11 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
) {
onFailure()
} else {
_toastLiveData.value = it.message
_toastLiveData.value = ToastMessage(message = it.message)
}
} else {
_toastLiveData.value =
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
ToastMessage(resId = R.string.common_error_unknown)
}
}
@@ -317,7 +327,8 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value =
ToastMessage(resId = R.string.common_error_unknown)
}
)
)

View File

@@ -17,6 +17,9 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.viewbinding.ViewBinding
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
import kotlin.math.max
abstract class BaseActivity<T : ViewBinding>(
@@ -43,6 +46,12 @@ abstract class BaseActivity<T : ViewBinding>(
}
}
override fun attachBaseContext(newBase: Context) {
// 앱 설정 언어가 있으면 해당 Locale을 적용한 Context로 래핑한다.
val wrapped = LocaleHelper.wrap(newBase)
super.attachBaseContext(wrapped)
}
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -103,7 +112,8 @@ abstract class BaseActivity<T : ViewBinding>(
layoutInflater = layoutInflater,
title = "포인트 지급",
desc = message,
confirmButtonTitle = "확인",
confirmButtonTitle = SodaLiveApplicationHolder.get()
.getString(R.string.screen_live_tag_confirm),
confirmButtonClick = {}
).show(screenWidth)
}

View File

@@ -52,9 +52,15 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
private fun setupTabs() {
// 탭 추가
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("캐릭터"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("작품별"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(""))
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_chat_tab_character)
)
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_chat_tab_original)
)
binding.tabLayout.addTab(
binding.tabLayout.newTab().setText(R.string.screen_chat_tab_talk)
)
// 탭 선택 리스너 설정
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {

View File

@@ -6,12 +6,12 @@ import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterImageList
import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterImagePurchaseRequest
import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterImagePurchaseResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Body
import retrofit2.http.POST
interface CharacterApi {
@GET("/api/chat/character/main")

View File

@@ -379,12 +379,11 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
SodaDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "본인인증",
desc = "보이스온의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n" +
"캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.",
confirmButtonTitle = "본인인증 하러가기",
title = getString(R.string.auth_title),
desc = getString(R.string.auth_desc),
confirmButtonTitle = getString(R.string.auth_go),
confirmButtonClick = { startAuthFlow() },
cancelButtonTitle = "취소",
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {},
descGravity = Gravity.CENTER
).show(screenWidth)
@@ -430,7 +429,10 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
val text = it?.message ?: it?.resId?.let { resId -> getString(resId) }
if (!text.isNullOrBlank()) {
Toast.makeText(requireActivity(), text, Toast.LENGTH_LONG).show()
}
}
}

View File

@@ -1,9 +1,7 @@
package kr.co.vividnext.sodalive.chat.character
class CharacterTabRepository(private val api: CharacterApi) {
fun getCharacterMain(
token: String
) = api.getCharacterMain(authHeader = token)
fun getCharacterMain(token: String) = api.getCharacterMain(authHeader = token)
fun refreshRecommendCharacters(
token: String

View File

@@ -5,9 +5,11 @@ import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
class CharacterTabViewModel(
private val repository: CharacterTabRepository
@@ -16,8 +18,8 @@ class CharacterTabViewModel(
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<ToastMessage?>()
val toastLiveData: LiveData<ToastMessage?>
get() = _toastLiveData
private var _bannerListLiveData = MutableLiveData<List<CharacterBannerResponse>>()
@@ -48,7 +50,9 @@ class CharacterTabViewModel(
_isLoading.value = true
compositeDisposable.add(
repository.getCharacterMain(token = "Bearer ${SharedPreferenceManager.token}")
repository.getCharacterMain(
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@@ -61,15 +65,18 @@ class CharacterTabViewModel(
_newCharacters.value = data.newCharacters
_recommendCharacters.value = data.recommendCharacters
} else {
_toastLiveData.value =
it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = it.message?.let { message ->
ToastMessage(message = message)
} ?: ToastMessage(resId = R.string.common_error_unknown)
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
)
@@ -78,7 +85,9 @@ class CharacterTabViewModel(
fun refreshRecommendCharacters() {
_isLoading.value = true
compositeDisposable.add(
repository.refreshRecommendCharacters(token = "Bearer ${SharedPreferenceManager.token}")
repository.refreshRecommendCharacters(
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@@ -86,15 +95,18 @@ class CharacterTabViewModel(
if (response.success && response.data != null) {
_recommendCharacters.value = response.data
} else {
_toastLiveData.value = response.message
?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.value = response.message?.let { message ->
ToastMessage(message = message)
} ?: ToastMessage(resId = R.string.common_error_unknown)
}
_isLoading.value = false
},
{ throwable ->
_isLoading.value = false
throwable.message?.let { msg -> Logger.e(msg) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(
ToastMessage(resId = R.string.common_error_unknown)
)
}
)
)

View File

@@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.UiText
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@@ -173,8 +174,7 @@ class CharacterCommentListFragment : BaseFragment<FragmentCharacterCommentListBi
}
viewModel.toastLiveData.observe(viewLifecycleOwner) { msg ->
msg?.let { showToast(it) }
msg?.let { showToast(it.asString(requireContext())) }
}
viewModel.totalCommentCount.observe(viewLifecycleOwner) { count ->

View File

@@ -6,14 +6,16 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UiText
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class CharacterCommentListViewModel(
private val repository: CharacterCommentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
private val _toastLiveData = MutableLiveData<UiText?>()
val toastLiveData: LiveData<UiText?>
get() = _toastLiveData
private val _isLoading = MutableLiveData(false)
@@ -80,14 +82,16 @@ class CharacterCommentListViewModel(
// 응답 아이템 전달 (비어있어도 전달) — UI는 addAll 처리
_commentList.postValue(items)
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
onFailure?.invoke()
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character comments load failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
onFailure?.invoke()
})
)
@@ -95,7 +99,7 @@ class CharacterCommentListViewModel(
fun createComment(characterId: Long, comment: String) {
if (comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
_toastLiveData.postValue(UiText.StringResource(R.string.character_comment_error_empty))
return
}
if (_isLoading.value == true) return
@@ -119,13 +123,15 @@ class CharacterCommentListViewModel(
cursor = null
getCommentList(characterId)
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character comment create failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}
@@ -151,20 +157,24 @@ class CharacterCommentListViewModel(
cursor = null
getCommentList(characterId)
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character comment delete failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}
fun reportComment(characterId: Long, commentId: Long, reason: String) {
if (reason.isBlank()) {
_toastLiveData.postValue("신고 사유를 입력하세요")
_toastLiveData.postValue(
UiText.StringResource(R.string.character_comment_error_report_reason)
)
return
}
if (_isLoading.value == true) return
@@ -182,15 +192,19 @@ class CharacterCommentListViewModel(
.subscribe({ resp ->
_isLoading.value = false
if (resp.success) {
_toastLiveData.postValue("신고가 접수되었습니다.")
_toastLiveData.postValue(
UiText.StringResource(R.string.character_comment_report_submitted)
)
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character comment report failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}

View File

@@ -60,6 +60,7 @@ class CharacterReplyHeaderVH(
) : CharacterReplyVH(binding) {
override fun bind(item: Any) {
val data = item as CharacterCommentResponse
val context = binding.root.context
if (data.memberProfileImage.isNotBlank()) {
binding.ivCommentProfile.load(data.memberProfileImage) {
crossfade(true)
@@ -71,7 +72,7 @@ class CharacterReplyHeaderVH(
binding.ivCommentProfile.setImageResource(R.drawable.ic_placeholder_profile)
}
binding.tvCommentNickname.text = data.memberNickname
binding.tvCommentDate.text = timeAgo(data.createdAt)
binding.tvCommentDate.text = formatCommentTime(context, data.createdAt)
binding.tvComment.text = data.comment
binding.tvWriteReply.visibility = View.GONE
binding.ivMenu.visibility = View.GONE
@@ -86,6 +87,7 @@ class CharacterReplyItemVH(
override fun bind(item: Any) {
val data = item as CharacterReplyResponse
val context = binding.root.context
if (data.memberProfileImage.isNotBlank()) {
binding.ivCommentProfile.load(data.memberProfileImage) {
crossfade(true)
@@ -98,7 +100,7 @@ class CharacterReplyItemVH(
}
binding.tvCommentNickname.text = data.memberNickname
binding.tvCommentDate.text = timeAgo(data.createdAt)
binding.tvCommentDate.text = formatCommentTime(context, data.createdAt)
binding.tvComment.text = data.comment
val isOwner = data.memberId == currentUserId
@@ -109,17 +111,3 @@ class CharacterReplyItemVH(
}
}
}
private fun timeAgo(createdAtMillis: Long): String {
val now = System.currentTimeMillis()
val diff = (now - createdAtMillis).coerceAtLeast(0)
val minutes = diff / 60_000
if (minutes < 1) return "방금전"
if (minutes < 60) return "${minutes}분전"
val hours = minutes / 60
if (hours < 24) return "${hours}시간전"
val days = hours / 24
if (days < 365) return "${days}일전"
val years = days / 365
return "${years}년전"
}

View File

@@ -15,6 +15,7 @@ import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.UiText
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@@ -179,7 +180,7 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
if (loading) loadingDialog.show(screenWidth) else loadingDialog.dismiss()
}
viewModel.toastLiveData.observe(viewLifecycleOwner) { msg ->
msg?.let { showToast(it) }
msg?.let { showToast(it.asString(requireContext())) }
}
viewModel.replies.observe(viewLifecycleOwner) { list ->
// 헤더(원본 댓글)는 index 0에 유지, 나머지를 교체

View File

@@ -6,14 +6,16 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UiText
class CharacterCommentReplyViewModel(
private val repository: CharacterCommentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?> get() = _toastLiveData
private val _toastLiveData = MutableLiveData<UiText?>()
val toastLiveData: LiveData<UiText?> get() = _toastLiveData
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> get() = _isLoading
@@ -65,20 +67,22 @@ class CharacterCommentReplyViewModel(
cursor = resp.data.cursor
page += 1
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character replies load failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}
fun createReply(characterId: Long, comment: String) {
if (comment.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
_toastLiveData.postValue(UiText.StringResource(R.string.character_comment_error_empty))
return
}
val originalId = _original.value?.commentId ?: return
@@ -110,13 +114,15 @@ class CharacterCommentReplyViewModel(
val current = _replies.value ?: emptyList()
_replies.postValue(current + listOf(me))
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character reply create failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}
@@ -139,20 +145,24 @@ class CharacterCommentReplyViewModel(
val current = _replies.value ?: emptyList()
_replies.postValue(current.filterNot { it.replyId == replyId })
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character reply delete failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}
fun reportReply(characterId: Long, replyId: Long, reason: String) {
if (reason.isBlank()) {
_toastLiveData.postValue("신고 사유를 입력하세요")
_toastLiveData.postValue(
UiText.StringResource(R.string.character_comment_error_report_reason)
)
return
}
if (_isLoading.value == true) return
@@ -170,15 +180,19 @@ class CharacterCommentReplyViewModel(
.subscribe({ resp ->
_isLoading.value = false
if (resp.success) {
_toastLiveData.postValue("신고가 접수되었습니다.")
_toastLiveData.postValue(
UiText.StringResource(R.string.character_comment_report_submitted)
)
} else {
val message = resp.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
val message = resp.message?.takeIf { it.isNotBlank() }
?.let { UiText.DynamicString(it) }
?: UiText.StringResource(R.string.common_error_unknown)
_toastLiveData.postValue(message)
}
}, { e ->
_isLoading.value = false
Logger.e(e, "Character reply report failed")
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(UiText.StringResource(R.string.common_error_unknown))
})
)
}

View File

@@ -27,12 +27,14 @@ class CharacterCommentReportBottomSheet : BottomSheetDialogFragment() {
var onSubmit: ((String) -> Unit)? = null
private var reasons: ArrayList<String>? = null
private var reasons: ArrayList<String> = ArrayList()
private var selectedIndex: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
reasons = arguments?.getStringArrayList(ARG_REASONS) ?: DEFAULT_REASONS
val defaultReasons = resources.getStringArray(R.array.character_comment_report_reasons)
.toCollection(ArrayList())
reasons = arguments?.getStringArrayList(ARG_REASONS) ?: defaultReasons
}
override fun onCreateView(
@@ -49,7 +51,7 @@ class CharacterCommentReportBottomSheet : BottomSheetDialogFragment() {
tvTitle.text = getString(R.string.report_title)
setReportEnabled(btnReport, false)
val items = reasons ?: DEFAULT_REASONS
val items = reasons
val textColor = ContextCompat.getColor(requireContext(), R.color.white)
// RadioButton 동적 생성 및 단일 선택 처리
@@ -64,8 +66,9 @@ class CharacterCommentReportBottomSheet : BottomSheetDialogFragment() {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
// 폰트: pretendard_regular
try {
typeface = ResourcesCompat.getFont(context, R.font.pretendard_regular)
} catch (_: Exception) { /* 폰트 미존재 대비 안전 처리 */ }
typeface = ResourcesCompat.getFont(context, R.font.regular)
} catch (_: Exception) { /* 폰트 미존재 대비 안전 처리 */
}
// 항목 간 간격: 기존 paddingVertical 12dp의 1.3배 -> 15.6dp
val vPadPx = (14f * resources.displayMetrics.density).toInt()
setPadding(paddingLeft, vPadPx, paddingRight, vPadPx)
@@ -106,16 +109,6 @@ class CharacterCommentReportBottomSheet : BottomSheetDialogFragment() {
companion object {
private const val ARG_REASONS = "arg_reasons"
private val DEFAULT_REASONS = arrayListOf(
"원치 않는 상업성 콘텐츠 또는 스팸",
"아동 학대",
"증오시 표현 또는 노골적인 폭력",
"테러 조장",
"희롱 또는 괴롭힘",
"자살 또는 자해",
"잘못된 정보"
)
fun newInstance(reasons: ArrayList<String>? = null): CharacterCommentReportBottomSheet {
return CharacterCommentReportBottomSheet().apply {
arguments = bundleOf(ARG_REASONS to reasons)

View File

@@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.content.Context
import kr.co.vividnext.sodalive.R
fun formatCommentTime(context: Context, createdAtMillis: Long): String {
val now = System.currentTimeMillis()
val diff = (now - createdAtMillis).coerceAtLeast(0)
val minutes = diff / 60_000
return when {
minutes < 1 -> context.getString(R.string.character_comment_time_just_now)
minutes < 60 -> context.getString(R.string.character_comment_time_minutes, minutes)
minutes < 1_440 -> {
val hours = minutes / 60
context.getString(R.string.character_comment_time_hours, hours)
}
minutes < 525_600 -> {
val days = minutes / 1_440
context.getString(R.string.character_comment_time_days, days)
}
else -> {
val years = minutes / 525_600
context.getString(R.string.character_comment_time_years, years)
}
}
}

Some files were not shown because too many files have changed in this diff Show More