Compare commits

198 Commits

Author SHA1 Message Date
7c44f3d66c docs(i18n): 일본어 리소스 PRD 완료 기준을 갱신한다 2026-08-11 19:01:50 +09:00
43b6496289 chore(app): 앱 버전을 1.56.1로 올린다 2026-08-11 18:53:30 +09:00
30a4a9ffc7 feat(i18n): 일본어 리소스를 반영한다 2026-08-11 17:58:56 +09:00
84cd9bbf41 docs(i18n): 일본어 리소스 반영 계획을 추가한다 2026-08-11 17:57:50 +09:00
75480e7a71 docs(creator): 커뮤니티 후속 작업 기록을 갱신한다 2026-08-11 00:11:11 +09:00
8b8b384b29 feat(creator): 커뮤니티 구매와 메뉴 동작을 정리한다 2026-08-11 00:10:46 +09:00
02cd583f56 chore(app): 데이터스토어 버전을 올린다 2026-08-04 14:13:01 +09:00
3c21a764ae fix(home): 라이브 전체 버튼을 오른쪽에 고정한다 2026-08-04 11:45:29 +09:00
d20d6aa971 docs(agent): 테스트 범위를 로직으로 제한한다 2026-08-04 11:44:58 +09:00
f50f987282 chore(app): 앱 버전 코드를 올린다 2026-08-03 23:04:16 +09:00
f508d847ea feat(creator): 내 채널 팔로워 목록을 연결한다 2026-08-03 21:39:23 +09:00
e86e5a1c3d chore(app): 앱 버전을 1.56.0으로 올린다 2026-08-03 18:21:42 +09:00
26eae18edc fix(content): 콘텐츠 상세 펼침 말줄임을 복원한다 2026-08-03 16:33:35 +09:00
b5cfdc83e3 fix(live): 전체 라이브 제목을 On Air로 통일한다 2026-08-03 15:07:31 +09:00
dd4a5167a9 fix(home): 라이브 전체 항목을 항상 표시한다 2026-07-31 18:34:23 +09:00
d592038edd fix(home): 홈 추천 후속 검증을 반영한다 2026-07-31 16:29:06 +09:00
a106548f87 fix(content): 콘텐츠 고정 한도 안내를 수정한다 2026-07-31 03:48:35 +09:00
49f6fe592c fix(home): 팔로잉 탭 후속 검증을 반영한다 2026-07-31 03:22:50 +09:00
f600d10e8b feat(home): 최근 활동 LIVE 라우팅을 추가한다 2026-07-30 16:42:33 +09:00
a1aa9c2243 docs(docs): 문서 샘플과 리뷰 규칙을 갱신한다 2026-07-30 14:18:07 +09:00
3a62ed8463 chore(git): worktree 디렉터리를 제외한다 2026-07-22 18:11:14 +09:00
6755c8ae99 feat(main): 메인 탭 당겨서 새로고침을 추가한다 2026-07-22 17:48:33 +09:00
b17e250665 docs(main): 당겨서 새로고침 계획을 추가한다 2026-07-22 00:35:34 +09:00
074c945008 chore(app): 앱 버전을 1.55.1로 올린다 2026-07-20 15:41:48 +09:00
16aa946621 feat(main): 홈 크리에이터 FAB를 추가한다 2026-07-17 00:08:33 +09:00
831533b4f6 refactor(creator): 진행 중 라이브 컴포넌트를 공통화한다 2026-07-16 22:59:07 +09:00
de90f771c1 refactor(v2): 도메인 액션 경로를 통합한다 2026-07-16 16:57:39 +09:00
4373452478 docs(v2): 라이브 액션 검증 기록을 갱신한다 2026-07-15 21:27:53 +09:00
0b3042d29e refactor(onair): 라이브 입장 흐름을 공통화한다 2026-07-15 21:27:44 +09:00
cc23992d2e refactor(home): 추천 라이브 진입을 공통화한다 2026-07-15 21:27:37 +09:00
3a49158243 refactor(creator): 라이브 액션을 공통화한다 2026-07-15 21:27:31 +09:00
d1509d4359 refactor(live): 공통 액션 코디네이터를 추가한다 2026-07-15 21:27:23 +09:00
c223b3b8a4 feat(live): 생성 결과 계약을 추가한다 2026-07-15 21:26:53 +09:00
b582545991 feat(live): 입장 정책 모델을 추가한다 2026-07-15 21:26:37 +09:00
fe79431744 refactor(content): 상세 진입을 공통 액션으로 통합한다 2026-07-14 22:11:10 +09:00
1c5fbb38f1 style(content): 주석 오디오 너비 계산 줄바꿈을 정리한다 2026-07-14 20:48:53 +09:00
dadf3a519d test(home): 팔로잉 empty 문구 검증을 정리한다 2026-07-14 20:45:34 +09:00
ddc949f9d9 refactor(live): 온에어 접근 가드를 공통화한다 2026-07-14 20:45:29 +09:00
6973816b8c refactor(creator): 크리에이터 접근 가드를 공통화한다 2026-07-14 20:45:23 +09:00
e5b99be9c4 refactor(main): 기존 로그인 가드를 제거한다 2026-07-14 20:45:16 +09:00
5ff92980a2 refactor(chat): 채팅 접근 가드를 공통화한다 2026-07-14 20:44:58 +09:00
2fda80d602 refactor(content): 콘텐츠 접근 가드를 공통화한다 2026-07-14 20:44:53 +09:00
7e12aa8720 refactor(home): 홈 접근 가드를 공통화한다 2026-07-14 20:44:42 +09:00
d5d3444c3a refactor(main): 메인 접근 가드를 공통화한다 2026-07-14 20:44:36 +09:00
a6601dcea1 feat(access): 공통 접근 실행기를 추가한다 2026-07-14 20:44:20 +09:00
4d51806cee feat(access): 접근 정책 모델을 추가한다 2026-07-14 20:44:13 +09:00
98db0b657e docs(v2): 공통 접근 가드 계획을 보강한다 2026-07-14 20:44:06 +09:00
9f70bcd3eb docs(v2): 공통 접근 가드 계획을 기록한다 2026-07-14 12:22:58 +09:00
cc8d061b51 docs(content): 상세 오디오 광고 제거 검증을 기록한다 2026-07-13 16:59:20 +09:00
1b952054fb fix(content): 상세 오디오 광고를 제거한다 2026-07-13 16:59:10 +09:00
2c609836fd fix(home): 팔로잉 empty 문구를 중앙 표시한다 2026-07-13 16:17:12 +09:00
da36964347 fix(home): 팔로잉 empty 문구 번역을 갱신한다 2026-07-13 16:17:06 +09:00
47b9c3dec2 docs(home): 팔로잉 empty 문구 계획을 기록한다 2026-07-13 16:17:01 +09:00
17a9a2f601 fix(creator): 채널 진입과 채팅 인증을 보강한다 2026-07-13 14:09:06 +09:00
c43b9cdbd8 docs(creator): 채널 로그인 가드 계획을 기록한다 2026-07-13 14:09:01 +09:00
7ec4e09fb4 versionCode 242 2026-07-13 12:00:54 +09:00
862cec8375 fix(dm): 광고 로드 실패 안내를 보강한다 2026-07-12 22:09:00 +09:00
2d4956ae25 fix(dm): 쿼터 안내 광고 버튼을 숨긴다 2026-07-12 22:08:41 +09:00
beadc7d4c4 chore(app): 버전 코드를 갱신한다 2026-07-12 20:52:07 +09:00
2633912e93 test(dm): 채팅 플로팅 버튼 제거 검증을 맞춘다 2026-07-12 20:52:01 +09:00
6cbc751f0c fix(community): 채널 이미지 원본비율 표시를 반영한다 2026-07-12 20:51:54 +09:00
2a499eaf41 fix(home): 커뮤니티 이미지 로드 크기를 전달한다 2026-07-12 20:51:36 +09:00
9244a00713 fix(feed): 커뮤니티 이미지 원본비율 높이를 적용한다 2026-07-12 20:51:30 +09:00
79eadbd39d docs(community): 이미지 원본비율 요구를 기록한다 2026-07-12 20:51:22 +09:00
c389d1945d fix(dm): 채팅 플로팅 버튼을 숨긴다 2026-07-12 17:45:19 +09:00
2d8f8cab81 fix(community): 상세 잠금 반응과 댓글을 숨긴다 2026-07-12 09:54:26 +09:00
f799449d54 docs(community): 상세 잠금 반응 숨김 계획을 기록한다 2026-07-12 09:54:18 +09:00
9da6c7381d fix(community): 리스트 잠금 반응 표시를 숨긴다 2026-07-12 09:54:10 +09:00
ced226c3ef docs(community): 리스트 잠금 반응 숨김 계획을 기록한다 2026-07-12 09:54:01 +09:00
f69a9a6ee0 docs(community): 그리드 잠금 오버레이 요구를 기록한다 2026-07-12 09:28:05 +09:00
94d2773d1e fix(community): 그리드 잠금 오버레이를 보정한다 2026-07-12 09:27:59 +09:00
bc5fd46671 fix(content): 페이월 가격 배지를 보정한다 2026-07-12 08:46:59 +09:00
a00745dd38 docs(content): 상세 오디오 구매 검증을 기록한다 2026-07-12 08:33:02 +09:00
01908c84d3 docs(content): 상세 오디오 구매 요구를 기록한다 2026-07-12 08:32:55 +09:00
39959c3f47 fix(content): 상세 오디오와 구매 흐름을 보정한다 2026-07-12 08:32:48 +09:00
53cc96b84d fix(home): 커뮤니티 하트 아이콘을 PNG로 교체한다 2026-07-12 07:46:40 +09:00
1aeb336898 fix(home): 추천 섹션 chevron을 숨긴다 2026-07-12 07:32:49 +09:00
1715c591a5 docs(home): 섹션 chevron 요구를 기록한다 2026-07-12 07:32:38 +09:00
eb41506bf4 fix(content): 상세 댓글 border를 반영한다 2026-07-12 06:29:50 +09:00
9fdcd49c98 docs(content): 상세 border 요구를 기록한다 2026-07-12 06:29:43 +09:00
8538f014aa fix(content): 댓글 섹션 제목을 변경한다 2026-07-12 05:39:04 +09:00
848cd03068 docs(home): 장르 크리에이터 미표시 검증을 기록한다 2026-07-12 05:29:44 +09:00
4debf4654d fix(home): 장르 크리에이터 섹션을 숨긴다 2026-07-12 05:29:37 +09:00
6ed2be4173 docs(home): 장르 크리에이터 미표시 요구를 기록한다 2026-07-12 05:29:26 +09:00
557de11bb3 fix(home): 최근 소식 상세 이동을 연결한다 2026-07-12 04:24:47 +09:00
97d917e35d docs(home): 최근 소식 상세 이동 요구를 기록한다 2026-07-12 04:24:42 +09:00
9b2666e671 fix(home): 팔로잉 라이브 카드 레이아웃을 보정한다 2026-07-12 03:47:55 +09:00
c883556dbf chore(gradle): Java toolchain resolver를 추가한다 2026-07-12 03:42:22 +09:00
5bcd0f64d2 fix(content): 전체 탭 pagination 스크롤을 안정화한다 2026-07-12 01:24:19 +09:00
d53167c48e docs(content): 전체 탭 pagination 안정화 요구를 기록한다 2026-07-12 01:24:02 +09:00
c2b212c1ce fix(splash): 스플래시 태그라인 문구를 변경한다 2026-07-11 23:55:41 +09:00
d1510b8ba3 fix(splash): 스플래시 화면 구성을 단순화한다 2026-07-11 23:55:27 +09:00
273feef017 chore(dm): WebSocket raw JSON 로그를 주입한다 2026-07-11 22:03:32 +09:00
11a42bf8a1 feat(dm): 메시지 시간을 표시한다 2026-07-11 22:03:24 +09:00
d436efdc66 fix(dm): 채팅방 입력과 프로필 표시를 보정한다 2026-07-11 22:03:16 +09:00
30f7bd7556 fix(dm): 실시간 연결 상태 처리를 보강한다 2026-07-11 22:03:03 +09:00
62007862a5 feat(dm): 음성 메시지 API 계약을 추가한다 2026-07-11 22:02:51 +09:00
2786e45c75 feat(dm): WebSocket envelope 계약을 보정한다 2026-07-11 22:02:40 +09:00
46c3a02655 refactor(dm): legacy realtime 구현을 제거한다 2026-07-11 22:02:29 +09:00
5ca07ef049 docs(dm): WebSocket 전환 요구를 보강한다 2026-07-11 22:02:20 +09:00
c715c890b5 feat(chat): 탭별 empty state를 표시한다 2026-07-11 22:02:13 +09:00
9925a954d1 feat(chat): empty state 문구 리소스를 추가한다 2026-07-11 22:02:04 +09:00
0c111aaf47 docs(chat): empty state 요구를 기록한다 2026-07-11 22:01:56 +09:00
2a2315ecbc docs(home): 커뮤니티 상세 이동 요구를 기록한다 2026-07-10 10:38:59 +09:00
17f77351ad fix(home): 커뮤니티 상세 이동을 연결한다 2026-07-10 10:38:42 +09:00
abd084cff8 fix(content-ranking): 카드 UI 크기를 반응형 적용한다 2026-07-10 10:16:36 +09:00
e77d0a1d5b fix(content-ranking): 텍스트 크기 기준을 추가한다 2026-07-10 10:16:27 +09:00
991d70f22b fix(creator-ranking): 카드 UI 크기를 반응형 적용한다 2026-07-10 10:16:18 +09:00
f477b563a2 fix(creator-ranking): 텍스트 크기 기준을 확장한다 2026-07-10 10:16:09 +09:00
63a7c28e96 feat(ranking): 반응형 폭 스케일을 추가한다 2026-07-10 10:16:01 +09:00
2b2cb66dfc docs(ranking): 반응형 크기 요구를 기록한다 2026-07-10 10:15:54 +09:00
bf5cc1d7bb fix(content): 전체 탭 sort-bar 표시를 정리한다 2026-07-10 07:35:46 +09:00
bbf94b8a0d feat(creator-channel): 정렬 팝업 옵션을 주입한다 2026-07-10 07:35:39 +09:00
951d7cf561 fix(content): 전체 탭 정렬 값을 보정한다 2026-07-10 07:35:31 +09:00
40337c9590 docs(content): 전체 탭 후속 요구를 기록한다 2026-07-10 07:35:19 +09:00
79a6ecdc0b test(json): 난독화 안정성 검증을 추가한다 2026-07-09 17:39:07 +09:00
33f42e62b6 fix(content): 콘텐츠 개요 query 값을 고정한다 2026-07-09 17:39:01 +09:00
1b660fe542 fix(content): 전체 탭 query 값을 고정한다 2026-07-09 17:38:55 +09:00
d0539b00b0 fix(content): 오디오 랭킹 query 값을 고정한다 2026-07-09 17:38:46 +09:00
e089ef6f8a fix(creator-channel): 콘텐츠 정렬 query 값을 고정한다 2026-07-09 17:38:39 +09:00
a8a70df304 fix(series): 요일 query 값을 고정한다 2026-07-09 17:38:34 +09:00
a207a8f840 fix(home): 홈 랭킹 query 값을 고정한다 2026-07-09 17:38:28 +09:00
6c85f81f98 fix(json): DTO 직렬화 이름을 고정한다 2026-07-09 17:38:18 +09:00
6a88019851 docs(json): 난독화 안정화 요구를 기록한다 2026-07-09 17:38:11 +09:00
379263ec87 fix(content): 추천 오디오 카드 폭을 보정한다 2026-07-09 14:56:19 +09:00
f3d610633c docs(content): 추천 오디오 폭 보정 요구를 기록한다 2026-07-09 14:56:06 +09:00
335db22680 docs(content): 댓글 많은 오디오 UI 보정 기록을 추가한다 2026-07-09 08:46:35 +09:00
b71bb43dd6 fix(content): 최근 댓글 오디오 카드 UI를 보정한다 2026-07-09 08:46:21 +09:00
dace1416c5 fix(content): 추천 섹션 순서를 조정한다 2026-07-09 08:28:23 +09:00
1baf3ac7e0 fix(creator-channel): FanTalk 문구를 한 줄로 표시한다 2026-07-09 08:19:04 +09:00
f3a4578a66 feat(creator-channel): 홈 섹션 이동을 연결한다 2026-07-09 08:19:00 +09:00
b745c3d675 docs(creator-channel): 홈 섹션 이동 요구를 기록한다 2026-07-09 08:18:54 +09:00
f993d343a9 feat(creator-channel): FanTalk 상세 진입점을 연결한다 2026-07-09 07:44:21 +09:00
1ba7846f4c feat(creator-channel): FanTalk 목록 상세 클릭을 추가한다 2026-07-09 07:44:15 +09:00
da26b65a73 feat(creator-channel): FanTalk 상세 화면을 추가한다 2026-07-09 07:44:06 +09:00
04dcb3f09f feat(creator-channel): FanTalk 상세 상태를 구현한다 2026-07-09 07:43:59 +09:00
6916459c9e feat(creator-channel): FanTalk 상세 모델을 추가한다 2026-07-09 07:43:53 +09:00
643fb9fa8e feat(creator-channel): FanTalk 답글 API 응답을 연결한다 2026-07-09 07:43:47 +09:00
f9160ccb02 docs(creator-channel): FanTalk 상세 작업 계획을 기록한다 2026-07-09 07:43:40 +09:00
e2b166de11 docs(creator-channel): FanTalk 상세 요구사항을 기록한다 2026-07-09 07:43:31 +09:00
89f3511864 test(creator-channel): 댓글 답글 후속 계약을 검증한다 2026-07-09 01:28:48 +09:00
3271c9e142 feat(creator-channel): 더보기 메뉴 폰트를 고정한다 2026-07-09 01:28:42 +09:00
1f1069b05d feat(creator-channel): 답글 수정 입력 모드를 구현한다 2026-07-09 01:28:35 +09:00
a325a1bbe9 feat(creator-channel): 댓글 수정 입력 모드를 구현한다 2026-07-09 01:28:29 +09:00
255e6dc67d feat(creator-channel): 더보기 메뉴 문구를 정리한다 2026-07-09 01:28:24 +09:00
b2685959ad docs(creator-channel): 댓글 답글 후속 요구를 기록한다 2026-07-09 01:28:18 +09:00
536effab40 test(creator-channel): 커뮤니티 상세 UI 계약을 검증한다 2026-07-09 00:54:12 +09:00
81537b1e96 feat(creator-channel): 커뮤니티 목록 클릭을 연결한다 2026-07-09 00:54:06 +09:00
e691f78e2e feat(creator-channel): 커뮤니티 상세 진입점을 연결한다 2026-07-09 00:53:59 +09:00
6125b9f7d1 feat(creator-channel): 커뮤니티 답글 화면을 구현한다 2026-07-09 00:53:48 +09:00
d10373d265 feat(creator-channel): 커뮤니티 상세 화면을 구현한다 2026-07-09 00:53:41 +09:00
5c1eb6204a feat(creator-channel): 커뮤니티 전송 아이콘을 추가한다 2026-07-09 00:53:35 +09:00
c3884ea917 feat(creator-channel): 커뮤니티 답글 화면 리소스를 추가한다 2026-07-09 00:53:30 +09:00
bc3fa9546b feat(creator-channel): 커뮤니티 상세 화면 리소스를 추가한다 2026-07-09 00:53:24 +09:00
ea66b6da1e fix(common): UTC 상대시간 파싱을 보완한다 2026-07-09 00:53:17 +09:00
c9e47214ea feat(creator-channel): 커뮤니티 답글 상태 관리를 추가한다 2026-07-09 00:53:12 +09:00
c04d603ac0 feat(creator-channel): 커뮤니티 상세 상태 관리를 추가한다 2026-07-09 00:53:06 +09:00
9e524d626a feat(creator-channel): 커뮤니티 상세 조회 계약을 추가한다 2026-07-09 00:52:59 +09:00
e0dc0c2cb7 docs(creator-channel): 커뮤니티 상세 요구사항을 기록한다 2026-07-09 00:52:49 +09:00
cbd4f04f1b docs(creator-channel): 후원 카드 공통화 작업을 기록한다 2026-07-08 18:44:15 +09:00
eeb777fa01 test(donation): 후원 header 색상 경계를 검증한다 2026-07-08 18:44:09 +09:00
9272db913c test(creator-channel): 후원 카드 바인더 계약을 검증한다 2026-07-08 18:43:56 +09:00
ff4c0104c5 refactor(creator-channel): 후원 카드 바인딩을 공통화한다 2026-07-08 18:43:47 +09:00
31ce0629d2 docs(creator-channel): 후원 header 색상 검증을 기록한다 2026-07-08 16:33:01 +09:00
6ab7878221 fix(creator-channel): 홈 후원 고액 header 색상을 보정한다 2026-07-08 16:32:54 +09:00
ddc18386fb fix(donation): 후원 내역 고액 header 색상을 보정한다 2026-07-08 16:32:46 +09:00
809ed5ce23 refactor(modal): V2 확인 모달 사용처를 교체한다 2026-07-08 16:04:01 +09:00
8e086c4888 feat(modal): V2 공통 모달창을 추가한다 2026-07-08 14:20:27 +09:00
bc6443e5bd docs(modal): V2 공통 모달창 계획을 기록한다 2026-07-08 14:20:20 +09:00
8d205690b3 feat(creator-channel): FanTalk 글쓰기 진입점을 연결한다 2026-07-08 03:44:54 +09:00
25af312d91 feat(creator-channel): FanTalk 글 입력 화면을 연결한다 2026-07-08 03:44:44 +09:00
b020b8096b chore(creator-channel): FanTalk 글 입력 다국어 문구를 추가한다 2026-07-08 03:44:36 +09:00
87f6bec138 feat(creator-channel): FanTalk 글 입력 기본 리소스를 추가한다 2026-07-08 03:44:31 +09:00
34266abd90 feat(creator-channel): FanTalk 글 입력 상태 관리를 추가한다 2026-07-08 03:44:22 +09:00
e8e8a63d6d docs(creator-channel): FanTalk 글 입력 계획을 기록한다 2026-07-08 03:44:15 +09:00
d61867af4e docs(creator-channel): FanTalk 글 입력 PRD를 기록한다 2026-07-08 03:44:09 +09:00
daeb0f75a2 fix(home): 영문 요일 약어 표기를 보완한다 2026-07-06 14:19:07 +09:00
6c497ed1d2 docs(main): 오디오 미니 플레이어 작업을 기록한다 2026-07-06 14:00:48 +09:00
7b975ec54d fix(audio): 오디오 알림 route extra를 전달한다 2026-07-06 14:00:34 +09:00
e3d81792bf fix(main): 오디오 알림 이동과 미니 플레이어를 연결한다 2026-07-06 14:00:23 +09:00
5b9b7eafa7 docs(home): 메인 홈 추천 탭 지금 라이브 중 입장 작업을 기록한다 2026-07-06 02:35:04 +09:00
5489c86cdc fix(home): 메인 홈 추천 탭 지금 라이브 중 입장을 연결한다 2026-07-06 02:34:41 +09:00
b1f605fda4 docs(home): On Air 라이브 아이템 작업을 기록한다 2026-07-03 14:47:44 +09:00
f9030ffd13 fix(home): On Air 라이브 아이템 UI를 보완한다 2026-07-03 14:47:27 +09:00
f74f5a534d docs(home): 팔로잉 최근 소식 구현 검증을 기록한다 2026-06-30 23:54:21 +09:00
3f3bacebd9 fix(home): 팔로잉 최근 소식 nested payload를 반영한다 2026-06-30 23:54:10 +09:00
2041036530 test(home): 팔로잉 최근 소식 nested payload 검증을 추가한다 2026-06-30 23:53:58 +09:00
81a2b541a0 docs(home): 팔로잉 최근 소식 응답 변경을 정의한다 2026-06-30 23:53:47 +09:00
e6cb94179b docs(home): 팔로잉 크리에이터 전체 버튼을 기록한다 2026-06-30 17:43:29 +09:00
658b4f0a9d fix(home): 팔로잉 크리에이터 전체 이동을 연결한다 2026-06-30 17:43:14 +09:00
05cbbdfddc docs(main): title bar 추가 범위를 기록한다 2026-06-30 16:12:52 +09:00
3c66ea986f fix(main): 콘텐츠와 대화 title bar 이동을 연결한다 2026-06-30 16:12:16 +09:00
d1a0cf27a7 fix(home): title bar 메뉴 이동을 연결한다 2026-06-30 15:46:55 +09:00
1aef4b1d89 docs(home): title bar 메뉴 액션 계획을 기록한다 2026-06-30 15:46:36 +09:00
c745481ab2 docs(home): 팔로잉 최근 소식 검증을 기록한다 2026-06-30 15:04:26 +09:00
7e281a15a5 fix(home): 팔로잉 최근 소식 표시를 보완한다 2026-06-30 15:04:14 +09:00
c5e94bbc92 feat(feed): 콘텐츠 카테고리 타입을 보완한다 2026-06-30 15:04:04 +09:00
61fe8a9fee fix(feed): 콘텐츠 카드 레이아웃을 보완한다 2026-06-30 15:03:50 +09:00
498 changed files with 37550 additions and 4475 deletions

1
.gitignore vendored
View File

@@ -319,5 +319,6 @@ app/release/
.kiro/
.omo/
.antigravitycli/
.worktrees/
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java

View File

@@ -114,12 +114,22 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
- PRD 문서는 `sample-prd.md` 파일에서 작업에 필요한 부분만 발췌해 작성한다. `sample-prd.md`가 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
- 문서 샘플의 기준 위치는 `docs/sample/`다.
- PRD 문서는 `docs/sample/sample-prd.md`, 구현 계획/TASK 문서는 `docs/sample/sample-plan-task.md`를 참조해 작업에 필요한 항목을 구체화한다.
- 샘플의 `<...>` placeholder와 예시 값은 실제 작업 값으로 교체한다. 필요한 샘플이 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
- 후속 수정이 발생해도 기존 검증 기록은 삭제하거나 덮어쓰지 않고 위치별로 누적한다.
- 자동 테스트는 기본적으로 `app/src/test`의 로직/local unit test만 작성하고 실행한다.
- 사용자가 명시적으로 요청하지 않으면 `app/src/androidTest` UI/계측 테스트를 작성하거나 `connectedDebugAndroidTest`, 기기·에뮬레이터 UI 조작, 스크린샷·시각 QA를 실행하지 않는다.
- UI 변경의 테스트 코드는 adapter 분기, mapper, formatter, presentation model, 라우팅처럼 화면 표현을 결정하는 로직만 검증하며 View 크기·간격·constraint·visibility 등 레이아웃 속성을 직접 검증하지 않는다.
- 코드 리뷰, 코드 품질 점검, 완료 Phase 재검토를 수행하면 `docs/sample/sample-review.md`를 참조해 `docs/[날짜]_구현할내용한글/reviews/[리뷰범위]-review.md`에 결과를 기록한다.
- 리뷰 보고서명의 `[리뷰범위]``phase<번호>-<구현 내용을 나타내는 영문 kebab-case>` 형식으로 작성한다. 예: `phase2-main-home-recommendation-review.md`
- 일반 빌드, 테스트, 린트 실행 결과는 리뷰 보고서를 만들지 않고 계획/TASK 문서의 Task별 검증 기록 또는 `Verification Log`에 남긴다.
- 리뷰에서 수정 항목이 확정되면 코드를 수정하기 전에 review ID, 대상 파일, 수정 범위, 회귀 테스트, 완료 증거를 포함한 신규 회귀 수정 Task를 `plan-task.md`의 해당 Phase에 추가하고 그 Task에 따라 바로 수정한다.
- 리뷰 후속 수정 시 기존 완료 Task를 다시 열지 않으며, 기존 완료 체크박스와 검증·리뷰 기록은 삭제하거나 덮어쓰지 않고 위치별로 누적한다.
- 샘플의 위치, 문서 구조, 작성·운용 규칙이 변경되면 같은 작업에서 `AGENTS.md`와 관련 가이드 문서를 함께 갱신한다.
## 상세 참조 문서
- 빌드/린트/테스트는 `docs/agent-guides/build-test-style.md`를 참고한다.

View File

@@ -86,8 +86,8 @@ android {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 35
versionCode 239
versionName "1.55.0"
versionCode 246
versionName "1.56.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -116,8 +116,6 @@ android {
// release용 ad unit id는 배포 전 실제 값으로 교체한다.
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140295-3"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140295-4"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140295-5"'
buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140295-6"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140295-7"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140295-8"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140295-9"'
@@ -160,8 +158,6 @@ android {
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140297-3"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140297-4"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140297-5"'
buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140297-6"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140297-7"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140297-8"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140297-9"'
@@ -271,7 +267,7 @@ dependencies {
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
}
implementation "androidx.datastore:datastore-preferences:1.2.0"
implementation "androidx.datastore:datastore-preferences:1.2.1"
// Gson
implementation "com.google.code.gson:gson:2.13.2"

View File

@@ -114,6 +114,18 @@
<activity android:name=".v2.main.MainV2Activity" />
<activity android:name=".v2.main.content.overview.ContentOverviewActivity" />
<activity android:name=".v2.creator.channel.CreatorChannelActivity" />
<activity
android:name=".v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity
android:name=".v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity
android:name=".v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity
android:name=".v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity android:name=".v2.live.onair.HomeOnAirLiveActivity" />
<activity
android:name=".v2.main.chat.dm.DmChatRoomActivity"

View File

@@ -361,17 +361,20 @@ class Agora(
fun deInitRtmClient(rtmEventListener: RtmEventListener) {
rtmClient?.removeEventListener(rtmEventListener)
rtmClient?.unsubscribe(roomChannelName, object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - $roomChannelName")
roomChannelName = null
}
rtmClient?.unsubscribe(
roomChannelName,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - $roomChannelName")
roomChannelName = null
}
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
}
}
})
)
rtmClient?.unsubscribe(
"inbox_${SharedPreferenceManager.userId}",
object : ResultCallback<Void> {
@@ -383,7 +386,8 @@ class Agora(
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
}
})
}
)
rtmClient?.logout(object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM logout")
@@ -398,7 +402,6 @@ class Agora(
// 상태 리셋
rtmLoggedIn = false
rtmLoginInProgress = false
}
// endregion
}

View File

@@ -136,7 +136,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
applicationContext,
BuildConfig.NOTIFLY_PROJECT_ID,
BuildConfig.NOTIFLY_USERNAME,
BuildConfig.NOTIFLY_PASSWORD,
BuildConfig.NOTIFLY_PASSWORD
)
}

View File

@@ -14,5 +14,5 @@ data class AddAllPlaybackTrackingRequest(
data class PlaybackTrackingData(
@SerializedName("contentId") val contentId: Long,
@SerializedName("playDateTime") val playDateTime: String,
@SerializedName("isPreview") val isPreview: Boolean,
@SerializedName("isPreview") val isPreview: Boolean
)

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
package kr.co.vividnext.sodalive.audio_content
import android.app.NotificationChannel
@@ -471,9 +473,17 @@ class AudioContentPlayService :
}
private fun updateNotification() {
val intent = Intent(this, MainV2Activity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val intent = Intent(this, MainV2Activity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
MainV2Activity.ROUTE_AUDIO_DETAIL
)
contentId?.let {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
}
val pendingIntent = PendingIntent.getActivity(
this,
0,

View File

@@ -127,7 +127,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
compositeDisposable.add(
repository.getCategoryList(
creatorId = userId,
token = "Bearer ${SharedPreferenceManager.token}",
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View File

@@ -10,5 +10,5 @@ enum class PurchaseOption {
BUY_ONLY,
@SerializedName("RENT_ONLY")
RENT_ONLY,
RENT_ONLY
}

View File

@@ -21,7 +21,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentNewAllAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
inner class ViewHolder(

View File

@@ -35,7 +35,7 @@ class AudioContentCommentReplyAdapter(
LayoutInflater.from(parent.context),
parent,
false
),
)
)
} else {
AudioContentCommentReplyItemViewHolder(
@@ -172,9 +172,7 @@ class AudioContentCommentReplyItemViewHolder(
private val context: Context,
private val creatorId: Long,
private val binding: ItemAudioContentCommentReplyBinding,
private val showOptionMenu: (
Context, View, Long, Long, Long, onClickModify: () -> Unit
) -> Unit,
private val showOptionMenu: (Context, View, Long, Long, Long, onClickModify: () -> Unit) -> Unit,
private val modifyComment: (Long, String) -> Unit
) : AudioContentCommentReplyViewHolder(binding) {

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
@@ -10,6 +12,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
@@ -28,18 +31,6 @@ import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.gson.Gson
import com.orhanobut.logger.Logger
import com.yandex.mobile.ads.banner.BannerAdSize
import com.yandex.mobile.ads.common.AdError
import com.yandex.mobile.ads.common.AdRequest
import com.yandex.mobile.ads.common.AdRequestConfiguration
import com.yandex.mobile.ads.common.AdRequestError
import com.yandex.mobile.ads.common.ImpressionData
import com.yandex.mobile.ads.interstitial.InterstitialAd
import com.yandex.mobile.ads.interstitial.InterstitialAdEventListener
import com.yandex.mobile.ads.interstitial.InterstitialAdLoadListener
import com.yandex.mobile.ads.interstitial.InterstitialAdLoader
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
@@ -72,7 +63,6 @@ import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import org.koin.android.ext.android.inject
import kotlin.math.ceil
import kotlin.math.roundToInt
@UnstableApi
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
@@ -101,42 +91,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private lateinit var orderType: OrderType
private lateinit var imm: InputMethodManager
private var audioContentPlayInterstitialAdLoader: InterstitialAdLoader? = null
private var audioContentPlayInterstitialAd: InterstitialAd? = null
private var pendingAudioContentPlayAction: (() -> Unit)? = null
private var hasConsumedAudioContentPlayInterstitialAttempt = false
private var isAudioContentPlaying = false
private var isAudioContentInterstitialEligible = false
private var audioContentStartPlaybackAction: (() -> Unit)? = null
private val audioContentPlayInterstitialAdLoadListener = object : InterstitialAdLoadListener {
override fun onAdLoaded(interstitialAd: InterstitialAd) {
clearAudioContentPlayInterstitialAd()
audioContentPlayInterstitialAd = interstitialAd
}
override fun onAdFailedToLoad(error: AdRequestError) {
Logger.e("Audio content interstitial failed to load: ${error.description}")
}
}
private val audioContentPlayInterstitialAdEventListener = object : InterstitialAdEventListener {
override fun onAdShown() = Unit
override fun onAdFailedToShow(adError: AdError) {
Logger.e("Audio content interstitial failed to show: ${adError.description}")
continuePendingAudioContentPlayAction()
}
override fun onAdDismissed() {
continuePendingAudioContentPlayAction()
}
override fun onAdClicked() = Unit
override fun onAdImpression(impressionData: ImpressionData?) = Unit
}
@SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@@ -154,9 +111,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.rlPreviewAlert.visibility = View.GONE
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
hasConsumedAudioContentPlayInterstitialAttempt = false
releaseAudioContentPlayInterstitial()
setupAudioContentPlayInterstitial()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
@@ -367,91 +321,9 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
}
setupAudioContentDetailInlineBanner()
setupAudioContentPlayInterstitial()
setupBuyerList()
}
private fun setupAudioContentDetailInlineBanner() {
binding.yandexInlineBannerView.post {
val density = resources.displayMetrics.density
val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth
val adWidthDp = (adWidthPixels / density).roundToInt()
val maxAdHeightDp = 90
binding.yandexInlineBannerView.apply {
setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID)
setAdSize(
BannerAdSize.inlineSize(
this@AudioContentDetailActivity,
adWidthDp,
maxAdHeightDp
)
)
loadAd(AdRequest.Builder().build())
}
}
}
private fun setupAudioContentPlayInterstitial() {
val adUnitId = BuildConfig.YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID
if (adUnitId.isBlank()) {
Logger.e("Audio content interstitial blocked: ad unit id is blank.")
return
}
audioContentPlayInterstitialAdLoader = InterstitialAdLoader(this).apply {
setAdLoadListener(audioContentPlayInterstitialAdLoadListener)
}
audioContentPlayInterstitialAdLoader?.loadAd(
AdRequestConfiguration.Builder(adUnitId).build()
)
}
private fun playAudioContentWithInterstitialIfAvailable(playAction: () -> Unit) {
if (hasConsumedAudioContentPlayInterstitialAttempt || isFinishing || isDestroyed) {
playAction()
return
}
val interstitialAd = audioContentPlayInterstitialAd ?: run {
playAction()
return
}
hasConsumedAudioContentPlayInterstitialAttempt = true
pendingAudioContentPlayAction = playAction
interstitialAd.setAdEventListener(audioContentPlayInterstitialAdEventListener)
runCatching {
interstitialAd.show(this)
}.onFailure {
Logger.e("Audio content interstitial failed to show: ${it.message}")
continuePendingAudioContentPlayAction()
}
}
private fun continuePendingAudioContentPlayAction() {
val playAction = pendingAudioContentPlayAction
pendingAudioContentPlayAction = null
clearAudioContentPlayInterstitialAd()
if (isFinishing || isDestroyed) {
return
}
playAction?.invoke()
}
private fun clearAudioContentPlayInterstitialAd() {
audioContentPlayInterstitialAd?.setAdEventListener(null)
audioContentPlayInterstitialAd = null
}
private fun releaseAudioContentPlayInterstitial() {
audioContentPlayInterstitialAdLoader?.setAdLoadListener(null)
audioContentPlayInterstitialAdLoader = null
pendingAudioContentPlayAction = null
clearAudioContentPlayInterstitialAd()
}
private fun pauseAudioContentPlayback() {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
@@ -474,13 +346,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
val startPlaybackClickListener = View.OnClickListener {
if (isAudioContentInterstitialEligible) {
playAudioContentWithInterstitialIfAvailable(startPlaybackAction)
} else {
startPlaybackAction()
}
}
val startPlaybackClickListener = View.OnClickListener { startPlaybackAction() }
binding.ivPlayOrPause.setOnClickListener(startPlaybackClickListener)
binding.llPreview.setOnClickListener(startPlaybackClickListener)
@@ -600,6 +466,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
viewModel.isExpandDetail.observe(this) {
binding.tvDetail.ellipsize = if (it) {
null
} else {
TextUtils.TruncateAt.END
}
binding.tvDetail.maxLines = if (it) {
Int.MAX_VALUE
} else {
@@ -944,12 +815,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
response.duration
)
audioContentStartPlaybackAction = null
isAudioContentInterstitialEligible = false
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
!response.existOrdered &&
response.price > 0
isAudioContentInterstitialEligible = response.price <= 0 || isAlertPreview
if (
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
@@ -1046,7 +915,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
} else if (response.releaseDate == null) {
audioContentStartPlaybackAction = null
isAudioContentInterstitialEligible = false
binding.llPreviewNo.visibility = View.VISIBLE
}
@@ -1366,8 +1234,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
override fun onDestroy() {
binding.yandexInlineBannerView.destroy()
releaseAudioContentPlayInterstitial()
super.onDestroy()
}

View File

@@ -51,7 +51,7 @@ data class GetAudioContentDetailResponse(
data class OtherContentResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverUrl") val coverUrl: String,
@SerializedName("coverUrl") val coverUrl: String
)
@Keep

View File

@@ -25,10 +25,15 @@ class AudioContentMainNewContentThemeAdapter(
fun bind(theme: String) {
if (
theme == selectedTheme ||
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
.getString(R.string.audio_content_label_all)) ||
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
.getString(R.string.screen_home_sort_revenue))
(
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

@@ -76,7 +76,8 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
excludeGif = true,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
aspectX = 1f,
aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),

View File

@@ -28,7 +28,7 @@ class AudioContentOrderConfirmDialog(
orderType: OrderType,
price: Int,
isAvailableUsePoint: Boolean,
confirmButtonClick: () -> Unit,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog

View File

@@ -1,20 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
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.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentOrderListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderListBinding>(
ActivityAudioContentOrderListBinding::inflate

View File

@@ -20,5 +20,5 @@ data class GetAudioContentOrderListItem(
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("orderType") val orderType: OrderType,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("commentCount") val commentCount: Int
)

View File

@@ -429,7 +429,6 @@ class AudioContentPlayerFragment(
mediaController?.seekTo(progress.toLong())
}
}
})
updateMediaMetadata(mediaController?.currentMediaItem?.mediaMetadata)

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
package kr.co.vividnext.sodalive.audio_content.player
import android.app.PendingIntent
@@ -155,6 +157,10 @@ class AudioContentPlayerService : MediaSessionService() {
private fun initMediaSession() {
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra(
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
MainV2Activity.ROUTE_AUDIO_PLAYER
)
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,

View File

@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.playlist.modify
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdAndOrder
@Keep
data class UpdatePlaylistRequest(
@SerializedName("title") val title: String? = null,
@SerializedName("desc") val desc: String? = null,

View File

@@ -14,8 +14,11 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseViewModel() {
enum class SeriesSortType {
@SerializedName("NEWEST") NEWEST,
@SerializedName("OLDEST") OLDEST
@SerializedName("NEWEST")
NEWEST,
@SerializedName("OLDEST")
OLDEST
}
private val _toastLiveData = MutableLiveData<String?>()

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle

View File

@@ -5,7 +5,6 @@ import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
@@ -23,7 +22,7 @@ interface SeriesMainApi {
@GET("/audio-content/series/main/day-of-week")
fun getDayOfWeekSeriesList(
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
@Query("dayOfWeek") dayOfWeek: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String

View File

@@ -19,7 +19,7 @@ class SeriesMainRepository(
size: Int,
token: String
) = api.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
dayOfWeek = dayOfWeek.queryValue,
page = page - 1,
size = size,
authHeader = token

View File

@@ -240,7 +240,7 @@ class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBindin
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() },
confirmButtonClick = { finish() }
).show(screenWidth)
}
}

View File

@@ -20,5 +20,5 @@ data class CreateAudioContentRequest(
@SerializedName("isPointAvailable") val isPointAvailable: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("previewStartTime") val previewStartTime: String? = null,
@SerializedName("previewEndTime") val previewEndTime: String? = null,
@SerializedName("previewEndTime") val previewEndTime: String? = null
)

View File

@@ -80,13 +80,7 @@ class AuditionApplicantListAdapter(
binding.sbProgress.max = currentTotalDuration
binding.sbProgress.progress = currentTime
binding.tvTotalDuration.text =
"/${
Utils.convertDurationToString(
currentTotalDuration,
showHours = false
)
}"
binding.tvTotalDuration.text = "/${Utils.convertDurationToString(currentTotalDuration, showHours = false)}"
binding.tvCurrentTime.text = Utils.convertDurationToString(
currentTime,
showHours = false

View File

@@ -17,7 +17,6 @@ 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

View File

@@ -43,7 +43,8 @@ class CharacterAdapter(
translationY = fm.descent
return true
}
})
}
)
}
} else {
binding.tvRanking.visibility = View.GONE

View File

@@ -17,7 +17,6 @@ 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
@@ -151,8 +150,7 @@ class CharacterCommentListFragment : BaseFragment<FragmentCharacterCommentListBi
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisible = (recyclerView.layoutManager as LinearLayoutManager)
.findLastCompletelyVisibleItemPosition()
val lastVisible = (recyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition()
val total = recyclerView.adapter?.itemCount ?: 0
if (!recyclerView.canScrollVertically(1) && lastVisible == total - 1) {
viewModel.getCommentList(characterId)
@@ -195,7 +193,6 @@ class CharacterCommentListFragment : BaseFragment<FragmentCharacterCommentListBi
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
companion object {
private const val EXTRA_CHARACTER_ID = "extra_character_id"
fun newInstance(characterId: Long): CharacterCommentListFragment {

View File

@@ -15,7 +15,6 @@ 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
@@ -48,15 +47,19 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
characterId = arguments?.getLong(EXTRA_CHARACTER_ID) ?: 0
original = arguments?.let {
val cid = it.getLong(EXTRA_ORIGINAL_COMMENT_ID, -1)
if (cid == -1L) null else CharacterCommentResponse(
commentId = cid,
memberId = it.getLong(EXTRA_ORIGINAL_MEMBER_ID),
memberProfileImage = it.getString(EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE) ?: "",
memberNickname = it.getString(EXTRA_ORIGINAL_MEMBER_NICKNAME) ?: "",
createdAt = it.getLong(EXTRA_ORIGINAL_CREATED_AT),
replyCount = it.getInt(EXTRA_ORIGINAL_REPLY_COUNT),
comment = it.getString(EXTRA_ORIGINAL_COMMENT_TEXT) ?: ""
)
if (cid == -1L) {
null
} else {
CharacterCommentResponse(
commentId = cid,
memberId = it.getLong(EXTRA_ORIGINAL_MEMBER_ID),
memberProfileImage = it.getString(EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE) ?: "",
memberNickname = it.getString(EXTRA_ORIGINAL_MEMBER_NICKNAME) ?: "",
createdAt = it.getLong(EXTRA_ORIGINAL_CREATED_AT),
replyCount = it.getInt(EXTRA_ORIGINAL_REPLY_COUNT),
comment = it.getString(EXTRA_ORIGINAL_COMMENT_TEXT) ?: ""
)
}
}
return super.onCreateView(inflater, container, savedInstanceState)
}
@@ -140,7 +143,7 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
outRect.right = 24f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 24f.dpToPx().toInt();
outRect.top = 24f.dpToPx().toInt()
outRect.bottom = 12f.dpToPx().toInt()
}
@@ -196,7 +199,6 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
}
}
companion object {
private const val EXTRA_CHARACTER_ID = "extra_character_id"
private const val EXTRA_ORIGINAL_COMMENT_ID = "extra_original_comment_id"

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View File

@@ -434,5 +434,4 @@ class CharacterDetailFragment : BaseFragment<FragmentCharacterDetailBinding>(
binding.tvPersonalityContent.maxLines = 3
binding.tvPersonalityContent.ellipsize = TextUtils.TruncateAt.END
}
}

View File

@@ -29,6 +29,7 @@ data class CharacterDetailResponse(
enum class CharacterType {
@SerializedName("Clone")
CLONE,
@SerializedName("Character")
CHARACTER
}

View File

@@ -49,7 +49,6 @@ class OtherCharacterAdapter(
transformations(RoundedCornersTransformation(16f.dpToPx()))
}
binding.root.setOnClickListener {
onItemClick?.invoke(item)
}

View File

@@ -118,9 +118,7 @@ class CharacterGalleryViewModel(
val token = "Bearer ${SharedPreferenceManager.token}"
isPurchasing = true
_uiState.value = _uiState.value?.copy(
isLoading = isRequesting || isPurchasing,
)
_uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing)
compositeDisposable.add(
repository.purchaseCharacterImage(token = token, imageId = imageId)
.subscribeOn(Schedulers.io())

View File

@@ -1,17 +1,18 @@
package kr.co.vividnext.sodalive.chat.original
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class OriginalWorkListResponse(
val totalCount: Long,
val content: List<OriginalWorkListItemResponse>
@SerializedName("totalCount") val totalCount: Long,
@SerializedName("content") val content: List<OriginalWorkListItemResponse>
)
@Keep
data class OriginalWorkListItemResponse(
val id: Long,
val imageUrl: String?,
val title: String,
val contentType: String
@SerializedName("id") val id: Long,
@SerializedName("imageUrl") val imageUrl: String?,
@SerializedName("title") val title: String,
@SerializedName("contentType") val contentType: String
)

View File

@@ -85,7 +85,6 @@ class OriginalWorkDetailActivity : BaseActivity<ActivityOriginalWorkDetailBindin
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}

View File

@@ -99,7 +99,6 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
items.addAll(list.map { BgItem(id = it.id, url = it.imageUrl) })
adapter.submit(items, selectedId)
loadingDialog.dismiss()
}, { _ ->
// 실패 시에도 현재까지의 목록 표시(없으면 빈 목록)
@@ -161,9 +160,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BgVH {
val binding = ItemChatBackgroundImageBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
val binding = ItemChatBackgroundImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BgVH(binding, onClick)
}

View File

@@ -655,14 +655,11 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// endregion
/** 쿼터 안내 메시지 뷰홀더: 광고 보기 + 캔 구매 버튼 */
class QuotaNoticeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val btnRewardedAd: View = itemView.findViewById(R.id.ll_rewarded_ad)
private val btnPurchase10Can: View = itemView.findViewById(R.id.ll_purchase_10_can)
private val btnPurchase20Can: View = itemView.findViewById(R.id.ll_purchase_20_can)
fun bind(onAction: (ChatQuotaNoticeAction) -> Unit) {
btnRewardedAd.setOnClickListener { onAction(ChatQuotaNoticeAction.REWARDED_AD) }
btnPurchase10Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_10_CAN) }
btnPurchase20Can.setOnClickListener { onAction(ChatQuotaNoticeAction.PURCHASE_20_CAN) }
}

View File

@@ -12,8 +12,10 @@ import androidx.annotation.Keep
enum class MessageStatus {
/** 전송 중 */
SENDING,
/** 전송 완료 */
SENT,
/** 전송 실패 */
FAILED
}

View File

@@ -4,7 +4,11 @@
package kr.co.vividnext.sodalive.chat.talk.room.db
import androidx.annotation.Keep
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Keep
@Dao

View File

@@ -45,15 +45,21 @@ class ImagePickerCropper(
// 13+ : 시스템 Photo Picker
private val pickPhoto: ActivityResultLauncher<PickVisualMediaRequest> =
caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
if (uri == null) {
onError(CancellationException("이미지 선택을 취소했습니다."))
} else {
handlePickedUri(uri)
}
}
// 12- : SAF GetContent
private val pickContent: ActivityResultLauncher<String> =
caller.registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
if (uri == null) {
onError(CancellationException("이미지 선택을 취소했습니다."))
} else {
handlePickedUri(uri)
}
}
// uCrop 결과 수신
@@ -105,7 +111,9 @@ class ImagePickerCropper(
val gifFile = copyUriToCacheAsGif(source)
lastCroppedFile = gifFile
val fileUri = FileProvider.getUriForFile(
context, "${BuildConfig.APPLICATION_ID}.fileprovider", gifFile
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
gifFile
)
// 2) 바로 반환 (크롭 생략)
@@ -137,13 +145,20 @@ class ImagePickerCropper(
)
?.use { c ->
val idx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idx >= 0 && c.moveToFirst()) c.getString(idx) else null
if (idx >= 0 && c.moveToFirst()) {
c.getString(idx)
} else {
null
}
}
}
private fun copyUriToCacheAsGif(uri: Uri): File {
val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir
else context.cacheDir
val base = if (config.useExternalCache) {
context.externalCacheDir ?: context.cacheDir
} else {
context.cacheDir
}
// 원본 이름 유지 시도, 실패하면 타임스탬프
val name = getDisplayName(uri)?.takeIf { it.endsWith(".gif", true) }
@@ -201,8 +216,11 @@ class ImagePickerCropper(
}
private fun createTempCropFile(): File {
val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir
else context.cacheDir
val base = if (config.useExternalCache) {
context.externalCacheDir ?: context.cacheDir
} else {
context.cacheDir
}
val ext = when (config.compressFormat) {
Bitmap.CompressFormat.PNG -> "png"

View File

@@ -89,14 +89,17 @@ object RealPathUtil {
} // MediaProvider
// DownloadsProvider
} else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
return if (isGooglePhotosUri(uri)) {
uri.lastPathSegment
} else {
getDataColumn(
context,
uri,
null,
null
)
}
} else if ("file".equals(uri.scheme!!, ignoreCase = true)) {
return uri.path
} // File
@@ -122,7 +125,6 @@ object RealPathUtil {
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)

View File

@@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.common
import android.content.Context
import kr.co.vividnext.sodalive.R
import java.text.ParseException
import java.text.ParsePosition
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
@@ -10,8 +10,9 @@ import java.util.Locale
import java.util.TimeZone
fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
val pastMillis = parseServerUtcToMillis(utcText)
val parsedTime = parseServerUtcToMillis(utcText)
?: return context.getString(R.string.character_comment_time_just_now)
val pastMillis = parsedTime.millis
val nowMillis = System.currentTimeMillis()
var diff = nowMillis - pastMillis
@@ -21,6 +22,10 @@ fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
val hour = 60 * minute
val day = 24 * hour
if (parsedTime.isIsoText && diff < RECENT_ISO_TIME_GRACE_MILLIS) {
return context.getString(R.string.character_comment_time_just_now)
}
if (diff < minute) {
return context.getString(R.string.character_comment_time_just_now)
}
@@ -75,19 +80,25 @@ class AndroidUtcRelativeTimeTextFormatter(context: Context) : UtcRelativeTimeTex
override fun format(utcText: String?): String = formatUtcRelativeTimeText(applicationContext, utcText)
}
private fun parseServerUtcToMillis(utcText: String?): Long? {
private fun parseServerUtcToMillis(utcText: String?): ParsedServerTime? {
if (utcText.isNullOrBlank()) return null
val value = utcText.trim()
val value = utcText.trim().normalizeIsoFraction()
if (value.all { it.isDigit() }) {
return try {
value.toLong()
val number = value.toLong()
ParsedServerTime(
millis = if (value.length <= EPOCH_SECONDS_MAX_LENGTH) number * 1000 else number,
isIsoText = false
)
} catch (_: NumberFormatException) {
null
}
}
val patterns = listOf(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
"yyyy-MM-dd'T'HH:mm:ssXXX",
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss",
@@ -99,11 +110,26 @@ private fun parseServerUtcToMillis(utcText: String?): Long? {
try {
val dateFormat = SimpleDateFormat(pattern, Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsed: Date? = dateFormat.parse(value)
if (parsed != null) return parsed.time
} catch (_: ParseException) {
val position = ParsePosition(0)
val parsed: Date? = dateFormat.parse(value, position)
if (parsed != null && position.index == value.length) {
return ParsedServerTime(millis = parsed.time, isIsoText = true)
}
} catch (_: RuntimeException) {
}
}
return null
}
private fun String.normalizeIsoFraction(): String {
return replace(Regex("""\.(\d{3})\d+"""), ".$1")
}
private data class ParsedServerTime(
val millis: Long,
val isIsoText: Boolean
)
private const val EPOCH_SECONDS_MAX_LENGTH = 10
private const val RECENT_ISO_TIME_GRACE_MILLIS = 10 * 60_000L

View File

@@ -1,10 +1,7 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import android.os.Build
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import kr.co.vividnext.sodalive.settings.language.LanguageManager
object Utils {
fun convertDurationToString(duration: Int, showHours: Boolean = true): String {

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.di
import android.content.Context
import com.google.gson.GsonBuilder
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.agora.v2v.V2vApi
import kr.co.vividnext.sodalive.agora.v2v.V2vRepository
@@ -181,10 +182,14 @@ import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelApi
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModel
import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModel
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveViewModel
@@ -345,7 +350,14 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), CharacterCommentApi::class.java) }
single { ApiBuilder().build(get(), OriginalWorkApi::class.java) }
single { ApiBuilder().build(get<Retrofit>(named("agoraRetrofit")), V2vApi::class.java) }
single { DmChatSocketClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl) }
single {
DmChatSocketClient(
okHttpClient = get(),
gson = get(),
baseUrl = baseUrl,
rawJsonLogger = { rawJson -> Logger.d("DM WebSocket raw JSON: $rawJson") }
)
}
}
private val viewModelModule = module {
@@ -452,7 +464,11 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorChannelAudioViewModel(get()) }
viewModel { CreatorChannelSeriesViewModel(get()) }
viewModel { CreatorChannelCommunityViewModel(get(), get()) }
viewModel { CreatorChannelCommunityDetailViewModel(get(), get(), get()) }
viewModel { CreatorChannelCommunityReplyViewModel(get(), get(), get()) }
viewModel { CreatorChannelFanTalkViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) }
viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
viewModel { CreatorChannelDonationViewModel(get(), get()) }
viewModel { PushNotificationListViewModel(get()) }
viewModel { CharacterTabViewModel(get()) }

View File

@@ -18,7 +18,7 @@ open class LiveDialog(
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
cancelButtonClick: (() -> Unit)? = null
) {
private val alertDialog: AlertDialog

View File

@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDon
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -73,13 +74,13 @@ interface ExplorerApi {
fun writeCheers(
@Body request: PostWriteCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
): Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>
@PUT("/explorer/profile/cheers")
fun modifyCheers(
@Body request: PutModifyCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
): Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>
@POST("/explorer/profile/channel-donation")
fun postChannelDonation(

View File

@@ -11,7 +11,7 @@ import kr.co.vividnext.sodalive.R
class CreatorFollowNotifyFragment(
private val onClickNotifyAll: () -> Unit,
private val onClickNotifyNone: () -> Unit,
private val onClickUnFollow: () -> Unit,
private val onClickUnFollow: () -> Unit
) : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,

View File

@@ -37,12 +37,12 @@ import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.common.image.BlurTransformation
import kr.co.vividnext.sodalive.base.BaseActivity
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.image.BlurTransformation
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
@@ -138,7 +138,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
binding.ivMenu
)
}
@@ -468,7 +468,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content,
cheersContent = content
)
}
},

View File

@@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName
data class PutModifyCheersRequest(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("content") var content: String? = null,
@SerializedName("isActive") var isActive: Boolean? = null,
@SerializedName("isActive") var isActive: Boolean? = null
)

View File

@@ -149,7 +149,6 @@ class CreatorCommunityAllViewModel(
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
fun registerComment(
@@ -282,49 +281,48 @@ class CreatorCommunityAllViewModel(
)
)
}
}
fun updateCommunityPostFixed(postId: Long, isFixed: Boolean) {
if (_isLoading.value == true) return
_isLoading.value = true
if (_isLoading.value == true) return
_isLoading.value = true
compositeDisposable.add(
repository.updateCommunityPostFixed(
postId = postId,
isFixed = isFixed,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
// 목록을 초기화하고 재조회하여 최신 고정 상태를 반영한다.
page = 1
isLast = false
getCommunityPostList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
compositeDisposable.add(
repository.updateCommunityPostFixed(
postId = postId,
isFixed = isFixed,
token = "Bearer ${SharedPreferenceManager.token}"
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
// 목록을 초기화하고 재조회하여 최신 고정 상태를 반영한다.
page = 1
isLast = false
getCommunityPostList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
}

View File

@@ -39,7 +39,8 @@ class CreatorCommunityReportDialog(
alertDialog.dismiss()
confirmButtonClick(reason)
} else {
Toast.makeText(activity,
Toast.makeText(
activity,
SodaLiveApplicationHolder.get()
.getString(R.string.screen_audio_content_detail_report_reason_required),
Toast.LENGTH_LONG

View File

@@ -16,7 +16,7 @@ class PurchaseCommunityPostDialog(
activity: Activity,
layoutInflater: LayoutInflater,
can: Int,
confirmButtonClick: () -> Unit,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog

View File

@@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName
data class PurchasePostRequest(
@SerializedName("postId") val postId: Long,
@SerializedName("timezone") val timezone: String,
@SerializedName("container") val container: String = "aos",
@SerializedName("container") val container: String = "aos"
)

View File

@@ -127,9 +127,7 @@ class CreatorCommunityCommentReplyItemViewHolder(
private val context: Context,
private val creatorId: Long,
private val binding: ItemCommunityPostCommentReplyBinding,
private val showOptionMenu: (
Context, View, Long, Long, Long, onClickModify: () -> Unit
) -> Unit,
private val showOptionMenu: (Context, View, Long, Long, Long, onClickModify: () -> Unit) -> Unit,
private val modifyComment: (Long, String) -> Unit,
private val onClickProfile: (Long) -> Unit
) : CreatorCommunityCommentReplyViewHolder(binding) {
@@ -178,5 +176,4 @@ class CreatorCommunityCommentReplyItemViewHolder(
binding.ivMenu.visibility = View.GONE
}
}
}

View File

@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.Manifest
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
@@ -76,7 +75,8 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
context = this,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f, aspectY = 1f,
aspectX = 1f,
aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),

View File

@@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName
enum class DonationRankingPeriod {
@SerializedName("WEEKLY")
WEEKLY,
@SerializedName("CUMULATIVE")
CUMULATIVE
}

View File

@@ -19,5 +19,5 @@ data class GetDonationAllResponse(
@SerializedName("donationRankingPeriod")
val donationRankingPeriod: DonationRankingPeriod? = null,
@SerializedName("userDonationRanking")
val userDonationRanking: List<UserDonationRankingResponse>,
val userDonationRanking: List<UserDonationRankingResponse>
)

View File

@@ -103,7 +103,7 @@ class UserProfileFantalkAllViewActivity : BaseActivity<ActivityUserProfileFantal
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content,
cheersContent = content
)
}
},

View File

@@ -13,7 +13,6 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityUserFollowerListBinding
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import org.koin.android.ext.android.inject

View File

@@ -1,8 +1,6 @@
package kr.co.vividnext.sodalive.following
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi
import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
import kr.co.vividnext.sodalive.user.UserApi
class FollowingCreatorRepository(private val api: LiveRecommendApi) {
fun getFollowedCreatorAllList(

View File

@@ -1,18 +1,26 @@
package kr.co.vividnext.sodalive.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
/**
* 콘텐츠 랭킹 정렬 기준
*/
enum class ContentRankingSortType {
@Keep
enum class ContentRankingSortType(val queryValue: String) {
// 매출
REVENUE,
@SerializedName("REVENUE")
REVENUE("REVENUE"),
// 판매량
SALES_COUNT,
@SerializedName("SALES_COUNT")
SALES_COUNT("SALES_COUNT"),
// 댓글 수
COMMENT_COUNT,
@SerializedName("COMMENT_COUNT")
COMMENT_COUNT("COMMENT_COUNT"),
// 좋아요 수
LIKE_COUNT
@SerializedName("LIKE_COUNT")
LIKE_COUNT("LIKE_COUNT")
}

View File

@@ -66,7 +66,6 @@ class CreatorRankingAdapter(
binding.tvFollow.text = context.getString(R.string.screen_home_following)
binding.tvFollow.setBackgroundResource(R.drawable.bg_round_corner_999_455a64)
binding.tvFollow.setTextColor(context.getColor(R.color.white))
} else {
binding.tvFollow.text = context.getString(R.string.screen_home_follow)
binding.tvFollow.setBackgroundResource(R.drawable.bg_round_corner_999_white)

View File

@@ -10,8 +10,8 @@ import kr.co.vividnext.sodalive.databinding.ItemDayOfWeekBinding
import java.util.Calendar
class DayOfWeekAdapter(
private val context: Context,
private val onClickItem: (SeriesPublishedDaysOfWeek) -> Unit,
context: Context,
private val onClickItem: (SeriesPublishedDaysOfWeek) -> Unit
) : RecyclerView.Adapter<DayOfWeekAdapter.ViewHolder>() {
inner class ViewHolder(
@@ -40,7 +40,7 @@ class DayOfWeekAdapter(
DayOfWeek(dayOfWeekStr = context.getString(R.string.day_fri_short), dayOfWeek = SeriesPublishedDaysOfWeek.FRI),
DayOfWeek(dayOfWeekStr = context.getString(R.string.day_sat_short), dayOfWeek = SeriesPublishedDaysOfWeek.SAT),
DayOfWeek(dayOfWeekStr = context.getString(R.string.day_sun_short), dayOfWeek = SeriesPublishedDaysOfWeek.SUN),
DayOfWeek(dayOfWeekStr = context.getString(R.string.day_random), dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM),
DayOfWeek(dayOfWeekStr = context.getString(R.string.day_random), dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM)
)
// 요일 숫자에 맞춰 배열

View File

@@ -23,7 +23,7 @@ interface HomeApi {
@GET("/api/home/day-of-week-series")
fun getDayOfWeekSeriesList(
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
@Query("dayOfWeek") dayOfWeek: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@@ -34,7 +34,7 @@ interface HomeApi {
@GET("/api/home/content-ranking")
fun getContentRankingBySort(
@Query("sort") sort: ContentRankingSortType,
@Query("sort") sort: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
}

View File

@@ -13,7 +13,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
class HomeContentAdapter(
private val itemSquareSizePx: Int? = null,
private val onClickItem: (Long) -> Unit,
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<HomeContentAdapter.ViewHolder>() {
private val items = mutableListOf<AudioContentMainItem>()

View File

@@ -14,9 +14,10 @@ class HomeRepository(private val api: HomeApi) {
)
fun getDayOfWeekSeriesList(
dayOfWeek: SeriesPublishedDaysOfWeek, token: String
dayOfWeek: SeriesPublishedDaysOfWeek,
token: String
) = api.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
dayOfWeek = dayOfWeek.queryValue,
authHeader = token
)
@@ -28,7 +29,7 @@ class HomeRepository(private val api: HomeApi) {
sortType: ContentRankingSortType,
token: String
) = api.getContentRankingBySort(
sort = sortType,
sort = sortType.queryValue,
authHeader = token
)
}

View File

@@ -1,5 +1,31 @@
package kr.co.vividnext.sodalive.home
enum class SeriesPublishedDaysOfWeek {
SUN, MON, TUE, WED, THU, FRI, SAT, RANDOM
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
enum class SeriesPublishedDaysOfWeek(val queryValue: String) {
@SerializedName("SUN")
SUN("SUN"),
@SerializedName("MON")
MON("MON"),
@SerializedName("TUE")
TUE("TUE"),
@SerializedName("WED")
WED("WED"),
@SerializedName("THU")
THU("THU"),
@SerializedName("FRI")
FRI("FRI"),
@SerializedName("SAT")
SAT("SAT"),
@SerializedName("RANDOM")
RANDOM("RANDOM")
}

View File

@@ -149,7 +149,6 @@ class LiveViewModel(
fun getSummary() {
if (!_isLoading.value!!) {
if (SharedPreferenceManager.token.isNotBlank()) {
getFollowedChannelList()
getLatestPostListFromCreatorsYouFollow()

View File

@@ -1,38 +0,0 @@
package kr.co.vividnext.sodalive.live.event_banner
import android.content.Context
import android.widget.ImageView
import coil.load
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.settings.event.EventItem
class EventBannerAdapter(
private val context: Context,
private val itemClick: (EventItem) -> Unit
) : BaseBannerAdapter<EventItem>() {
override fun bindData(
holder: BaseViewHolder<EventItem>,
data: EventItem,
position: Int,
pageSize: Int
) {
val ivThumbnail = holder.findViewById<ImageView>(R.id.iv_thumbnail)
ivThumbnail.load(data.thumbnailImageUrl) {
crossfade(true)
val layoutParams = ivThumbnail.layoutParams
val screenWidth = context.resources.displayMetrics.widthPixels
layoutParams.width = screenWidth
layoutParams.height = (screenWidth * 300) / 1000
ivThumbnail.layoutParams = layoutParams
}
ivThumbnail.setOnClickListener { itemClick(data) }
}
override fun getLayoutId(viewType: Int): Int {
return R.layout.item_event_slider
}
}

View File

@@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.GetRoomListResponse
class LiveNowAdapter(
private val onClick: (GetRoomListResponse) -> Unit,
private val onClick: (GetRoomListResponse) -> Unit
) : RecyclerView.Adapter<LiveNowAdapter.ViewHolder>() {
var items = mutableListOf<GetRoomListResponse>()

View File

@@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx
class LiveRecommendChannelAdapter(
private val onClick: (Long) -> Unit,
private val onClickMore: () -> Unit,
private val onClickMore: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val items = mutableListOf<GetRecommendChannelResponse>()

View File

@@ -15,5 +15,5 @@ data class GetLiveReservationResponse(
@SerializedName("price") val price: Int,
@SerializedName("masterNickname") val masterNickname: String,
@SerializedName("beginDateTimeUtc") val beginDateTimeUtc: String,
@SerializedName("cancelable") val cancelable: Boolean,
@SerializedName("cancelable") val cancelable: Boolean
) : Parcelable

View File

@@ -126,9 +126,7 @@ class LiveReservationCancelActivity : BaseActivity<ActivityLiveReservationCancel
binding.tvCancel.setOnClickListener {
viewModel.cancelReservation {
if (binding.tvPrice.text == SodaLiveApplicationHolder.get()
.getString(R.string.live_reservation_free)
) {
if (binding.tvPrice.text == SodaLiveApplicationHolder.get().getString(R.string.live_reservation_free)) {
binding.tvCancelComplete.visibility = View.GONE
} else {
binding.tvCancelComplete.visibility = View.VISIBLE

View File

@@ -6,10 +6,10 @@ 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.ItemLiveReservationStatusBinding
import kr.co.vividnext.sodalive.extensions.convertDateFormat
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
import java.util.Locale
@@ -22,7 +22,7 @@ class LiveReservationStatusAdapter(
val items = mutableListOf<GetLiveReservationResponse>()
inner class ViewHolder(
private val binding: ItemLiveReservationStatusBinding,
private val binding: ItemLiveReservationStatusBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetLiveReservationResponse) {

View File

@@ -90,7 +90,6 @@ class LiveReservationStatusViewModel(
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(

View File

@@ -3,7 +3,12 @@ package kr.co.vividnext.sodalive.live.room
import com.google.gson.annotations.SerializedName
enum class GenderRestriction {
@SerializedName("ALL") ALL,
@SerializedName("MALE_ONLY") MALE_ONLY,
@SerializedName("FEMALE_ONLY") FEMALE_ONLY
@SerializedName("ALL")
ALL,
@SerializedName("MALE_ONLY")
MALE_ONLY,
@SerializedName("FEMALE_ONLY")
FEMALE_ONLY
}

View File

@@ -481,8 +481,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
context = this,
excludeGif = true,
config = ImagePickerCropper.Config(
aspectX = 2f, aspectY = 3.8f,
maxWidth = 1080, maxHeight = 2052,
aspectX = 2f,
aspectY = 3.8f,
maxWidth = 1080,
maxHeight = 2052,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
@@ -640,7 +642,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
userId,
nickname,
isBlock,
view,
view
)
}
)
@@ -1208,7 +1210,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
roomInfoEditDialog.setMenuPreset(it)
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageFile, isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled ->
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageFile,
isActivateMenu, menuId, menu, isAdult, isEntryMessageEnabled ->
if (isEntryMessageEnabled != null) {
this.isEntryMessageEnabled = isEntryMessageEnabled
}
@@ -1337,7 +1340,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
hasInvokedJoinChannel = true
joinChannel(response)
} else {
Logger.e("joinChannel - skip (rtcConnected=$rtcConnected, rtmLoggedIn=$rtmLoggedIn, hasInvokedJoinChannel=$hasInvokedJoinChannel)")
Logger.e(
"joinChannel - skip (rtcConnected=$rtcConnected, " +
"rtmLoggedIn=$rtmLoggedIn, hasInvokedJoinChannel=$hasInvokedJoinChannel)"
)
}
}
@@ -3027,8 +3033,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val elapsed = System.currentTimeMillis() - pressStartTime
// 경과 시간 기준으로 물 채우기(progress) 계산 (0..100)
val progressFraction = (elapsed.coerceAtMost(longPressDurationMs)
.toFloat() / longPressDurationMs.toFloat())
val progressFraction = (
elapsed.coerceAtMost(longPressDurationMs)
.toFloat() / longPressDurationMs.toFloat()
)
val progress = (progressFraction * 100f).toInt()
longPressCenterHeart?.let { heartView ->
try {
@@ -3131,8 +3139,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
heart.setFrontWaveColor("#ff959a".toColorInt())
heart.setBehindWaveColor("#ff959a".toColorInt())
val elapsed = System.currentTimeMillis() - pressStartTime
val progress = ((elapsed.coerceAtMost(longPressDurationMs)
.toFloat() / longPressDurationMs.toFloat()) * 100f).toInt()
val progress = (
(
elapsed.coerceAtMost(longPressDurationMs).toFloat() / longPressDurationMs.toFloat()
) * 100f
).toInt()
try {
heart.progress = progress
} catch (_: Throwable) {
@@ -3589,14 +3600,20 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val s = 0.9f
moveTo(0f, -0.45f * s)
cubicTo(
0.62f * s, -1.02f * s,
1.22f * s, -0.04f * s,
0f, 0.65f * s
0.62f * s,
-1.02f * s,
1.22f * s,
-0.04f * s,
0f,
0.65f * s
)
cubicTo(
-1.22f * s, -0.04f * s,
-0.62f * s, -1.02f * s,
0f, -0.45f * s
-1.22f * s,
-0.04f * s,
-0.62f * s,
-1.02f * s,
0f,
-0.45f * s
)
close()
}
@@ -3784,14 +3801,20 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val s = 0.9f
moveTo(0f, -0.45f * s)
cubicTo(
0.62f * s, -1.02f * s,
1.22f * s, -0.04f * s,
0f, 0.65f * s
0.62f * s,
-1.02f * s,
1.22f * s,
-0.04f * s,
0f,
0.65f * s
)
cubicTo(
-1.22f * s, -0.04f * s,
-0.62f * s, -1.02f * s,
0f, -0.45f * s
-1.22f * s,
-0.04f * s,
-0.62f * s,
-1.02f * s,
0f,
-0.45f * s
)
close()
}
@@ -3899,14 +3922,20 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val s = 0.9f
moveTo(0f, -0.45f * s)
cubicTo(
0.62f * s, -1.02f * s,
1.22f * s, -0.04f * s,
0f, 0.65f * s
0.62f * s,
-1.02f * s,
1.22f * s,
-0.04f * s,
0f,
0.65f * s
)
cubicTo(
-1.22f * s, -0.04f * s,
-0.62f * s, -1.02f * s,
0f, -0.45f * s
-1.22f * s,
-0.04f * s,
-0.62f * s,
-1.02f * s,
0f,
-0.45f * s
)
close()
}
@@ -3946,7 +3975,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (rotateEnabled) 300f * Math.random().toFloat() else 0f // deg/s
val rotation0 = (-180f + 360f * Math.random().toFloat())
val fadeStartRatio = 0.80f + 0.15f * Math.random().toFloat() // 0.80..0.95
val fadeStartY = h * fadeStartRatio
@@ -3991,7 +4019,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val fadeAlpha = if (y >= d.fadeStartY) {
val denom = (h - d.fadeStartY).coerceAtLeast(1f)
(1f - (y - d.fadeStartY) / denom).coerceIn(0f, 1f)
} else 1f
} else {
1f
}
val alpha = fadeAlpha.coerceIn(0f, 1f)
paint.alpha = (alpha * 255).toInt().coerceIn(0, 255)
@@ -4139,7 +4169,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
return false
}
})
.into(binding.ivSignature)
}
@@ -4205,7 +4234,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
}
} else {
binding.flRoulette.visibility = View.GONE
}
@@ -4221,7 +4249,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
type = LiveRoomChatRawMessageType.ROULETTE_DONATION,
message = randomItem,
can = can,
donationMessage = "",
donationMessage = ""
)
)
@@ -4269,7 +4297,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
.subscribe(
{ response ->
Logger.e("성공: $response")
}, { error ->
},
{ error ->
Logger.e("실패: ${error.message}")
}
)

View File

@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.live.room
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
@@ -10,12 +9,13 @@ import android.view.View
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.drawable.toDrawable
import kr.co.vividnext.sodalive.databinding.DialogLiveRoomBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LiveRoomDialog(
activity: Activity,
layoutInflater: LayoutInflater,
layoutInflater: LayoutInflater
) {
private val alertDialog: AlertDialog
@@ -27,7 +27,7 @@ class LiveRoomDialog(
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
}
fun show(width: Int) {

View File

@@ -2,10 +2,10 @@ package kr.co.vividnext.sodalive.live.room
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.drawable.toDrawable
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
@@ -17,7 +17,7 @@ class LiveRoomNoChattingDialog(
layoutInflater: LayoutInflater,
nickname: String,
profileUrl: String,
confirmButtonClick: () -> Unit,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog
@@ -30,7 +30,7 @@ class LiveRoomNoChattingDialog(
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()

View File

@@ -3,10 +3,21 @@ package kr.co.vividnext.sodalive.live.room
import com.google.gson.annotations.SerializedName
enum class LiveRoomRequestType {
@SerializedName("INVITE_SPEAKER") INVITE_SPEAKER,
@SerializedName("CHANGE_LISTENER") CHANGE_LISTENER,
@SerializedName("KICK_OUT") KICK_OUT,
@SerializedName("SET_MANAGER") SET_MANAGER,
@SerializedName("RELEASE_MANAGER") RELEASE_MANAGER,
@SerializedName("NO_CHATTING") NO_CHATTING,
@SerializedName("INVITE_SPEAKER")
INVITE_SPEAKER,
@SerializedName("CHANGE_LISTENER")
CHANGE_LISTENER,
@SerializedName("KICK_OUT")
KICK_OUT,
@SerializedName("SET_MANAGER")
SET_MANAGER,
@SerializedName("RELEASE_MANAGER")
RELEASE_MANAGER,
@SerializedName("NO_CHATTING")
NO_CHATTING
}

View File

@@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.live.room
import com.google.gson.annotations.SerializedName
enum class LiveRoomStatus {
@SerializedName("NOW") NOW,
@SerializedName("RESERVATION") RESERVATION
@SerializedName("NOW")
NOW,
@SerializedName("RESERVATION")
RESERVATION
}

View File

@@ -3,6 +3,9 @@ package kr.co.vividnext.sodalive.live.room
import com.google.gson.annotations.SerializedName
enum class LiveRoomType {
@SerializedName("OPEN") OPEN,
@SerializedName("PRIVATE") PRIVATE,
@SerializedName("OPEN")
OPEN,
@SerializedName("PRIVATE")
PRIVATE
}

View File

@@ -7,5 +7,5 @@ import java.util.TimeZone
@Keep
data class StartLiveRequest(
@SerializedName("roomId") val roomId: Long,
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id
)

View File

@@ -61,7 +61,7 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
"%d-%02d-%02d",
year,
monthOfYear + 1,
dayOfMonth,
dayOfMonth
)
viewModel.setReservationDate(
String.format(
@@ -136,8 +136,10 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
context = this,
excludeGif = true,
config = ImagePickerCropper.Config(
aspectX = 2f, aspectY = 3.8f,
maxWidth = 1080, maxHeight = 2052,
aspectX = 2f,
aspectY = 3.8f,
maxWidth = 1080,
maxHeight = 2052,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),

View File

@@ -1,12 +1,12 @@
package kr.co.vividnext.sodalive.live.room.create
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UiText
@@ -16,7 +16,6 @@ import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
import kr.co.vividnext.sodalive.R
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody

View File

@@ -20,7 +20,7 @@ class LiveCancelDialog(
confirmButtonTitle: String,
confirmButtonClick: (String) -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
cancelButtonClick: (() -> Unit)? = null
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLiveInputBinding.inflate(layoutInflater)

View File

@@ -21,7 +21,7 @@ class LivePaymentDialog(
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
cancelButtonClick: (() -> Unit)? = null
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLivePaymentBinding.inflate(layoutInflater)

View File

@@ -20,7 +20,7 @@ class LiveRoomPasswordDialog(
activity: Activity,
layoutInflater: LayoutInflater,
can: Int,
confirmButtonClick: (String) -> Unit,
confirmButtonClick: (String) -> Unit
) {
private val alertDialog: AlertDialog

View File

@@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName
data class LiveRoomLikeHeartRequest(
@SerializedName("roomId") val roomId: Long,
@SerializedName("container") val container: String,
@SerializedName("heartCount") val heartCount: Int = 1,
@SerializedName("heartCount") val heartCount: Int = 1
)

View File

@@ -15,7 +15,6 @@ 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.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityMenuConfigBinding
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel

View File

@@ -195,19 +195,19 @@ abstract class LiveRoomProfileViewHolder(binding: ViewBinding) :
}
class LiveRoomProfileSpeakerTitleViewHolder(
private val binding: ItemLiveRoomProfileHeaderBinding,
private val binding: ItemLiveRoomProfileHeaderBinding
) : LiveRoomProfileViewHolder(binding) {
override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
}
class LiveRoomProfileListenerTitleViewHolder(
private val binding: ItemLiveRoomProfileHeaderBinding,
private val binding: ItemLiveRoomProfileHeaderBinding
) : LiveRoomProfileViewHolder(binding) {
override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
}
class LiveRoomProfileManagerTitleViewHolder(
private val binding: ItemLiveRoomProfileHeaderBinding,
private val binding: ItemLiveRoomProfileHeaderBinding
) : LiveRoomProfileViewHolder(binding) {
override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
}

View File

@@ -28,7 +28,7 @@ class LiveRoomUserProfileDialog(
private val onClickChangeListener: (Long) -> Unit,
private val onClickKickOut: (Long) -> Unit,
private val onClickNoChatting: (Long, String, String) -> Unit,
private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit,
private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLiveRoomUserProfileBinding.inflate(layoutInflater)

View File

@@ -24,8 +24,6 @@ class LiveTagFragment(
private val selectedTags: Set<String>,
private val onItemClick: (String, Boolean) -> Boolean
) : BottomSheetDialogFragment() {
private val viewModel: LiveTagViewModel by inject()
private lateinit var adapter: LiveTagAdapter

View File

@@ -190,12 +190,12 @@ class LiveRoomEditViewModel(
beginDate = date.convertDateFormat(
from = "yyyy.MM.dd",
to = "yyyy-MM-dd",
to = "yyyy-MM-dd"
)
beginTime = time.convertDateFormat(
from = "a hh:mm",
to = "HH:mm",
to = "HH:mm"
)
beginDateTimeStr = "$beginDate $beginTime"

View File

@@ -132,9 +132,12 @@ class RouletteView @JvmOverloads constructor(
val targetAngle = 0 - (getAngleForOption(option) + 360 * 10)
val rotateAnimation = RotateAnimation(
0f, targetAngle,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f
0f,
targetAngle,
RotateAnimation.RELATIVE_TO_SELF,
0.5f,
RotateAnimation.RELATIVE_TO_SELF,
0.5f
)
rotateAnimation.duration = 2000
rotateAnimation.fillAfter = true

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