2071 Commits
Author SHA1 Message Date
klaus 1f8d0beb2f fix(my): 배너 이미지 리소스를 정리한다 2026-08-26 17:13:11 +09:00
klaus 10886153b2 docs(creator): 채널 표시 정리 문서를 완료한다 2026-08-26 16:30:16 +09:00
klaus 1f88d721d7 feat(creator): 채널 표시와 상대 시간을 정리한다 2026-08-26 16:30:03 +09:00
klaus 9939910c71 chore(app): versionCode를 250으로 올린다 2026-08-26 16:29:55 +09:00
klaus 4f4ae62026 fix(creator): 후원 item locale 표시를 적용한다 2026-08-26 13:27:20 +09:00
klaus 3560d7bf89 chore(app): versionCode를 249로 올린다 2026-08-26 13:24:45 +09:00
klaus 5cfc42774c test(ui): 문구 변경 검증을 정리한다 2026-08-26 00:56:18 +09:00
klaus 4130ab296e fix(home): 모두 팔로우 확인 문구를 정리한다 2026-08-25 16:38:43 +09:00
klaus 87d9a1d377 fix(fanletter-write): 오른쪽 하단에 있는 글자수 단위(자) 제거 2026-08-25 16:04:43 +09:00
klaus 70f4266ed8 fix(ui): 텍스트 탭 가로 스크롤을 적용한다 2026-08-25 15:57:10 +09:00
klaus 7a9c2512cf fix(brand): 보이스온 표기를 소다라이브로 교체한다 2026-08-25 13:59:55 +09:00
klaus a04704db40 fix(content): 콘텐츠, 시리즈 오리지널 아이콘 리소스를 교체한다 2026-08-25 12:26:30 +09:00
klaus ec808cfd0c fix(home): 텍스트 로고 리소스를 교체한다 2026-08-25 12:18:51 +09:00
klaus 817234a80b fix(app): 런처 아이콘 리소스를 교체한다 2026-08-25 12:09:55 +09:00
klaus 88cb581231 fix(splash): 로고와 문구 배치를 정리한다 2026-08-25 11:49:19 +09:00
klaus 0ec6c6c29b fix(following): empty 안내 문구를 정리한다 2026-08-19 17:57:53 +09:00
klaus 3d217197e7 fix(nav): 선택 아이콘 리소스를 정리한다 2026-08-18 13:29:57 +09:00
klaus 8f942c676a fix(home): 팔로잉 크리에이터 전체 버튼을 고정한다 2026-08-18 13:25:21 +09:00
klaus 388304dbaa feat(following): 팔로우 해제 확인 dialog를 적용한다 2026-08-18 11:57:21 +09:00
klaus 16633b978e docs(following): 팔로잉 해제 확인 계획을 추가한다 2026-08-18 11:55:26 +09:00
klaus 80a8cd1d2b feat(creator): 일본어 보기 방식 label을 숨긴다 2026-08-14 19:20:42 +09:00
klaus 60df86f899 feat(creator): empty state 문구와 버튼 정책을 정리한다 2026-08-14 18:30:03 +09:00
klaus 5d43f367e3 docs(creator): empty state 문구 계획을 추가한다 2026-08-14 18:29:30 +09:00
klaus 2452a84395 test(i18n): 팬레터 일본어 문구 기대값을 맞춘다 2026-08-14 16:21:27 +09:00
klaus 7a21b37d8f feat(creator): 차단 성공 후 채널에서 이탈한다 2026-08-14 16:20:29 +09:00
klaus cbce4af2f9 fix(i18n): 전체 카운트 문구를 정리한다 2026-08-14 14:16:40 +09:00
klaus 7f8388aa21 feat(i18n): 팬레터 문구를 반영한다 2026-08-14 12:31:01 +09:00
klaus 7c44f3d66c docs(i18n): 일본어 리소스 PRD 완료 기준을 갱신한다 2026-08-11 19:01:50 +09:00
klaus 43b6496289 chore(app): 앱 버전을 1.56.1로 올린다 2026-08-11 18:53:30 +09:00
klaus 30a4a9ffc7 feat(i18n): 일본어 리소스를 반영한다 2026-08-11 17:58:56 +09:00
klaus 84cd9bbf41 docs(i18n): 일본어 리소스 반영 계획을 추가한다 2026-08-11 17:57:50 +09:00
klaus 75480e7a71 docs(creator): 커뮤니티 후속 작업 기록을 갱신한다 2026-08-11 00:11:11 +09:00
klaus 8b8b384b29 feat(creator): 커뮤니티 구매와 메뉴 동작을 정리한다 2026-08-11 00:10:46 +09:00
klaus 02cd583f56 chore(app): 데이터스토어 버전을 올린다 2026-08-04 14:13:01 +09:00
klaus 3c21a764ae fix(home): 라이브 전체 버튼을 오른쪽에 고정한다 2026-08-04 11:45:29 +09:00
klaus d20d6aa971 docs(agent): 테스트 범위를 로직으로 제한한다 2026-08-04 11:44:58 +09:00
klaus f50f987282 chore(app): 앱 버전 코드를 올린다 2026-08-03 23:04:16 +09:00
klaus f508d847ea feat(creator): 내 채널 팔로워 목록을 연결한다 2026-08-03 21:39:23 +09:00
klaus e86e5a1c3d chore(app): 앱 버전을 1.56.0으로 올린다 2026-08-03 18:21:42 +09:00
klaus 26eae18edc fix(content): 콘텐츠 상세 펼침 말줄임을 복원한다 2026-08-03 16:33:35 +09:00
klaus b5cfdc83e3 fix(live): 전체 라이브 제목을 On Air로 통일한다 2026-08-03 15:07:31 +09:00
klaus dd4a5167a9 fix(home): 라이브 전체 항목을 항상 표시한다 2026-07-31 18:34:23 +09:00
klaus d592038edd fix(home): 홈 추천 후속 검증을 반영한다 2026-07-31 16:29:06 +09:00
klaus a106548f87 fix(content): 콘텐츠 고정 한도 안내를 수정한다 2026-07-31 03:48:35 +09:00
klaus 49f6fe592c fix(home): 팔로잉 탭 후속 검증을 반영한다 2026-07-31 03:22:50 +09:00
klaus f600d10e8b feat(home): 최근 활동 LIVE 라우팅을 추가한다 2026-07-30 16:42:33 +09:00
klaus a1aa9c2243 docs(docs): 문서 샘플과 리뷰 규칙을 갱신한다 2026-07-30 14:18:07 +09:00
klaus 3a62ed8463 chore(git): worktree 디렉터리를 제외한다 2026-07-22 18:11:14 +09:00
klaus 6755c8ae99 feat(main): 메인 탭 당겨서 새로고침을 추가한다 2026-07-22 17:48:33 +09:00
klaus b17e250665 docs(main): 당겨서 새로고침 계획을 추가한다 2026-07-22 00:35:34 +09:00
klaus 074c945008 chore(app): 앱 버전을 1.55.1로 올린다 2026-07-20 15:41:48 +09:00
klaus 16aa946621 feat(main): 홈 크리에이터 FAB를 추가한다 2026-07-17 00:08:33 +09:00
klaus 831533b4f6 refactor(creator): 진행 중 라이브 컴포넌트를 공통화한다 2026-07-16 22:59:07 +09:00
klaus de90f771c1 refactor(v2): 도메인 액션 경로를 통합한다 2026-07-16 16:57:39 +09:00
klaus 4373452478 docs(v2): 라이브 액션 검증 기록을 갱신한다 2026-07-15 21:27:53 +09:00
klaus 0b3042d29e refactor(onair): 라이브 입장 흐름을 공통화한다 2026-07-15 21:27:44 +09:00
klaus cc23992d2e refactor(home): 추천 라이브 진입을 공통화한다 2026-07-15 21:27:37 +09:00
klaus 3a49158243 refactor(creator): 라이브 액션을 공통화한다 2026-07-15 21:27:31 +09:00
klaus d1509d4359 refactor(live): 공통 액션 코디네이터를 추가한다 2026-07-15 21:27:23 +09:00
klaus c223b3b8a4 feat(live): 생성 결과 계약을 추가한다 2026-07-15 21:26:53 +09:00
klaus b582545991 feat(live): 입장 정책 모델을 추가한다 2026-07-15 21:26:37 +09:00
klaus fe79431744 refactor(content): 상세 진입을 공통 액션으로 통합한다 2026-07-14 22:11:10 +09:00
klaus 1c5fbb38f1 style(content): 주석 오디오 너비 계산 줄바꿈을 정리한다 2026-07-14 20:48:53 +09:00
klaus dadf3a519d test(home): 팔로잉 empty 문구 검증을 정리한다 2026-07-14 20:45:34 +09:00
klaus ddc949f9d9 refactor(live): 온에어 접근 가드를 공통화한다 2026-07-14 20:45:29 +09:00
klaus 6973816b8c refactor(creator): 크리에이터 접근 가드를 공통화한다 2026-07-14 20:45:23 +09:00
klaus e5b99be9c4 refactor(main): 기존 로그인 가드를 제거한다 2026-07-14 20:45:16 +09:00
klaus 5ff92980a2 refactor(chat): 채팅 접근 가드를 공통화한다 2026-07-14 20:44:58 +09:00
klaus 2fda80d602 refactor(content): 콘텐츠 접근 가드를 공통화한다 2026-07-14 20:44:53 +09:00
klaus 7e12aa8720 refactor(home): 홈 접근 가드를 공통화한다 2026-07-14 20:44:42 +09:00
klaus d5d3444c3a refactor(main): 메인 접근 가드를 공통화한다 2026-07-14 20:44:36 +09:00
klaus a6601dcea1 feat(access): 공통 접근 실행기를 추가한다 2026-07-14 20:44:20 +09:00
klaus 4d51806cee feat(access): 접근 정책 모델을 추가한다 2026-07-14 20:44:13 +09:00
klaus 98db0b657e docs(v2): 공통 접근 가드 계획을 보강한다 2026-07-14 20:44:06 +09:00
klaus 9f70bcd3eb docs(v2): 공통 접근 가드 계획을 기록한다 2026-07-14 12:22:58 +09:00
klaus cc8d061b51 docs(content): 상세 오디오 광고 제거 검증을 기록한다 2026-07-13 16:59:20 +09:00
klaus 1b952054fb fix(content): 상세 오디오 광고를 제거한다 2026-07-13 16:59:10 +09:00
klaus 2c609836fd fix(home): 팔로잉 empty 문구를 중앙 표시한다 2026-07-13 16:17:12 +09:00
klaus da36964347 fix(home): 팔로잉 empty 문구 번역을 갱신한다 2026-07-13 16:17:06 +09:00
klaus 47b9c3dec2 docs(home): 팔로잉 empty 문구 계획을 기록한다 2026-07-13 16:17:01 +09:00
klaus 17a9a2f601 fix(creator): 채널 진입과 채팅 인증을 보강한다 2026-07-13 14:09:06 +09:00
klaus c43b9cdbd8 docs(creator): 채널 로그인 가드 계획을 기록한다 2026-07-13 14:09:01 +09:00
klaus 7ec4e09fb4 versionCode 242 2026-07-13 12:00:54 +09:00
klaus 862cec8375 fix(dm): 광고 로드 실패 안내를 보강한다 2026-07-12 22:09:00 +09:00
klaus 2d4956ae25 fix(dm): 쿼터 안내 광고 버튼을 숨긴다 2026-07-12 22:08:41 +09:00
klaus beadc7d4c4 chore(app): 버전 코드를 갱신한다 2026-07-12 20:52:07 +09:00
klaus 2633912e93 test(dm): 채팅 플로팅 버튼 제거 검증을 맞춘다 2026-07-12 20:52:01 +09:00
klaus 6cbc751f0c fix(community): 채널 이미지 원본비율 표시를 반영한다 2026-07-12 20:51:54 +09:00
klaus 2a499eaf41 fix(home): 커뮤니티 이미지 로드 크기를 전달한다 2026-07-12 20:51:36 +09:00
klaus 9244a00713 fix(feed): 커뮤니티 이미지 원본비율 높이를 적용한다 2026-07-12 20:51:30 +09:00
klaus 79eadbd39d docs(community): 이미지 원본비율 요구를 기록한다 2026-07-12 20:51:22 +09:00
klaus c389d1945d fix(dm): 채팅 플로팅 버튼을 숨긴다 2026-07-12 17:45:19 +09:00
klaus 2d8f8cab81 fix(community): 상세 잠금 반응과 댓글을 숨긴다 2026-07-12 09:54:26 +09:00
klaus f799449d54 docs(community): 상세 잠금 반응 숨김 계획을 기록한다 2026-07-12 09:54:18 +09:00
klaus 9da6c7381d fix(community): 리스트 잠금 반응 표시를 숨긴다 2026-07-12 09:54:10 +09:00
klaus ced226c3ef docs(community): 리스트 잠금 반응 숨김 계획을 기록한다 2026-07-12 09:54:01 +09:00
klaus f69a9a6ee0 docs(community): 그리드 잠금 오버레이 요구를 기록한다 2026-07-12 09:28:05 +09:00
klaus 94d2773d1e fix(community): 그리드 잠금 오버레이를 보정한다 2026-07-12 09:27:59 +09:00
klaus bc5fd46671 fix(content): 페이월 가격 배지를 보정한다 2026-07-12 08:46:59 +09:00
klaus a00745dd38 docs(content): 상세 오디오 구매 검증을 기록한다 2026-07-12 08:33:02 +09:00
klaus 01908c84d3 docs(content): 상세 오디오 구매 요구를 기록한다 2026-07-12 08:32:55 +09:00
klaus 39959c3f47 fix(content): 상세 오디오와 구매 흐름을 보정한다 2026-07-12 08:32:48 +09:00
klaus 53cc96b84d fix(home): 커뮤니티 하트 아이콘을 PNG로 교체한다 2026-07-12 07:46:40 +09:00
klaus 1aeb336898 fix(home): 추천 섹션 chevron을 숨긴다 2026-07-12 07:32:49 +09:00
klaus 1715c591a5 docs(home): 섹션 chevron 요구를 기록한다 2026-07-12 07:32:38 +09:00
klaus eb41506bf4 fix(content): 상세 댓글 border를 반영한다 2026-07-12 06:29:50 +09:00
klaus 9fdcd49c98 docs(content): 상세 border 요구를 기록한다 2026-07-12 06:29:43 +09:00
klaus 8538f014aa fix(content): 댓글 섹션 제목을 변경한다 2026-07-12 05:39:04 +09:00
klaus 848cd03068 docs(home): 장르 크리에이터 미표시 검증을 기록한다 2026-07-12 05:29:44 +09:00
klaus 4debf4654d fix(home): 장르 크리에이터 섹션을 숨긴다 2026-07-12 05:29:37 +09:00
klaus 6ed2be4173 docs(home): 장르 크리에이터 미표시 요구를 기록한다 2026-07-12 05:29:26 +09:00
klaus 557de11bb3 fix(home): 최근 소식 상세 이동을 연결한다 2026-07-12 04:24:47 +09:00
klaus 97d917e35d docs(home): 최근 소식 상세 이동 요구를 기록한다 2026-07-12 04:24:42 +09:00
klaus 9b2666e671 fix(home): 팔로잉 라이브 카드 레이아웃을 보정한다 2026-07-12 03:47:55 +09:00
klaus c883556dbf chore(gradle): Java toolchain resolver를 추가한다 2026-07-12 03:42:22 +09:00
klaus 5bcd0f64d2 fix(content): 전체 탭 pagination 스크롤을 안정화한다 2026-07-12 01:24:19 +09:00
klaus d53167c48e docs(content): 전체 탭 pagination 안정화 요구를 기록한다 2026-07-12 01:24:02 +09:00
klaus c2b212c1ce fix(splash): 스플래시 태그라인 문구를 변경한다 2026-07-11 23:55:41 +09:00
klaus d1510b8ba3 fix(splash): 스플래시 화면 구성을 단순화한다 2026-07-11 23:55:27 +09:00
klaus 273feef017 chore(dm): WebSocket raw JSON 로그를 주입한다 2026-07-11 22:03:32 +09:00
klaus 11a42bf8a1 feat(dm): 메시지 시간을 표시한다 2026-07-11 22:03:24 +09:00
klaus d436efdc66 fix(dm): 채팅방 입력과 프로필 표시를 보정한다 2026-07-11 22:03:16 +09:00
klaus 30f7bd7556 fix(dm): 실시간 연결 상태 처리를 보강한다 2026-07-11 22:03:03 +09:00
klaus 62007862a5 feat(dm): 음성 메시지 API 계약을 추가한다 2026-07-11 22:02:51 +09:00
klaus 2786e45c75 feat(dm): WebSocket envelope 계약을 보정한다 2026-07-11 22:02:40 +09:00
klaus 46c3a02655 refactor(dm): legacy realtime 구현을 제거한다 2026-07-11 22:02:29 +09:00
klaus 5ca07ef049 docs(dm): WebSocket 전환 요구를 보강한다 2026-07-11 22:02:20 +09:00
klaus c715c890b5 feat(chat): 탭별 empty state를 표시한다 2026-07-11 22:02:13 +09:00
klaus 9925a954d1 feat(chat): empty state 문구 리소스를 추가한다 2026-07-11 22:02:04 +09:00
klaus 0c111aaf47 docs(chat): empty state 요구를 기록한다 2026-07-11 22:01:56 +09:00
klaus 2a2315ecbc docs(home): 커뮤니티 상세 이동 요구를 기록한다 2026-07-10 10:38:59 +09:00
klaus 17f77351ad fix(home): 커뮤니티 상세 이동을 연결한다 2026-07-10 10:38:42 +09:00
klaus abd084cff8 fix(content-ranking): 카드 UI 크기를 반응형 적용한다 2026-07-10 10:16:36 +09:00
klaus e77d0a1d5b fix(content-ranking): 텍스트 크기 기준을 추가한다 2026-07-10 10:16:27 +09:00
klaus 991d70f22b fix(creator-ranking): 카드 UI 크기를 반응형 적용한다 2026-07-10 10:16:18 +09:00
klaus f477b563a2 fix(creator-ranking): 텍스트 크기 기준을 확장한다 2026-07-10 10:16:09 +09:00
klaus 63a7c28e96 feat(ranking): 반응형 폭 스케일을 추가한다 2026-07-10 10:16:01 +09:00
klaus 2b2cb66dfc docs(ranking): 반응형 크기 요구를 기록한다 2026-07-10 10:15:54 +09:00
klaus bf5cc1d7bb fix(content): 전체 탭 sort-bar 표시를 정리한다 2026-07-10 07:35:46 +09:00
klaus bbf94b8a0d feat(creator-channel): 정렬 팝업 옵션을 주입한다 2026-07-10 07:35:39 +09:00
klaus 951d7cf561 fix(content): 전체 탭 정렬 값을 보정한다 2026-07-10 07:35:31 +09:00
klaus 40337c9590 docs(content): 전체 탭 후속 요구를 기록한다 2026-07-10 07:35:19 +09:00
klaus 79a6ecdc0b test(json): 난독화 안정성 검증을 추가한다 2026-07-09 17:39:07 +09:00
klaus 33f42e62b6 fix(content): 콘텐츠 개요 query 값을 고정한다 2026-07-09 17:39:01 +09:00
klaus 1b660fe542 fix(content): 전체 탭 query 값을 고정한다 2026-07-09 17:38:55 +09:00
klaus d0539b00b0 fix(content): 오디오 랭킹 query 값을 고정한다 2026-07-09 17:38:46 +09:00
klaus e089ef6f8a fix(creator-channel): 콘텐츠 정렬 query 값을 고정한다 2026-07-09 17:38:39 +09:00
klaus a8a70df304 fix(series): 요일 query 값을 고정한다 2026-07-09 17:38:34 +09:00
klaus a207a8f840 fix(home): 홈 랭킹 query 값을 고정한다 2026-07-09 17:38:28 +09:00
klaus 6c85f81f98 fix(json): DTO 직렬화 이름을 고정한다 2026-07-09 17:38:18 +09:00
klaus 6a88019851 docs(json): 난독화 안정화 요구를 기록한다 2026-07-09 17:38:11 +09:00
klaus 379263ec87 fix(content): 추천 오디오 카드 폭을 보정한다 2026-07-09 14:56:19 +09:00
klaus f3d610633c docs(content): 추천 오디오 폭 보정 요구를 기록한다 2026-07-09 14:56:06 +09:00
klaus 335db22680 docs(content): 댓글 많은 오디오 UI 보정 기록을 추가한다 2026-07-09 08:46:35 +09:00
klaus b71bb43dd6 fix(content): 최근 댓글 오디오 카드 UI를 보정한다 2026-07-09 08:46:21 +09:00
klaus dace1416c5 fix(content): 추천 섹션 순서를 조정한다 2026-07-09 08:28:23 +09:00
klaus 1baf3ac7e0 fix(creator-channel): FanTalk 문구를 한 줄로 표시한다 2026-07-09 08:19:04 +09:00
klaus f3a4578a66 feat(creator-channel): 홈 섹션 이동을 연결한다 2026-07-09 08:19:00 +09:00
klaus b745c3d675 docs(creator-channel): 홈 섹션 이동 요구를 기록한다 2026-07-09 08:18:54 +09:00
klaus f993d343a9 feat(creator-channel): FanTalk 상세 진입점을 연결한다 2026-07-09 07:44:21 +09:00
klaus 1ba7846f4c feat(creator-channel): FanTalk 목록 상세 클릭을 추가한다 2026-07-09 07:44:15 +09:00
klaus da26b65a73 feat(creator-channel): FanTalk 상세 화면을 추가한다 2026-07-09 07:44:06 +09:00
klaus 04dcb3f09f feat(creator-channel): FanTalk 상세 상태를 구현한다 2026-07-09 07:43:59 +09:00
klaus 6916459c9e feat(creator-channel): FanTalk 상세 모델을 추가한다 2026-07-09 07:43:53 +09:00
klaus 643fb9fa8e feat(creator-channel): FanTalk 답글 API 응답을 연결한다 2026-07-09 07:43:47 +09:00
klaus f9160ccb02 docs(creator-channel): FanTalk 상세 작업 계획을 기록한다 2026-07-09 07:43:40 +09:00
klaus e2b166de11 docs(creator-channel): FanTalk 상세 요구사항을 기록한다 2026-07-09 07:43:31 +09:00
klaus 89f3511864 test(creator-channel): 댓글 답글 후속 계약을 검증한다 2026-07-09 01:28:48 +09:00
klaus 3271c9e142 feat(creator-channel): 더보기 메뉴 폰트를 고정한다 2026-07-09 01:28:42 +09:00
klaus 1f1069b05d feat(creator-channel): 답글 수정 입력 모드를 구현한다 2026-07-09 01:28:35 +09:00
klaus a325a1bbe9 feat(creator-channel): 댓글 수정 입력 모드를 구현한다 2026-07-09 01:28:29 +09:00
klaus 255e6dc67d feat(creator-channel): 더보기 메뉴 문구를 정리한다 2026-07-09 01:28:24 +09:00
klaus b2685959ad docs(creator-channel): 댓글 답글 후속 요구를 기록한다 2026-07-09 01:28:18 +09:00
klaus 536effab40 test(creator-channel): 커뮤니티 상세 UI 계약을 검증한다 2026-07-09 00:54:12 +09:00
klaus 81537b1e96 feat(creator-channel): 커뮤니티 목록 클릭을 연결한다 2026-07-09 00:54:06 +09:00
klaus e691f78e2e feat(creator-channel): 커뮤니티 상세 진입점을 연결한다 2026-07-09 00:53:59 +09:00
klaus 6125b9f7d1 feat(creator-channel): 커뮤니티 답글 화면을 구현한다 2026-07-09 00:53:48 +09:00
klaus d10373d265 feat(creator-channel): 커뮤니티 상세 화면을 구현한다 2026-07-09 00:53:41 +09:00
klaus 5c1eb6204a feat(creator-channel): 커뮤니티 전송 아이콘을 추가한다 2026-07-09 00:53:35 +09:00
klaus c3884ea917 feat(creator-channel): 커뮤니티 답글 화면 리소스를 추가한다 2026-07-09 00:53:30 +09:00
klaus bc3fa9546b feat(creator-channel): 커뮤니티 상세 화면 리소스를 추가한다 2026-07-09 00:53:24 +09:00
klaus ea66b6da1e fix(common): UTC 상대시간 파싱을 보완한다 2026-07-09 00:53:17 +09:00
klaus c9e47214ea feat(creator-channel): 커뮤니티 답글 상태 관리를 추가한다 2026-07-09 00:53:12 +09:00
klaus c04d603ac0 feat(creator-channel): 커뮤니티 상세 상태 관리를 추가한다 2026-07-09 00:53:06 +09:00
klaus 9e524d626a feat(creator-channel): 커뮤니티 상세 조회 계약을 추가한다 2026-07-09 00:52:59 +09:00
klaus e0dc0c2cb7 docs(creator-channel): 커뮤니티 상세 요구사항을 기록한다 2026-07-09 00:52:49 +09:00
klaus cbd4f04f1b docs(creator-channel): 후원 카드 공통화 작업을 기록한다 2026-07-08 18:44:15 +09:00
klaus eeb777fa01 test(donation): 후원 header 색상 경계를 검증한다 2026-07-08 18:44:09 +09:00
klaus 9272db913c test(creator-channel): 후원 카드 바인더 계약을 검증한다 2026-07-08 18:43:56 +09:00
klaus ff4c0104c5 refactor(creator-channel): 후원 카드 바인딩을 공통화한다 2026-07-08 18:43:47 +09:00
klaus 31ce0629d2 docs(creator-channel): 후원 header 색상 검증을 기록한다 2026-07-08 16:33:01 +09:00
klaus 6ab7878221 fix(creator-channel): 홈 후원 고액 header 색상을 보정한다 2026-07-08 16:32:54 +09:00
klaus ddc18386fb fix(donation): 후원 내역 고액 header 색상을 보정한다 2026-07-08 16:32:46 +09:00
klaus 809ed5ce23 refactor(modal): V2 확인 모달 사용처를 교체한다 2026-07-08 16:04:01 +09:00
klaus 8e086c4888 feat(modal): V2 공통 모달창을 추가한다 2026-07-08 14:20:27 +09:00
klaus bc6443e5bd docs(modal): V2 공통 모달창 계획을 기록한다 2026-07-08 14:20:20 +09:00
klaus 8d205690b3 feat(creator-channel): FanTalk 글쓰기 진입점을 연결한다 2026-07-08 03:44:54 +09:00
klaus 25af312d91 feat(creator-channel): FanTalk 글 입력 화면을 연결한다 2026-07-08 03:44:44 +09:00
klaus b020b8096b chore(creator-channel): FanTalk 글 입력 다국어 문구를 추가한다 2026-07-08 03:44:36 +09:00
klaus 87f6bec138 feat(creator-channel): FanTalk 글 입력 기본 리소스를 추가한다 2026-07-08 03:44:31 +09:00
klaus 34266abd90 feat(creator-channel): FanTalk 글 입력 상태 관리를 추가한다 2026-07-08 03:44:22 +09:00
klaus e8e8a63d6d docs(creator-channel): FanTalk 글 입력 계획을 기록한다 2026-07-08 03:44:15 +09:00
klaus d61867af4e docs(creator-channel): FanTalk 글 입력 PRD를 기록한다 2026-07-08 03:44:09 +09:00
klaus daeb0f75a2 fix(home): 영문 요일 약어 표기를 보완한다 2026-07-06 14:19:07 +09:00
klaus 6c497ed1d2 docs(main): 오디오 미니 플레이어 작업을 기록한다 2026-07-06 14:00:48 +09:00
klaus 7b975ec54d fix(audio): 오디오 알림 route extra를 전달한다 2026-07-06 14:00:34 +09:00
klaus e3d81792bf fix(main): 오디오 알림 이동과 미니 플레이어를 연결한다 2026-07-06 14:00:23 +09:00
klaus 5b9b7eafa7 docs(home): 메인 홈 추천 탭 지금 라이브 중 입장 작업을 기록한다 2026-07-06 02:35:04 +09:00
klaus 5489c86cdc fix(home): 메인 홈 추천 탭 지금 라이브 중 입장을 연결한다 2026-07-06 02:34:41 +09:00
klaus b1f605fda4 docs(home): On Air 라이브 아이템 작업을 기록한다 2026-07-03 14:47:44 +09:00
klaus f9030ffd13 fix(home): On Air 라이브 아이템 UI를 보완한다 2026-07-03 14:47:27 +09:00
klaus f74f5a534d docs(home): 팔로잉 최근 소식 구현 검증을 기록한다 2026-06-30 23:54:21 +09:00
klaus 3f3bacebd9 fix(home): 팔로잉 최근 소식 nested payload를 반영한다 2026-06-30 23:54:10 +09:00
klaus 2041036530 test(home): 팔로잉 최근 소식 nested payload 검증을 추가한다 2026-06-30 23:53:58 +09:00
klaus 81a2b541a0 docs(home): 팔로잉 최근 소식 응답 변경을 정의한다 2026-06-30 23:53:47 +09:00
klaus e6cb94179b docs(home): 팔로잉 크리에이터 전체 버튼을 기록한다 2026-06-30 17:43:29 +09:00
klaus 658b4f0a9d fix(home): 팔로잉 크리에이터 전체 이동을 연결한다 2026-06-30 17:43:14 +09:00
klaus 05cbbdfddc docs(main): title bar 추가 범위를 기록한다 2026-06-30 16:12:52 +09:00
klaus 3c66ea986f fix(main): 콘텐츠와 대화 title bar 이동을 연결한다 2026-06-30 16:12:16 +09:00
klaus d1a0cf27a7 fix(home): title bar 메뉴 이동을 연결한다 2026-06-30 15:46:55 +09:00
klaus 1aef4b1d89 docs(home): title bar 메뉴 액션 계획을 기록한다 2026-06-30 15:46:36 +09:00
klaus c745481ab2 docs(home): 팔로잉 최근 소식 검증을 기록한다 2026-06-30 15:04:26 +09:00
klaus 7e281a15a5 fix(home): 팔로잉 최근 소식 표시를 보완한다 2026-06-30 15:04:14 +09:00
klaus c5e94bbc92 feat(feed): 콘텐츠 카테고리 타입을 보완한다 2026-06-30 15:04:04 +09:00
klaus 61fe8a9fee fix(feed): 콘텐츠 카드 레이아웃을 보완한다 2026-06-30 15:03:50 +09:00
klaus 926e7eabd3 chore(creator-ranking): 기존 카드 뷰를 제거한다 2026-06-30 02:44:36 +09:00
klaus 7c7aa0f442 refactor(creator-ranking): 어댑터 뷰 타입을 정리한다 2026-06-30 02:44:19 +09:00
klaus 86be8e4af1 refactor(creator-ranking): 하위 행 뷰를 추가한다 2026-06-30 02:44:04 +09:00
klaus d5cc5df704 refactor(creator-ranking): 상위 카드 뷰를 추가한다 2026-06-30 02:43:54 +09:00
klaus aac09c14bc refactor(creator-ranking): 배치 모델을 단순화한다 2026-06-30 02:43:39 +09:00
klaus 566f76df6a docs(test): UI 테스트 기준을 보완한다 2026-06-30 02:43:10 +09:00
klaus 3ce5cd0e3b docs(creator-ranking): 뷰 재작성 작업을 기록한다 2026-06-30 02:42:56 +09:00
klaus f3f175fcc6 docs(creator-ranking): 뷰 재작성 PRD를 보완한다 2026-06-30 02:42:46 +09:00
klaus 16995e5a0f fix(content): 콘텐츠 랭킹 순위 표시를 보완한다 2026-06-30 02:42:31 +09:00
klaus 43c8762df7 docs(content): 콘텐츠 랭킹 작업 기록을 추가한다 2026-06-30 02:40:42 +09:00
klaus f27668e038 docs(content): 콘텐츠 랭킹 PRD를 보완한다 2026-06-30 02:40:31 +09:00
klaus 3e14cf4320 test(content): FIRST 오디오 전체보기 테스트를 정리한다 2026-06-29 17:40:39 +09:00
klaus 78573d0e81 fix(content): FIRST 오디오 전체보기 타입을 제거한다 2026-06-29 17:40:16 +09:00
klaus bc5814b7c5 test(home): FIRST 오디오 홈 테스트를 정리한다 2026-06-29 17:40:00 +09:00
klaus ed2e478291 chore(home): FIRST 오디오 문구 리소스를 제거한다 2026-06-29 17:39:25 +09:00
klaus 3ba46916b1 fix(home): FIRST 오디오 섹션 UI를 제거한다 2026-06-29 17:39:14 +09:00
klaus 9d2ccd975f fix(home): FIRST 오디오 응답 매핑을 제거한다 2026-06-29 17:38:58 +09:00
klaus acbf9bb013 docs(content): FIRST 오디오 전체보기 제거를 기록한다 2026-06-29 17:38:35 +09:00
klaus 9fce374842 docs(home): FIRST 오디오 제거 요구사항을 기록한다 2026-06-29 17:38:10 +09:00
klaus 08c7bcf4f2 fix(home): 최근 데뷔 카드 clipping을 보완한다 2026-06-29 16:41:32 +09:00
klaus 93a5d1293c docs(home): 최근 데뷔 요구사항을 기록한다 2026-06-29 16:41:15 +09:00
klaus 5578d59e1f feat(release): release 서명 설정을 추가한다 2026-06-29 16:40:58 +09:00
klaus f1e3b5a49e docs(release): release 서명 요구사항을 기록한다 2026-06-29 16:40:18 +09:00
klaus b9d474075f versionCode 239, versionName 1.55.0 2026-06-27 09:47:53 +09:00
klaus 37200773aa docs(content): Phase 8 검증을 기록한다 2026-06-27 09:20:49 +09:00
klaus bbe9eca97b docs(content): Phase 7 검증을 기록한다 2026-06-27 09:04:49 +09:00
klaus 77a201e139 feat(content): New&Hot 전체보기 이동을 연결한다 2026-06-27 09:04:41 +09:00
klaus 4c7887055d feat(home): FIRST 오디오 전체보기 이동을 연결한다 2026-06-27 09:04:31 +09:00
klaus 0d7512fee2 docs(content): 전체보기 구현 검증을 기록한다 2026-06-27 08:30:22 +09:00
klaus 0da2a3f85f chore(content): 미사용 전체보기 아이콘을 제거한다 2026-06-27 08:30:14 +09:00
klaus 9ab5101c48 feat(content): 전체보기 화면 등록을 추가한다 2026-06-27 08:30:06 +09:00
klaus 0906a21c6d feat(content): 전체보기 화면 UI를 추가한다 2026-06-27 08:29:58 +09:00
klaus 0134942172 feat(content): 전체보기 ViewModel을 추가한다 2026-06-27 08:29:44 +09:00
klaus 0a4b88fabe feat(content): 전체보기 UI 모델 매핑을 추가한다 2026-06-27 08:29:19 +09:00
klaus b772e5416e feat(content): 전체보기 API 계층을 추가한다 2026-06-27 08:29:07 +09:00
klaus 1416db105e docs(content): 전체 탭 이동 검증을 기록한다 2026-06-27 06:32:49 +09:00
klaus b6cc37fe50 feat(content): 추천 섹션 전체 탭 이동을 연결한다 2026-06-27 06:32:41 +09:00
klaus 43e90ed47b feat(content): 전체 탭 외부 선택을 추가한다 2026-06-27 06:32:31 +09:00
klaus 5099795d29 docs(content): chevron 표시 검증을 기록한다 2026-06-27 06:02:48 +09:00
klaus 73dc939cd3 feat(content): 추천 섹션 chevron 표시를 적용한다 2026-06-27 06:02:38 +09:00
klaus 1bd0f369ee test(home): FIRST 오디오 chevron 검증을 추가한다 2026-06-27 06:02:26 +09:00
klaus 8da39949e5 docs(content): 콘텐츠 전체보기 문서를 정리한다 2026-06-27 05:29:35 +09:00
klaus 0454980fae fix(main): 대화 탭 로그인 가드를 적용한다 2026-06-27 04:47:57 +09:00
klaus 0679dc7deb docs(channel): 탭 전환 스크롤 요구사항을 기록한다 2026-06-27 04:28:44 +09:00
klaus 0e07ed0b5a fix(channel): 탭 전환 자동 스크롤을 제거한다 2026-06-27 04:28:35 +09:00
klaus 1e073e85a1 docs(main): 로그인 가드 후속 진행을 기록한다 2026-06-27 03:55:36 +09:00
klaus 4a45f9699c test(home): 팔로잉 테스트 상태를 격리한다 2026-06-27 03:55:10 +09:00
klaus a91ec763e1 feat(content): 콘텐츠 이동 가드를 보강한다 2026-06-27 03:54:45 +09:00
klaus a0cfa70551 feat(home): 팔로잉 탭 로그인 가드를 보강한다 2026-06-27 03:54:21 +09:00
klaus 39dd003b4e docs(main): 로그인 가드 진행을 기록한다 2026-06-27 02:35:45 +09:00
klaus 6299259f89 feat(main): 로그인 가드 헬퍼를 추가한다 2026-06-27 02:35:37 +09:00
klaus 12ea65f14f docs(main): 로그인 성인 콘텐츠 가드 문서를 갱신한다 2026-06-27 01:59:41 +09:00
klaus cc1e41e7b4 docs(live): 온에어 라이브 최종 검증을 기록한다 2026-06-27 01:27:55 +09:00
klaus 0c1ad5c746 docs(live): 온에어 라이브 화면 구현을 기록한다 2026-06-27 01:11:05 +09:00
klaus 7c775efada feat(home): 온에어 라이브 더보기를 연결한다 2026-06-27 01:10:34 +09:00
klaus ee2f5572fc feat(live): 온에어 라이브 화면을 연결한다 2026-06-27 01:10:27 +09:00
klaus d16f0928bb feat(live): 온에어 라이브 목록 어댑터를 추가한다 2026-06-27 01:10:20 +09:00
klaus 1c2fcdd1b4 feat(live): 온에어 라이브 화면 리소스를 추가한다 2026-06-27 01:10:11 +09:00
klaus 541771d72c test(live): 온에어 입장 가능 정책을 검증한다 2026-06-27 01:10:02 +09:00
klaus 2ad59dfd96 docs(live): 온에어 라이브 구현 검증을 기록한다 2026-06-26 23:43:26 +09:00
klaus d3454cc293 feat(live): 온에어 라이브 ViewModel을 연결한다 2026-06-26 23:43:16 +09:00
klaus c3377e39e6 feat(live): 온에어 라이브 매핑을 추가한다 2026-06-26 23:43:05 +09:00
klaus 6e04a10a3b feat(live): 온에어 라이브 API를 추가한다 2026-06-26 23:42:55 +09:00
klaus e13da00215 feat(live): 인증 헤더 헬퍼를 추가한다 2026-06-26 23:42:41 +09:00
klaus 0943237b5f docs(live): 현재 진행 라이브 리스트 문서를 추가한다 2026-06-26 22:54:34 +09:00
klaus 66116351c2 docs(home): 팔로잉 탭 후속 검증을 기록한다 2026-06-26 15:43:30 +09:00
klaus 68ba58a73c test(home): 팔로잉 탭 후속 UI 검증을 보강한다 2026-06-26 15:43:03 +09:00
klaus 595bc50cde fix(home): 팔로잉 탭 후속 UI를 보정한다 2026-06-26 15:42:38 +09:00
klaus 50b45d04e5 docs(home): 팔로잉 탭 UI 검증을 기록한다 2026-06-25 23:34:32 +09:00
klaus 01d0367f50 feat(home): 팔로잉 탭 화면을 연결한다 2026-06-25 23:34:24 +09:00
klaus f009a2edb8 feat(home): 팔로잉 소식 카드를 추가한다 2026-06-25 23:34:11 +09:00
klaus ccd2398a7c feat(home): 팔로잉 스케줄 카드를 추가한다 2026-06-25 23:34:02 +09:00
klaus 8f5012fe2a feat(home): 팔로잉 라이브 카드를 추가한다 2026-06-25 23:33:51 +09:00
klaus 34ac433247 feat(home): 팔로잉 크리에이터와 채팅 카드를 추가한다 2026-06-25 23:33:40 +09:00
klaus 11b06d8f9c feat(home): 팔로잉 탭 DI를 등록한다 2026-06-25 22:22:41 +09:00
klaus 2128bbf197 feat(home): 팔로잉 ViewModel을 추가한다 2026-06-25 22:22:32 +09:00
klaus 502bbd4f35 feat(home): 팔로잉 응답 UI 매핑을 추가한다 2026-06-25 22:22:22 +09:00
klaus 21d3ed4603 feat(home): 팔로잉 인증 헤더를 추가한다 2026-06-25 22:22:11 +09:00
klaus 2d9200ec4d feat(home): 팔로잉 탭 문자열을 추가한다 2026-06-25 22:21:58 +09:00
klaus a088811b2f feat(home): 팔로잉 탭 API 계약을 추가한다 2026-06-25 22:21:46 +09:00
klaus e331a7e072 docs(home): 팔로잉 탭 요구와 계획을 기록한다 2026-06-25 22:21:33 +09:00
klaus bb00dd13b0 docs(content): original tag 이미지 교체를 기록한다 2026-06-25 19:11:30 +09:00
klaus 7748431330 fix(content): original tag 이미지 표시를 보정한다 2026-06-25 19:11:08 +09:00
klaus 8c5c1dce53 docs(content): 전체 탭 grid 검증을 기록한다 2026-06-25 18:31:46 +09:00
klaus 136fdced17 fix(content): 전체 탭 grid 폭을 주입한다 2026-06-25 18:31:39 +09:00
klaus 78e0a53018 fix(content): 콘텐츠 카드 동적 폭을 추가한다 2026-06-25 18:31:26 +09:00
klaus 15de359d4f docs(content): 전체 탭 grid 폭 요구를 기록한다 2026-06-25 18:31:15 +09:00
klaus c030cbabd3 fix(content): 랭킹 카드 간격과 이미지를 보정한다 2026-06-25 18:30:55 +09:00
klaus ab9d14598c docs(content): 랭킹 카드 후속 요구를 기록한다 2026-06-25 18:29:58 +09:00
klaus 71b0c64fd2 fix(creator): 소개 본문 타이포그래피를 보정한다 2026-06-25 18:29:40 +09:00
klaus 2feacd9416 docs(creator): 소개 타이포그래피 요구를 기록한다 2026-06-25 18:29:28 +09:00
klaus f8a5e4c44f docs(content): 크리에이터 랭킹 rank 보정을 기록한다 2026-06-25 15:59:39 +09:00
klaus c3f576dd5f docs(content): 콘텐츠 랭킹 rank 보정 PRD를 기록한다 2026-06-25 15:59:05 +09:00
klaus 102c454f9c docs(content): 콘텐츠 랭킹 rank 보정 계획을 기록한다 2026-06-25 15:58:42 +09:00
klaus 84b5ab4279 fix(content): 크리에이터 랭킹 rank 간격을 보정한다 2026-06-25 15:58:23 +09:00
klaus 11853761cf fix(content): 콘텐츠 랭킹 rank 간격을 보정한다 2026-06-25 15:57:53 +09:00
klaus 2e86e21cb7 docs(content): 전체 탭 spacing 검증을 기록한다 2026-06-25 12:23:29 +09:00
klaus 7436ac77d9 docs(content): 추천 탭 spacing 검증을 기록한다 2026-06-25 12:23:20 +09:00
klaus 00aac29d89 fix(content): 전체 탭 grid 간격을 보정한다 2026-06-25 12:23:12 +09:00
klaus 33e137bc22 docs(content): 전체 탭 Phase 4-6 검증을 기록한다 2026-06-25 11:54:58 +09:00
klaus f4a32086b0 feat(content): 전체 탭 화면 연결을 추가한다 2026-06-25 11:54:43 +09:00
klaus 4df724ee56 feat(content): 전체 탭 화면 레이아웃을 추가한다 2026-06-25 11:54:32 +09:00
klaus c1a21985fd feat(content): 전체 탭 카드 어댑터를 추가한다 2026-06-25 11:54:05 +09:00
klaus 0a97194b31 feat(content): 전체 탭 ViewModel을 추가한다 2026-06-25 11:53:51 +09:00
klaus 8150b680c1 docs(content): 전체 탭 Phase 1-3 검증을 기록한다 2026-06-25 01:55:45 +09:00
klaus cdc847dcca feat(content): 시리즈 성인 배지를 추가한다 2026-06-25 01:55:16 +09:00
klaus af895ed510 feat(content): 전체 탭 UI 매핑을 추가한다 2026-06-25 01:55:05 +09:00
klaus ee74519f6f feat(content): 전체 탭 요일 매핑을 추가한다 2026-06-25 01:54:52 +09:00
klaus d47e90d340 feat(content): 전체 탭 문자열을 추가한다 2026-06-25 01:54:09 +09:00
klaus b53853f193 feat(content): 전체 탭 DI를 등록한다 2026-06-25 01:54:01 +09:00
klaus b69a713641 feat(content): 전체 탭 API 계약을 추가한다 2026-06-25 01:53:53 +09:00
klaus 191b64d96e docs(content): 전체 탭 구현 문서를 추가한다 2026-06-25 01:09:20 +09:00
klaus a747729c26 docs(content): 랭킹 Phase 6 검증을 기록한다 2026-06-24 15:41:03 +09:00
klaus 8ba3ed5fb5 fix(content): 랭킹 상세 이동 guard를 보강한다 2026-06-24 15:40:14 +09:00
klaus a9446ef5cc docs(content): 랭킹 탭 Phase 4-5 검증을 기록한다 2026-06-24 14:45:53 +09:00
klaus 2818f8d4a4 feat(content): 랭킹 탭 화면 연결을 추가한다 2026-06-24 14:45:45 +09:00
klaus cf89052806 feat(content): 랭킹 탭 레이아웃을 추가한다 2026-06-24 14:45:34 +09:00
klaus fcd445666e feat(content): 랭킹 탭 문자열을 추가한다 2026-06-24 14:45:27 +09:00
klaus 3599197f01 feat(content): 랭킹 ViewModel DI를 등록한다 2026-06-24 14:45:19 +09:00
klaus f4e46f9d20 feat(content): 랭킹 ViewModel을 추가한다 2026-06-24 14:45:10 +09:00
klaus f2996f599a feat(content): 랭킹 응답 매핑을 추가한다 2026-06-24 13:35:28 +09:00
klaus 0d0bec1904 feat(content): 랭킹 API 계약을 추가한다 2026-06-24 13:35:20 +09:00
klaus 8bc1ec5830 feat(content): 랭킹 변동 숨김 처리를 추가한다 2026-06-24 13:35:03 +09:00
klaus b858003b6d feat(content): 랭킹 변동 표시 옵션을 추가한다 2026-06-24 13:34:53 +09:00
klaus 37b70a956c docs(content): 콘텐츠 랭킹 탭 계획을 추가한다 2026-06-24 13:34:44 +09:00
klaus 6590cf8300 docs(content): 추천 Phase 7 검증을 기록한다 2026-06-23 17:33:49 +09:00
klaus 8d33b90e67 docs(content): 추천 Phase 4-6 검증을 기록한다 2026-06-23 17:18:51 +09:00
klaus e4d650c3e7 feat(content): 추천 API 상태를 화면에 바인딩한다 2026-06-23 17:18:39 +09:00
klaus 1a45f42f9e feat(content): 추천 시리즈 어댑터를 추가한다 2026-06-23 17:18:20 +09:00
klaus 55255621e3 feat(content): 추천 댓글 오디오 어댑터를 추가한다 2026-06-23 17:18:05 +09:00
klaus cd323c3f69 feat(content): 추천 NewAndHot 어댑터를 추가한다 2026-06-23 17:17:48 +09:00
klaus 27add8b244 feat(content): 추천 공통 오디오 카드를 추가한다 2026-06-23 17:17:33 +09:00
klaus 0ea2a10554 feat(content): 추천 배너 경로를 연결한다 2026-06-23 17:17:17 +09:00
klaus bc4f565074 docs(content): 추천 탭 Phase 1-3 검증을 기록한다 2026-06-23 15:51:51 +09:00
klaus de03d2ff76 feat(content): 추천 Fragment 연결을 추가한다 2026-06-23 15:51:39 +09:00
klaus 315df46b7e feat(content): 추천 화면 레이아웃을 추가한다 2026-06-23 15:51:23 +09:00
klaus 4117afa9e4 feat(content): 추천 화면 문자열을 추가한다 2026-06-23 15:50:57 +09:00
klaus dd742ce245 feat(content): 추천 ViewModel DI를 추가한다 2026-06-23 15:50:34 +09:00
klaus c02437797c feat(content): 추천 UI 모델 매핑을 추가한다 2026-06-23 15:50:24 +09:00
klaus 5746239873 feat(content): 추천 API 계약을 추가한다 2026-06-23 15:50:11 +09:00
klaus 62a11023fb feat(content): 보관함 바 아이콘을 추가한다 2026-06-23 14:37:23 +09:00
klaus d98fdf2e98 docs(content): 메인 콘텐츠 추천 탭 문서를 추가한다 2026-06-23 14:36:58 +09:00
klaus f8e916d280 docs(home): AI 캐릭터 이동 검증을 기록한다 2026-06-23 11:34:24 +09:00
klaus b52e7b5430 test(creator): FanTalk empty 버튼 검증 줄바꿈을 정리한다 2026-06-23 11:34:05 +09:00
klaus 51b50eed75 feat(home): AI 캐릭터 크리에이터 이동을 연결한다 2026-06-23 11:33:35 +09:00
klaus 74efe45d05 docs(creator): FanTalk empty 보정 검증을 기록한다 2026-06-23 00:16:12 +09:00
klaus 6ebd59ff9c feat(creator): FanTalk empty 위치를 보정한다 2026-06-23 00:16:00 +09:00
klaus 7e1e453a6b docs(creator): 후원 empty 후속 검증을 기록한다 2026-06-22 23:55:56 +09:00
klaus c25acea5d4 feat(creator): 후원 empty 표시 위치를 보정한다 2026-06-22 23:55:50 +09:00
klaus 7a705b4355 feat(creator): 후원 empty 아이템을 추가한다 2026-06-22 23:55:43 +09:00
klaus 933e118c36 feat(creator): 후원 empty 랭킹 상태를 유지한다 2026-06-22 23:55:36 +09:00
klaus 092dff2a9d docs(creator): 후원 탭 후속 검증을 기록한다 2026-06-22 23:22:45 +09:00
klaus fe19552741 test(creator): 후원 성공 테스트 기대값을 보정한다 2026-06-22 23:22:40 +09:00
klaus 0122c8c5ed feat(creator): 후원 랭킹 레이아웃을 보정한다 2026-06-22 23:22:34 +09:00
klaus 9b19be7775 feat(creator): 후원 floating button 위치를 조정한다 2026-06-22 23:22:26 +09:00
klaus 4d79cb65cd docs(creator): 후원 탭 후속 검증을 기록한다 2026-06-22 22:07:17 +09:00
klaus 8db913812d feat(creator): 후원 탭 Activity 연결을 추가한다 2026-06-22 22:07:12 +09:00
klaus a7ce991f7d feat(creator): 후원 탭 어댑터 연결을 추가한다 2026-06-22 22:07:06 +09:00
klaus ba6616c81a fix(creator): 후원 empty 실패 메시지를 표시한다 2026-06-22 22:06:58 +09:00
klaus 4097181923 feat(creator): 후원 empty 액션을 연결한다 2026-06-22 22:06:52 +09:00
klaus ecaa1f01e8 feat(creator): 후원 empty 레이아웃을 보정한다 2026-06-22 22:06:46 +09:00
klaus a3ea2051cb docs(creator): 후원 탭 구현 검증을 기록한다 2026-06-22 21:24:50 +09:00
klaus 5ca5da45ba feat(creator): 후원 탭 화면을 구현한다 2026-06-22 21:24:43 +09:00
klaus 77d889d9ab feat(creator): 후원 탭 어댑터를 추가한다 2026-06-22 21:24:35 +09:00
klaus 6c8d3dfc76 feat(creator): 후원 탭 레이아웃을 추가한다 2026-06-22 21:24:09 +09:00
klaus bb8442a32d feat(creator): 후원 탭 문자열을 추가한다 2026-06-22 21:24:00 +09:00
klaus 32504349cd feat(creator): 후원 탭 상태 관리를 추가한다 2026-06-22 21:23:52 +09:00
klaus 0344518130 feat(creator): 후원 탭 UI 모델 매핑을 추가한다 2026-06-22 21:23:42 +09:00
klaus eb71034365 feat(creator): 후원 탭 API 계약을 추가한다 2026-06-22 21:21:59 +09:00
klaus 5a9449059b docs(creator): 후원 탭 Phase 1 검증을 기록한다 2026-06-22 19:16:33 +09:00
klaus 3a94878020 docs(creator): 크리에이터 채널 후원 탭 문서를 추가한다 2026-06-22 18:38:00 +09:00
klaus f6190030a4 docs(creator): FanTalk 탭 Phase 6 검증을 기록한다 2026-06-22 18:02:22 +09:00
klaus b04c01c930 docs(creator): FanTalk 탭 Phase 4와 5 검증을 기록한다 2026-06-22 17:46:41 +09:00
klaus 40ef5710fb feat(creator): FanTalk 탭 화면 연결을 추가한다 2026-06-22 17:46:25 +09:00
klaus 790e08f1b5 feat(creator): FanTalk 더보기 팝업을 추가한다 2026-06-22 17:45:36 +09:00
klaus 270fe32d94 feat(creator): FanTalk 목록 아이템을 추가한다 2026-06-22 17:45:20 +09:00
klaus c87a6878b5 feat(creator): FanTalk 목록 레이아웃을 추가한다 2026-06-22 17:44:56 +09:00
klaus c4906bd0b5 feat(creator): FanTalk 탭 문자열을 추가한다 2026-06-22 17:43:06 +09:00
klaus 4012b92357 docs(creator): FanTalk 탭 구현 검증을 기록한다 2026-06-22 16:38:15 +09:00
klaus 50449f43ac feat(creator): FanTalk 탭 상태 관리를 추가한다 2026-06-22 16:38:08 +09:00
klaus b7eba4c99a feat(creator): FanTalk 탭 UI 모델 매핑을 추가한다 2026-06-22 16:38:00 +09:00
klaus 25320e283d feat(creator): FanTalk 탭 API 계약을 추가한다 2026-06-22 16:37:53 +09:00
klaus 6517e739b3 docs(creator): FanTalk 탭 후속 검증을 기록한다 2026-06-22 15:44:28 +09:00
klaus 4f7dd97be6 docs(creator): FanTalk 탭 문서를 추가한다 2026-06-22 15:18:58 +09:00
klaus 7ddfbb0a18 docs(creator): 커뮤니티 탭 검증 결과를 기록한다 2026-06-22 14:41:52 +09:00
klaus 2d41b07852 docs(creator): 커뮤니티 탭 후속 검증을 기록한다 2026-06-22 14:28:26 +09:00
klaus 7958547b28 docs(creator): 커뮤니티 탭 요구사항을 보정한다 2026-06-22 14:28:15 +09:00
klaus b0346ae00c fix(creator): 커뮤니티 리스트 표시를 보정한다 2026-06-22 14:28:08 +09:00
klaus 8f5c55e0d1 fix(creator): 커뮤니티 썸네일 그리드를 보정한다 2026-06-22 14:28:02 +09:00
klaus 4288e7284b fix(creator): 커뮤니티 탭 하단 재평가를 보정한다 2026-06-22 14:27:53 +09:00
klaus cb5d4f954d docs(creator): 커뮤니티 탭 Phase 5 검증을 기록한다 2026-06-22 01:44:34 +09:00
klaus a36c3b74e8 feat(creator): 커뮤니티 탭 activity 동작을 연결한다 2026-06-22 01:44:29 +09:00
klaus e29ae4fedb feat(creator): 커뮤니티 탭 pager 연결을 추가한다 2026-06-22 01:44:20 +09:00
klaus 74c11f2aa6 docs(repo): 레거시 코드 원칙을 추가한다 2026-06-22 01:44:14 +09:00
klaus 3e4c00fee8 test(creator): 커뮤니티 탭 레이아웃 검증을 추가한다 2026-06-22 00:36:49 +09:00
klaus 318944fbfe fix(creator): 커뮤니티 게시글 표시 정책을 보정한다 2026-06-22 00:36:42 +09:00
klaus 7ccc676192 feat(creator): 커뮤니티 탭 화면을 구현한다 2026-06-22 00:36:34 +09:00
klaus 2b21e07571 feat(creator): 커뮤니티 탭 어댑터를 추가한다 2026-06-22 00:36:27 +09:00
klaus 6ae28e4d84 feat(creator): 커뮤니티 탭 레이아웃을 추가한다 2026-06-22 00:36:21 +09:00
klaus d6b49eb3e8 docs(creator): 커뮤니티 탭 Phase 3 검증을 기록한다 2026-06-21 22:32:47 +09:00
klaus 88fcbe49f4 feat(creator): 커뮤니티 상태에 UI 모델을 적용한다 2026-06-21 22:32:33 +09:00
klaus f9501c156a feat(creator): 커뮤니티 게시글 UI 모델을 추가한다 2026-06-21 22:32:23 +09:00
klaus efe12774f7 feat(creator): 커뮤니티 보기 방식 문자열을 추가한다 2026-06-21 22:31:54 +09:00
klaus 9b5bcbe41e docs(creator): 커뮤니티 탭 Phase 2 검증을 기록한다 2026-06-21 20:44:38 +09:00
klaus d4448820d6 feat(creator): 커뮤니티 탭 상태 관리를 추가한다 2026-06-21 20:44:27 +09:00
klaus 744132fd7e feat(creator): 커뮤니티 탭 API 계약을 추가한다 2026-06-21 20:44:15 +09:00
klaus a444dd8677 docs(creator): 채널 커뮤니티 탭 사전 확인을 기록한다 2026-06-21 19:35:09 +09:00
klaus 546a665ba3 feat(creator): 채널 커뮤니티 탭 아이콘을 추가한다 2026-06-21 19:28:36 +09:00
klaus 6bff74cd1e docs(creator): 채널 커뮤니티 탭 문서를 추가한다 2026-06-21 19:28:30 +09:00
klaus 1dc6aa5283 docs(creator): 시리즈 탭 최종 검증을 기록한다 2026-06-20 05:42:56 +09:00
klaus a2dba0456b docs(creator): 시리즈 탭 구현 검증을 기록한다 2026-06-20 04:50:50 +09:00
klaus 015a6ac865 feat(creator): 시리즈 탭 activity 연동을 추가한다 2026-06-20 04:50:43 +09:00
klaus fcf35e2513 feat(creator): 시리즈 탭 pager 연결을 추가한다 2026-06-20 04:50:37 +09:00
klaus 92cea6d3ee feat(creator): 시리즈 탭 UI를 구현한다 2026-06-20 04:50:30 +09:00
klaus 7ea06fda2f feat(creator): 시리즈 탭 문자열을 추가한다 2026-06-20 04:50:22 +09:00
klaus a9456abfb0 refactor(creator): 시리즈 subtitle 모델을 분리한다 2026-06-20 04:49:36 +09:00
klaus 3dcc48c9d9 docs(creator): 시리즈 탭 Phase 2와 3 검증을 기록한다 2026-06-20 02:53:43 +09:00
klaus c25d4cd161 feat(creator): 시리즈 탭 상태 관리를 추가한다 2026-06-20 02:53:24 +09:00
klaus 185c92e9af feat(creator): 시리즈 탭 API 계약을 추가한다 2026-06-20 02:53:16 +09:00
klaus 8fae9e6a96 docs(creator): 시리즈 탭 사전 확인을 기록한다 2026-06-20 02:13:54 +09:00
klaus 8a5fc48650 docs(creator): 시리즈 탭 구현 계획을 기록한다 2026-06-20 02:05:23 +09:00
klaus 688ba0a63d docs(creator): 라이브 오디오 후속 보정을 기록한다 2026-06-19 21:47:25 +09:00
klaus a0f263c1fd fix(creator): 오디오 탭 표시를 보정한다 2026-06-19 21:47:19 +09:00
klaus e3cea856b8 fix(creator): 라이브 empty 배치를 보정한다 2026-06-19 21:47:12 +09:00
klaus df78b8a3f5 fix(creator): 탭 ViewPager 높이 계산을 보정한다 2026-06-19 21:47:06 +09:00
klaus 1a9a78ec70 docs(creator): 오디오 탭 Phase 5와 6 검증을 기록한다 2026-06-19 21:04:20 +09:00
klaus 1f855102ce fix(creator): 오디오 탭 owner reload를 보정한다 2026-06-19 21:04:15 +09:00
klaus 757f242285 feat(creator): 오디오 탭 activity 연동을 추가한다 2026-06-19 21:04:08 +09:00
klaus bcbc48540e feat(creator): 오디오 탭 콘텐츠 UI를 연결한다 2026-06-19 21:04:01 +09:00
klaus 5b89d6c6d7 feat(creator): 오디오 탭 pager 연결을 추가한다 2026-06-19 21:03:55 +09:00
klaus 3a421d2a60 refactor(creator): 라이브 replay adapter를 공통화한다 2026-06-19 21:03:47 +09:00
klaus 9d7bc6969b refactor(creator): 오디오 콘텐츠 공통 모델을 추가한다 2026-06-19 21:03:33 +09:00
klaus e12f00b5b4 feat(creator): 오디오 탭 fragment 골격을 추가한다 2026-06-19 19:13:00 +09:00
klaus 0b2faf2c6e feat(creator): 오디오 탭 레이아웃을 추가한다 2026-06-19 19:12:54 +09:00
klaus 763a86704c test(creator): 라이브 공통 UI 검증을 갱신한다 2026-06-19 19:12:47 +09:00
klaus 6df7666cec refactor(creator): 라이브 replay item을 공통화한다 2026-06-19 19:12:35 +09:00
klaus d97c2792f5 refactor(creator): 라이브 sort popup을 공통 컴포넌트로 전환한다 2026-06-19 19:11:29 +09:00
klaus f848f22029 feat(creator): 채널 sort popup을 공통 컴포넌트로 추가한다 2026-06-19 19:11:12 +09:00
klaus 011a762ecf feat(creator): 채널 sort popup 리소스를 공통화한다 2026-06-19 19:11:05 +09:00
klaus 241532f60e feat(creator): 채널 sort 공통 모델을 추가한다 2026-06-19 19:10:58 +09:00
klaus 6770dbd682 docs(creator): 오디오 탭 공통 UI 재사용 계획을 갱신한다 2026-06-19 17:59:24 +09:00
klaus 12a9d9f398 docs(creator): 오디오 탭 Phase 3 검증을 기록한다 2026-06-19 17:39:51 +09:00
klaus c82513eaf0 fix(creator): 오디오 탭 theme 선택 정규화를 보정한다 2026-06-19 17:39:41 +09:00
klaus 845b36828b feat(creator): 오디오 탭 mapper를 추가한다 2026-06-19 17:39:31 +09:00
klaus d0843d94ed docs(creator): 오디오 탭 Phase 2 검증을 기록한다 2026-06-19 15:41:42 +09:00
klaus c9d911f339 feat(creator): 오디오 탭 상태 관리를 추가한다 2026-06-19 15:41:34 +09:00
klaus 4e4d13b4de feat(creator): 오디오 탭 API 계약을 추가한다 2026-06-19 15:41:25 +09:00
klaus a0274518d2 docs(creator): 오디오 탭 Phase 1 검증을 기록한다 2026-06-19 14:48:01 +09:00
klaus 88dd7cc04c docs(creator): 오디오 탭 구현 계획을 기록한다 2026-06-19 14:26:11 +09:00
klaus dacd3a67a1 docs(dm): DM push deep_link 계약을 기록한다 2026-06-19 05:03:37 +09:00
klaus da64806d88 fix(dm): MainV2Activity chat path 라우팅을 보정한다 2026-06-19 05:03:32 +09:00
klaus a87d2990b0 fix(dm): MainActivity chat path 라우팅을 보정한다 2026-06-19 05:03:25 +09:00
klaus 5e95aa4168 fix(dm): DeepLinkActivity chat path 라우팅을 보정한다 2026-06-19 05:03:20 +09:00
klaus 9c642fb3b7 fix(dm): FCM deep_link payload 알림 생성을 보정한다 2026-06-19 05:03:14 +09:00
klaus 8b515bba97 docs(dm): Phase 13 검증 기록을 갱신한다 2026-06-19 03:57:44 +09:00
klaus 1550014670 docs(dm): Phase 12 검증 기록을 갱신한다 2026-06-19 00:10:12 +09:00
klaus a6862c51e4 test(dm): 제거 endpoint와 음성 범위 회귀를 고정한다 2026-06-19 00:10:05 +09:00
klaus de90b34cd4 fix(dm): USER_CREATOR DM push 라우팅을 보정한다 2026-06-19 00:09:59 +09:00
klaus bbb84d4ffa fix(dm): FCM DM payload 보존을 보정한다 2026-06-19 00:09:51 +09:00
klaus f560adabfa docs(dm): WebSocket lifecycle 검증을 기록한다 2026-06-18 23:31:20 +09:00
klaus 8f69c1ab82 fix(dm): WebSocket heartbeat와 token 재연결을 보정한다 2026-06-18 23:31:15 +09:00
klaus a6485292e4 docs(dm): MESSAGE race 검증을 기록한다 2026-06-18 22:57:48 +09:00
klaus 482d517145 fix(dm): MESSAGE 선도착 ACK 처리를 보정한다 2026-06-18 22:57:36 +09:00
klaus 2c1eb03e5f feat(dm): 메시지 전송 pending을 requestId로 관리한다 2026-06-18 19:09:40 +09:00
klaus e640ee6c46 docs(dm): WebSocket 연결 완료 검증을 기록한다 2026-06-18 18:42:30 +09:00
klaus e8ae5b9639 fix(dm): WebSocket 연결 완료 시점을 보정한다 2026-06-18 18:42:24 +09:00
klaus 32e33a243a docs(dm): WebSocket 후속 검증 계획을 보강한다 2026-06-18 18:26:56 +09:00
klaus 379284ca4f docs(dm): WebSocket 저장소 전환 검증을 기록한다 2026-06-18 18:26:16 +09:00
klaus dd7a6465c1 refactor(dm): 채팅 화면 전송을 WebSocket으로 전환한다 2026-06-18 18:25:43 +09:00
klaus deba733522 refactor(dm): 채팅 저장소를 WebSocket 기준으로 전환한다 2026-06-18 18:25:29 +09:00
klaus 3d71def880 docs(dm): WebSocket 검증 기록을 갱신한다 2026-06-18 17:41:56 +09:00
klaus c5bcaf7329 feat(dm): WebSocket 클라이언트를 추가한다 2026-06-18 17:41:41 +09:00
klaus e76562067f feat(dm): WebSocket 계약 모델을 추가한다 2026-06-18 17:41:31 +09:00
klaus 0e03a1a14a docs(dm): WebSocket 전환 계획을 기록한다 2026-06-18 17:03:07 +09:00
klaus fa4e41589b docs(creator): 라이브 탭 후속 검증을 기록한다 2026-06-18 15:22:17 +09:00
klaus b11cf53f67 fix(creator): 라이브 기본 조회 개수를 보정한다 2026-06-18 15:22:11 +09:00
klaus 7db7fdffdf fix(creator): 라이브 다시듣기 item 표시를 보정한다 2026-06-18 15:21:32 +09:00
klaus efac753f83 fix(creator): 라이브 empty와 CTA 목록 여백을 보정한다 2026-06-18 15:21:26 +09:00
klaus f4af9868e6 fix(creator): 라이브 탭 하단 CTA와 sticky 전환을 보정한다 2026-06-18 15:21:18 +09:00
klaus a1e8f8edb3 docs(creator): 라이브 Phase 7 검증을 기록한다 2026-06-18 11:26:43 +09:00
klaus 7469172cfc docs(creator): 라이브 본인 CTA 검증을 기록한다 2026-06-18 11:13:17 +09:00
klaus a49951c51f feat(creator): 라이브 탭 본인 CTA를 연결한다 2026-06-18 11:12:55 +09:00
klaus 0d11839a96 docs(creator): 라이브 정렬 팝업 검증을 기록한다 2026-06-18 00:12:14 +09:00
klaus 8213d2de42 feat(creator): 라이브 정렬 팝업을 연결한다 2026-06-18 00:11:49 +09:00
klaus decc2cbb31 feat(creator): 라이브 정렬 팝업 UI를 추가한다 2026-06-18 00:11:28 +09:00
klaus b639782e89 docs(creator): 라이브 탭 UI 검증을 기록한다 2026-06-17 23:24:45 +09:00
klaus 10004652e4 fix(creator): 라이브 더보기 상태 보존을 보정한다 2026-06-17 23:24:39 +09:00
klaus e90fb04de9 feat(creator): 라이브 탭 화면을 연결한다 2026-06-17 23:24:34 +09:00
klaus 7fb52b3c85 feat(creator): 라이브 탭 레이아웃을 추가한다 2026-06-17 23:24:13 +09:00
klaus dd13c619ac feat(creator): 라이브 탭 배경 리소스를 추가한다 2026-06-17 23:24:08 +09:00
klaus 41ef04b193 feat(creator): 라이브 탭 상태 문구를 추가한다 2026-06-17 23:23:55 +09:00
klaus 747569c1cc docs(creator): 라이브 다시듣기 mapper 검증을 기록한다 2026-06-17 19:14:03 +09:00
klaus 1f0adb21a7 feat(creator): 라이브 다시듣기 mapper를 추가한다 2026-06-17 19:13:58 +09:00
klaus f015aea062 feat(creator): 라이브 탭 정렬 문구를 추가한다 2026-06-17 19:13:50 +09:00
klaus 3f16cc9312 docs(creator): 라이브 탭 API 계약을 기록한다 2026-06-17 18:47:12 +09:00
klaus c1ac428ded test(creator): 채널 홈 source 검증을 갱신한다 2026-06-17 18:47:03 +09:00
klaus 4a38703873 feat(creator): 라이브 탭 ViewModel을 추가한다 2026-06-17 18:46:57 +09:00
klaus e5d4f1d40d feat(creator): 라이브 탭 API 계약을 추가한다 2026-06-17 18:46:50 +09:00
klaus d25f509118 docs(creator): 채널 공통 저장소 rename을 기록한다 2026-06-17 16:14:19 +09:00
klaus ecaeea6262 refactor(creator): 채널 공통 저장소 이름을 정리한다 2026-06-17 16:13:56 +09:00
klaus cff8469604 docs(creator): 라이브 탭 문서를 갱신한다 2026-06-17 15:54:53 +09:00
klaus 9f242f0201 feat(creator): 라이브 탭 아이콘을 추가한다 2026-06-17 15:44:45 +09:00
klaus bb3c63e2fb docs(creator): 라이브 탭 구현 계획을 추가한다 2026-06-17 15:44:27 +09:00
klaus ca0c586c0c docs(creator): 원본 태그 교체를 기록한다 2026-06-17 14:15:42 +09:00
klaus bd60185db8 feat(creator): 원본 태그 이미지를 적용한다 2026-06-17 14:15:24 +09:00
klaus 01afed89f8 docs(dm): creatorId 진입 crash 수정을 기록한다 2026-06-17 13:52:49 +09:00
klaus 3dacc50e1b fix(creator): 채널 버튼 drawable compat을 적용한다 2026-06-17 13:52:44 +09:00
klaus 236b874e82 fix(dm): creatorId 방 열기 thread를 보정한다 2026-06-17 13:52:38 +09:00
klaus 26f44dd448 docs(creator): 채널 라이브 검증을 기록한다 2026-06-17 10:59:00 +09:00
klaus f2f2a3143d fix(creator): 채널 라이브 진입을 보강한다 2026-06-17 10:58:49 +09:00
klaus 34876cf46f feat(creator): 라이브 생성 안내 문구를 추가한다 2026-06-17 10:58:31 +09:00
klaus deda8322d5 feat(creator): 라이브 coordinator를 추가한다 2026-06-17 10:58:19 +09:00
klaus ba0aef4349 docs(creator): 본인 FAB 검증을 기록한다 2026-06-16 22:24:42 +09:00
klaus 5d52787ea9 feat(creator): 본인 FAB 액션을 연결한다 2026-06-16 22:24:30 +09:00
klaus 6a6b1138a8 feat(creator): 본인 홈 FAB를 추가한다 2026-06-16 22:03:49 +09:00
klaus 722f84039f feat(creator): 본인 FAB 문구를 추가한다 2026-06-16 22:03:41 +09:00
klaus 710ce2602b feat(creator): 본인 FAB 배경을 추가한다 2026-06-16 22:03:35 +09:00
klaus 3e96a5435e docs(creator): 본인 FAB 기준을 갱신한다 2026-06-16 22:03:28 +09:00
klaus 433bf172ce fix(creator): 채널 후원 empty 레이아웃을 보정한다 2026-06-16 21:22:45 +09:00
klaus 5969f50888 fix(creator): 채널 홈 활동 표시를 보정한다 2026-06-16 21:19:03 +09:00
klaus 9900ac02f5 fix(creator): 채널 홈 카드 태그 크기를 보정한다 2026-06-16 21:18:38 +09:00
klaus ee4de78c6c feat(creator): 시리즈 상세 이동을 연결한다 2026-06-16 21:18:26 +09:00
klaus 43c2f6f417 fix(creator): 채널 활동 dday 응답을 매핑한다 2026-06-16 21:18:15 +09:00
klaus f7c1a5168f docs(creator): 채널 홈 후원 검증을 기록한다 2026-06-16 19:22:42 +09:00
klaus 28433c10df feat(creator): 채널 후원 버튼을 연결한다 2026-06-16 19:22:28 +09:00
klaus de351d700c feat(creator): 채널 후원 요청을 연결한다 2026-06-16 19:22:21 +09:00
klaus a01675b592 feat(creator): 채널 후원 empty 상태를 매핑한다 2026-06-16 19:21:54 +09:00
klaus 6ba5bf2cb1 feat(creator): 채널 후원 empty 리소스를 추가한다 2026-06-16 19:21:45 +09:00
klaus f6395b5a3e test(creator): 채널 홈 후속 동작을 검증한다 2026-06-16 17:27:52 +09:00
klaus 1cd676bcb4 feat(creator): 채널 상단 액션을 연결한다 2026-06-16 17:27:38 +09:00
klaus 0bb5796da1 feat(main): 채팅 DM 필터 진입을 추가한다 2026-06-16 17:27:29 +09:00
klaus 5f4140ea68 feat(creator): 채널 본인 상태를 계산한다 2026-06-16 17:27:24 +09:00
klaus b2878e42ea feat(creator): 채널 신고 차단 동작을 연결한다 2026-06-16 17:27:13 +09:00
klaus e8bd31454e feat(creator): 채널 더보기 메뉴를 추가한다 2026-06-16 17:27:08 +09:00
klaus 59545ca82c feat(creator): 채널 홈 상단 UI 상태를 보정한다 2026-06-16 17:27:03 +09:00
klaus cd1f9db8ae docs(creator): 채널 홈 후속 요구사항을 기록한다 2026-06-16 17:26:57 +09:00
klaus d719470f8c docs(creator): 채널 홈 탭 전환 검증을 기록한다 2026-06-16 14:35:51 +09:00
klaus f3c19ed8ba feat(creator): 채널 홈 탭 전환을 연결한다 2026-06-16 14:35:39 +09:00
klaus 984aa13edf feat(creator): 채널 탭 프래그먼트 골격을 추가한다 2026-06-16 14:35:23 +09:00
klaus 5ac900f3b7 docs(guide): 검증 기록 위치 규칙을 정리한다 2026-06-16 12:25:49 +09:00
klaus a21427b549 docs(creator): 채널 홈 탭 전환 계획을 보강한다 2026-06-16 12:20:38 +09:00
klaus f667bf1096 feat(creator): 본인 페이지 FAB 아이콘을 추가한다 2026-06-16 12:13:24 +09:00
klaus 458cdc3280 docs(creator): 채널 홈 후속 요구사항을 정리한다 2026-06-16 12:13:10 +09:00
klaus 2cf492d634 docs(creator): 채널 홈 검증 기록을 보강한다 2026-06-15 23:34:13 +09:00
klaus e16bc306f7 refactor(creator): 채널 홈 액티비티 이름을 정리한다 2026-06-15 23:33:41 +09:00
klaus fcb198c8a8 docs(creator): 채널 홈 최종 검증을 기록한다 2026-06-15 22:40:53 +09:00
klaus 40679a624b fix(creator): 오디션 지원자 프로필 이동을 복구한다 2026-06-15 22:40:42 +09:00
klaus a2be1739a6 docs(creator): 채널 홈 Phase 7 검증을 기록한다 2026-06-15 21:01:58 +09:00
klaus b529a83fe6 feat(profile): 내 채널 진입점을 전환한다 2026-06-15 21:01:40 +09:00
klaus 20491906fd feat(follow): 팔로잉 채널 진입점을 전환한다 2026-06-15 21:01:34 +09:00
klaus 9a8559af8b feat(search): 탐색 검색 채널 진입점을 전환한다 2026-06-15 21:01:28 +09:00
klaus 541333bc44 feat(live): 라이브 채널 진입점을 전환한다 2026-06-15 21:01:21 +09:00
klaus 9c4c0506af feat(home): 홈 채널 진입점을 전환한다 2026-06-15 21:01:11 +09:00
klaus 00b5a3687e feat(main): 메인 채널 진입점을 전환한다 2026-06-15 21:01:03 +09:00
klaus b41e5c6074 feat(audio): 오디오 채널 진입점을 전환한다 2026-06-15 21:00:49 +09:00
klaus 19b8e4750f feat(creator): 채널 홈 팔로우와 탭 동작을 연결한다 2026-06-15 21:00:42 +09:00
klaus bfb5440c9e fix(creator): 채널 홈 SNS xurl 파싱을 수정한다 2026-06-15 21:00:12 +09:00
klaus 1ce974938d docs(creator): 채널 홈 스크롤 구현 검증을 기록한다 2026-06-15 19:10:54 +09:00
klaus dc217f97af feat(creator): 채널 홈 탭 고정 스크롤을 연결한다 2026-06-15 19:10:49 +09:00
klaus d3bfc57294 feat(creator): 채널 홈 스크롤 상태 계산을 추가한다 2026-06-15 19:10:43 +09:00
klaus cd5fbc0858 docs(creator): 채널 홈 소개 활동 SNS 검증을 기록한다 2026-06-15 18:43:53 +09:00
klaus 2d2844338a feat(creator): 채널 홈 소개 활동 SNS 섹션을 재구성한다 2026-06-15 18:43:47 +09:00
klaus 1f3abfde4f feat(creator): 채널 홈 SNS 아이콘 레이아웃을 추가한다 2026-06-15 18:43:39 +09:00
klaus 2d58a876da feat(creator): 채널 홈 시리즈, 커뮤니티, 팬 Talk 섹션을 재구성한다 2026-06-15 17:22:32 +09:00
klaus c45c5a2a5c docs(creator): 채널 홈 오디오 섹션 검증을 기록한다 2026-06-15 15:01:46 +09:00
klaus 5fde0bc469 feat(creator): 채널 홈 오디오 섹션을 재구성한다 2026-06-15 15:01:41 +09:00
klaus edb7d98de7 feat(creator): 채널 홈 오디오 콘텐츠 카드를 추가한다 2026-06-15 15:01:21 +09:00
klaus 573df4318b test(creator): 채널 홈 화면 계약을 검증한다 2026-06-15 13:21:40 +09:00
klaus febd718796 fix(creator): 채널 홈 토스트 이벤트를 단발 처리한다 2026-06-15 13:21:34 +09:00
klaus a20655badb test(creator): 채널 홈 매퍼 검증을 보강한다 2026-06-15 13:21:28 +09:00
klaus 11fd892310 feat(creator): 채널 홈 섹션 렌더링을 연결한다 2026-06-15 13:21:15 +09:00
klaus 2253dabfe9 feat(creator): 채널 홈 일정 카드를 추가한다 2026-06-15 13:21:07 +09:00
klaus e2575e2b15 feat(creator): 채널 홈 공지 카드를 추가한다 2026-06-15 13:21:01 +09:00
klaus 05f724dbe1 feat(creator): 채널 홈 후원 카드를 추가한다 2026-06-15 13:20:54 +09:00
klaus d4b3a9a8a4 feat(creator): 채널 홈 최신 오디오 카드를 추가한다 2026-06-15 13:20:47 +09:00
klaus 74687daf28 feat(creator): 채널 홈 라이브 카드를 추가한다 2026-06-15 13:20:42 +09:00
klaus a8ba791349 feat(creator): 채널 홈 헤더 리소스를 추가한다 2026-06-15 13:20:35 +09:00
klaus 21383ced46 feat(creator): 채널 홈 문구 리소스를 추가한다 2026-06-15 13:20:25 +09:00
klaus a631aa1b65 feat(creator): 채널 홈 Activity 진입점을 추가한다 2026-06-15 13:20:11 +09:00
klaus 402ea5e9c0 docs(creator): 채널 홈 세부 구현 계획을 갱신한다 2026-06-15 13:19:56 +09:00
klaus 654a74aacf docs(creator): 채널 홈 Phase 4 검증을 기록한다 2026-06-13 17:20:17 +09:00
klaus 3027934295 feat(creator): 채널 홈 ViewModel을 추가한다 2026-06-13 17:20:06 +09:00
klaus a355838039 feat(creator): 채널 홈 UI 상태를 추가한다 2026-06-13 17:19:58 +09:00
klaus 0ae6596816 docs(creator): 채널 홈 탭 계획을 갱신한다 2026-06-13 16:22:05 +09:00
klaus da23253a9c feat(creator): 채널 홈 저장소를 등록한다 2026-06-13 16:21:59 +09:00
klaus 0934a7ec51 feat(creator): 채널 홈 API 모델을 추가한다 2026-06-13 16:21:53 +09:00
klaus 80e8213f12 refactor(home): 추천 활동 타입을 공용 타입으로 교체한다 2026-06-13 16:21:46 +09:00
klaus 55b4d9bc8d feat(common): 크리에이터 활동 타입을 공용화한다 2026-06-13 16:21:34 +09:00
klaus 92fdd6ab54 docs(creator): 채널 홈 탭 채팅 계획을 보완한다 2026-06-12 23:47:29 +09:00
klaus 3514104e7c feat(creator): 채널 액션 아이콘을 추가한다 2026-06-12 16:42:47 +09:00
klaus 122c559e57 feat(creator): 채널 타이틀바 아이콘을 추가한다 2026-06-12 16:42:42 +09:00
klaus b2cb08e96a docs(creator): 채널 홈 탭 계획을 기록한다 2026-06-12 16:42:22 +09:00
klaus 9e9533c7a6 fix(audio): 핀 아이콘 표시 크기를 고정한다 2026-06-11 13:28:01 +09:00
klaus f6ab1bd4ef fix(audio): 핀 아이콘 리소스 밀도를 보정한다 2026-06-11 13:27:52 +09:00
klaus f47a9d6d93 docs(chat): DM Phase 6 검증을 기록한다 2026-06-11 12:06:03 +09:00
klaus ff728af7cc docs(test): DM 채팅 테스트 명령을 추가한다 2026-06-11 12:05:57 +09:00
klaus 1e8beb458c feat(chat): DM 채팅방 실행 구성을 등록한다 2026-06-11 12:05:51 +09:00
klaus a6d8cd54f3 fix(chat): DM realtime 정리 누락을 보정한다 2026-06-11 12:05:45 +09:00
klaus 841ed5f6f8 fix(chat): DM SSE read timeout을 제거한다 2026-06-11 12:05:39 +09:00
klaus 0263e64f40 docs(chat): DM 채팅 Phase 5 검증을 기록한다 2026-06-11 11:17:46 +09:00
klaus a0b95ea3bd feat(chat): 채팅 탭에서 DM 채팅방을 연다 2026-06-11 11:17:26 +09:00
klaus 590a52c605 feat(chat): DM 채팅방 Activity를 추가한다 2026-06-11 11:17:04 +09:00
klaus f2687b8243 feat(chat): DM 채팅 실시간 수신을 연결한다 2026-06-11 11:16:44 +09:00
klaus 871f4e73e8 feat(chat): DM SSE 재연결 기반을 추가한다 2026-06-11 11:16:19 +09:00
klaus e19442a61a docs(chat): DM 채팅 Phase 4 검증을 기록한다 2026-06-10 19:27:09 +09:00
klaus 3285958918 feat(chat): DM 재시도 아이콘을 추가한다 2026-06-10 19:26:46 +09:00
klaus 06088c578f feat(chat): DM 채팅방 레이아웃을 추가한다 2026-06-10 19:26:11 +09:00
klaus 63a4f5b4f7 docs(chat): DM 채팅 Phase 3 검증을 기록한다 2026-06-10 18:48:36 +09:00
klaus 56f110c548 feat(chat): DM 채팅방 ViewModel을 추가한다 2026-06-10 18:48:27 +09:00
klaus 406c377d13 docs(chat): DM 채팅 Phase 2 검증을 기록한다 2026-06-10 18:11:59 +09:00
klaus fd0382ea65 feat(chat): DM 채팅 SSE 클라이언트를 추가한다 2026-06-10 18:11:53 +09:00
klaus a289849a07 feat(chat): DM 채팅 저장소를 추가한다 2026-06-10 18:11:47 +09:00
klaus 630f84c3e5 feat(chat): DM 채팅 메시지 매퍼를 추가한다 2026-06-10 17:39:52 +09:00
klaus e1ae7df0ee feat(chat): DM 채팅 API 모델을 추가한다 2026-06-10 17:39:41 +09:00
klaus 5f7fc68c8c docs(chat): DM 채팅화면 계획을 기록한다 2026-06-10 17:39:36 +09:00
klaus f4f561b396 docs(chat): 통합 검증 결과를 기록한다 2026-06-10 15:28:56 +09:00
klaus 40f8355037 docs(chat): 타이틀바 액션 간격 검증을 기록한다 2026-06-10 15:20:33 +09:00
klaus ccc4a822e3 fix(chat): 타이틀바 액션 간격을 보정한다 2026-06-10 15:20:09 +09:00
klaus 70177d8f81 docs(chat): 채팅 탭 스크롤 요구사항을 기록한다 2026-06-10 15:06:41 +09:00
klaus b38e58af9a feat(chat): 채팅 탭 화면 동작을 연결한다 2026-06-10 15:06:35 +09:00
klaus 516e4a94bf fix(widget): CapsuleTab 여백과 선택 배경을 보정한다 2026-06-10 15:06:29 +09:00
klaus cdb491b21b docs(chat): 채팅 탭 리뷰 개선을 기록한다 2026-06-10 14:27:40 +09:00
klaus a769fc6a64 docs(chat): 채팅 탭 Phase 6 완료를 기록한다 2026-06-10 14:27:16 +09:00
klaus 2c30da8110 fix(widget): CapsuleTab 선택 색상을 보정한다 2026-06-10 14:26:45 +09:00
klaus ee703eb13a feat(chat): 채팅 탭 기본 layout을 추가한다 2026-06-10 14:26:39 +09:00
klaus 55fb032b22 docs(chat): 채팅방 Adapter 검증을 기록한다 2026-06-10 13:26:03 +09:00
klaus 346671b3e2 feat(chat): 채팅방 목록 Adapter를 추가한다 2026-06-10 13:25:41 +09:00
klaus 5574e68b16 feat(chat): 채팅방 항목 레이아웃을 추가한다 2026-06-10 13:25:25 +09:00
klaus 445d91d594 feat(chat): 채팅방 필터 문자열을 추가한다 2026-06-10 13:25:09 +09:00
klaus 896935e19a docs(chat): 채팅방 ViewModel 검증을 기록한다 2026-06-10 12:00:34 +09:00
klaus bb17f0014a feat(chat): 채팅방 ViewModel을 추가한다 2026-06-10 12:00:27 +09:00
klaus ed8a0e9a09 refactor(chat): 채팅방 시간 표시 책임을 분리한다 2026-06-10 12:00:18 +09:00
klaus 10a74c2e5e docs(guide): 채팅방 테스트 명령을 추가한다 2026-06-10 11:30:14 +09:00
klaus d66743ae1e docs(chat): 채팅방 모델 작업 검증을 기록한다 2026-06-10 11:30:07 +09:00
klaus 5a17d7d2f6 feat(chat): 채팅방 UI 모델 매핑을 추가한다 2026-06-10 11:30:00 +09:00
klaus 50f0fb3d15 feat(chat): 채팅방 시간 표시를 추가한다 2026-06-10 11:29:52 +09:00
klaus 4c351da60c feat(chat): 채팅방 필터 모델을 추가한다 2026-06-10 11:05:39 +09:00
klaus 89837877a2 docs(chat): 채팅방 목록 데이터 계층 작업을 기록한다 2026-06-09 23:30:19 +09:00
klaus 32c30132b9 feat(chat): 채팅방 목록 API와 저장소를 추가한다 2026-06-09 23:30:11 +09:00
klaus 06e4b5ad34 refactor(chat): 채팅방 모델 파일명을 정리한다 2026-06-09 23:29:50 +09:00
klaus 972d225a86 feat(chat): 채팅방 목록 응답 모델을 추가한다 2026-06-09 23:14:19 +09:00
klaus 7acc7b51b6 refactor(chat): 채팅 Fragment 패키지를 분리한다 2026-06-09 23:14:09 +09:00
klaus 86e18a1f7c docs(chat): 채팅 탭 구현 계획을 추가한다 2026-06-09 23:13:59 +09:00
klaus bb4d290ca1 refactor(home): 홈 메인 Fragment 패키지를 분리한다 2026-06-09 18:17:57 +09:00
klaus 4e98a1dfea fix(home): 채팅 탭 문구를 대화로 변경한다 2026-06-09 18:06:21 +09:00
klaus c3bbf9203d docs(home): 랭킹 순위 영역 보정 검증을 기록한다 2026-06-09 15:13:14 +09:00
klaus 64fb55db55 fix(widget): 랭킹 순위 영역 위치를 보정한다 2026-06-09 15:13:01 +09:00
klaus ea076a5ac7 docs(home): 랭킹 순위 정렬 검증을 기록한다 2026-06-09 14:56:04 +09:00
klaus 6b0232b7e4 test(widget): 랭킹 순위 텍스트 정렬 검증을 추가한다 2026-06-09 14:55:58 +09:00
klaus 178397923b docs(home): 랭킹 페이지 최종 검증을 기록한다 2026-06-08 18:22:35 +09:00
klaus de4e90b98e docs(home): 랭킹 탭 검증을 기록한다 2026-06-08 18:07:07 +09:00
klaus 5d66014044 feat(home): 랭킹 탭 목록을 연결한다 2026-06-08 18:07:00 +09:00
klaus bb60f8bb9f docs(home): 크리에이터 랭킹 ViewModel 검증을 기록한다 2026-06-08 17:41:35 +09:00
klaus b199804827 feat(home): 크리에이터 랭킹 의존성을 등록한다 2026-06-08 17:41:29 +09:00
klaus 37dc3cd24a feat(home): 크리에이터 랭킹 ViewModel을 추가한다 2026-06-08 17:41:25 +09:00
klaus a4710ef6bb docs(guide): 테스트명 예외 규칙을 추가한다 2026-06-08 15:22:53 +09:00
klaus 28423d81bb docs(home): 크리에이터 랭킹 검증을 기록한다 2026-06-08 15:22:48 +09:00
klaus 21e94af8d1 feat(home): 크리에이터 랭킹 응답 매핑을 추가한다 2026-06-08 15:22:16 +09:00
klaus 6d980e319b feat(widget): 랭킹 카드 순위 변동 숨김을 적용한다 2026-06-08 15:22:10 +09:00
klaus 4a21827b47 feat(widget): 랭킹 순위 변동 표시 옵션을 추가한다 2026-06-08 15:22:02 +09:00
klaus 9600147240 docs(home): 크리에이터 랭킹 계획을 추가한다 2026-06-08 14:01:27 +09:00
klaus ecc59376a3 docs(home): 최근 활동 이동 검증을 기록한다 2026-06-06 00:08:54 +09:00
klaus 4b4a23c92e feat(home): 최근 활동 카드 이동을 연결한다 2026-06-06 00:08:41 +09:00
klaus c36eddb207 docs(home): 배너 이동 검증을 기록한다 2026-06-05 23:04:51 +09:00
klaus e160a10708 feat(home): 추천 배너 이동을 연결한다 2026-06-05 23:04:40 +09:00
klaus cdcd938cdf docs(home): 프로필과 상대 시간 검증을 기록한다 2026-06-05 22:01:23 +09:00
klaus 58e69be510 feat(home): 추천 시간과 프로필 표시를 보완한다 2026-06-05 22:01:16 +09:00
klaus 7f417c3a3f feat(common): UTC 상대 시간 포매터를 추가한다 2026-06-05 22:01:08 +09:00
klaus 9378f96b73 docs(home): 추천 안정화 검증을 기록한다 2026-06-05 20:54:01 +09:00
klaus 7e78ace304 docs(home): 추천 배너 요구사항을 갱신한다 2026-06-05 20:53:55 +09:00
klaus a3aa406e13 test(home): 추천 응답 스키마 검증을 추가한다 2026-06-05 20:53:50 +09:00
klaus 8371dc7baf feat(home): 추천 UI 바인딩을 갱신한다 2026-06-05 20:53:44 +09:00
klaus 44457349aa feat(home): 추천 응답 모델을 갱신한다 2026-06-05 20:53:34 +09:00
klaus 51226cf5cc fix(widget): 배너 가상 목록 갱신을 안정화한다 2026-06-05 20:53:28 +09:00
klaus 7f307346f3 fix(common): 이미지 캐시 디렉터리를 분리한다 2026-06-05 20:53:20 +09:00
klaus 9de4493e89 docs(home): 추천 상태 연결 검증을 기록한다 2026-06-05 16:01:43 +09:00
klaus c6680d0bd2 feat(home): 추천 상태 observe를 연결한다 2026-06-05 16:01:37 +09:00
klaus 6679808a18 feat(home): 콘텐츠 카드 클릭을 연결한다 2026-06-05 16:01:31 +09:00
klaus 7a4fa3c50f feat(home): 크리에이터 카드 클릭을 연결한다 2026-06-05 16:01:23 +09:00
klaus 7c0af85aaa docs(home): 사업자 정보 검증을 기록한다 2026-06-05 14:30:55 +09:00
klaus 8457194bb5 fix(home): 사업자 정보 문구를 공용 리소스로 정리한다 2026-06-05 14:30:44 +09:00
klaus f07132c48b feat(home): 사업자 정보 inline 더보기를 추가한다 2026-06-05 14:30:34 +09:00
klaus 9c20b86373 docs(home): 인기 커뮤니티 검증을 기록한다 2026-06-05 13:17:14 +09:00
klaus 293f34ca13 test(home): 인기 커뮤니티 섹션 검증을 추가한다 2026-06-05 13:17:06 +09:00
klaus dca06cdc3c feat(home): 인기 커뮤니티 섹션을 바인딩한다 2026-06-05 13:16:59 +09:00
klaus 1cbf989577 feat(home): 인기 커뮤니티 adapter를 추가한다 2026-06-05 13:16:53 +09:00
klaus 5b3b7c72d2 feat(widget): 커뮤니티 이미지와 유료 overlay를 추가한다 2026-06-05 13:16:47 +09:00
klaus cd274a6d2f docs(home): 응원 크리에이터 검증을 기록한다 2026-06-04 20:18:12 +09:00
klaus edf4a94494 feat(home): 응원 크리에이터 카드 바인딩을 추가한다 2026-06-04 20:18:06 +09:00
klaus d5f46e6325 feat(home): 응원 크리에이터 카드 리소스를 추가한다 2026-06-04 20:18:00 +09:00
klaus 22ac5b0b54 docs(home): 장르 크리에이터 수정 검증을 기록한다 2026-06-04 19:38:14 +09:00
klaus bb29fc8010 test(home): 장르 크리에이터 페이지 검증을 추가한다 2026-06-04 19:38:08 +09:00
klaus c733796aeb feat(home): 장르 크리에이터 그룹 바인딩을 추가한다 2026-06-04 19:38:03 +09:00
klaus c714a9d4c8 feat(home): 장르 크리에이터 페이지 리소스를 추가한다 2026-06-04 19:37:56 +09:00
klaus c436866a51 docs(home): 모두 팔로우 버튼 검증을 기록한다 2026-06-04 17:59:36 +09:00
klaus 02480a96e9 fix(home): 모두 팔로우 완료 상태를 반영한다 2026-06-04 17:59:29 +09:00
klaus 3e8ea0473f feat(home): 모두 팔로우 버튼 배경을 추가한다 2026-06-04 17:59:23 +09:00
klaus 99c2082022 docs(agent): clipping 처리 가이드를 추가한다 2026-06-02 19:34:48 +09:00
klaus 4629ef00a9 fix(widget): 캐릭터 채팅 썸네일 clipping을 Kotlin에서 설정한다 2026-06-02 19:34:19 +09:00
klaus a5b4d5046d docs(home): AI 캐릭터 섹션 검증 기록을 추가한다 2026-06-02 19:21:23 +09:00
klaus 29e64188c9 feat(home): AI 캐릭터 섹션 간격을 정리한다 2026-06-02 19:20:57 +09:00
klaus 8a73ea0472 docs(home): 첫 오디오 섹션 검증 기록을 추가한다 2026-06-02 19:00:30 +09:00
klaus b402025ca5 feat(widget): 오디오 콘텐츠 카드 배지를 정리한다 2026-06-02 19:00:13 +09:00
klaus bd475d1c87 feat(home): 첫 오디오 콘텐츠 카드를 정리한다 2026-06-02 18:59:57 +09:00
klaus 816641d7c5 docs(home): 최근 데뷔 섹션 검증 기록을 추가한다 2026-06-02 17:28:47 +09:00
klaus 3028288bb3 feat(home): 최근 데뷔 크리에이터 카드를 정리한다 2026-06-02 17:28:40 +09:00
klaus 089e980832 feat(home): 홈 추천 섹션 간격을 정리한다 2026-06-02 17:27:57 +09:00
klaus f93236b2d0 docs(home): 라이브 섹션 검증 기록을 추가한다 2026-06-02 17:05:23 +09:00
klaus 9b29623f6f feat(home): 라이브 섹션 전체 아이템을 추가한다 2026-06-02 17:04:53 +09:00
klaus 0e50d7f8d5 docs(home): adapter 파일 분리 기록을 추가한다 2026-06-02 16:34:27 +09:00
klaus 9a8231c2b6 refactor(home): 홈 추천 adapter 파일을 분리한다 2026-06-02 16:34:16 +09:00
klaus 0a5be6467c docs(home): Phase 6 검증 기록을 추가한다 2026-06-02 16:23:45 +09:00
klaus 716f3c6880 test(home): 홈 추천 섹션 레이아웃 검증을 추가한다 2026-06-02 16:23:36 +09:00
klaus b20866fedd feat(home): 홈 추천 섹션 바인딩을 추가한다 2026-06-02 16:23:28 +09:00
klaus 14e7b33b63 feat(home): 최근 활동 크리에이터 카드를 정리한다 2026-06-02 16:23:21 +09:00
klaus cfa297ac3f feat(home): 홈 추천 크리에이터 카드 리소스를 추가한다 2026-06-02 16:23:11 +09:00
klaus e0e64090cd docs(home): Phase 5 검증 기록을 추가한다 2026-06-02 14:51:28 +09:00
klaus 68f8d869cc feat(home): 홈 추천 상단 UI shell을 추가한다 2026-06-02 14:51:01 +09:00
klaus d07a2837d9 feat(home): 홈 추천 섹션 문자열을 정리한다 2026-06-02 14:50:31 +09:00
klaus caee72f9c3 docs(home): Phase 4 검증 기록을 추가한다 2026-06-02 13:29:36 +09:00
klaus 4870b7377a feat(home): 홈 추천 ViewModel을 추가한다 2026-06-02 13:29:01 +09:00
klaus dd002e9f82 feat(home): 홈 추천 UI 상태 모델을 추가한다 2026-06-02 13:28:32 +09:00
klaus 4743c9cb8f feat(home): 홈 추천 UI 아이콘 리소스를 추가한다 2026-06-02 12:10:35 +09:00
klaus 3132269038 feat(home): 홈 추천 DI 등록을 추가한다 2026-06-02 12:10:18 +09:00
klaus 9eb1f7709d feat(home): 홈 추천 문자열 리소스를 추가한다 2026-06-02 12:10:01 +09:00
klaus 4817641155 feat(home): 홈 추천 activity type 매퍼를 추가한다 2026-06-02 12:09:46 +09:00
klaus c74ceba495 feat(home): 홈 추천 API 데이터 계층을 추가한다 2026-06-02 12:09:25 +09:00
klaus 0d339f9565 docs(home): 메인 홈 추천 요구사항을 문서화한다 2026-06-02 12:09:04 +09:00
klaus ee58794441 docs(workflow): 문서 폴더명 공백 대체 규칙 추가 2026-06-02 00:18:38 +09:00
klaus 4ab2490534 docs(agent): 계획 문서 규칙 수정 2026-06-01 19:34:16 +09:00
klaus 9a47c42958 docs(banner): Phase 9와 10 검증을 기록한다 2026-05-28 14:41:24 +09:00
klaus a35310e536 feat(banner): 배너 시각 정렬을 보완한다 2026-05-28 14:41:17 +09:00
klausandSisyphus ecb9f5a260 docs(banner): Phase 8 검증을 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 13:11:12 +09:00
klausandSisyphus 8dd2371ce4 feat(banner): Phase 8 배너 동작을 보완한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 13:10:38 +09:00
klausandSisyphus df782d7968 docs(banner): Phase 7 검증을 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 12:12:24 +09:00
klausandSisyphus 7672a3bbe8 style(widget): 테스트 unused import를 제거한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 12:10:27 +09:00
klausandSisyphus 9a56c124cc style(banner): 배너 ktlint 지적을 정리한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 12:10:02 +09:00
klausandSisyphus 2fa9561a09 docs(banner): Phase 6 검증을 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 11:51:20 +09:00
klausandSisyphus 462d9c90b5 feat(banner): 배너 preview 회귀 테스트를 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 11:51:14 +09:00
klausandSisyphus db72c4bf7d docs(banner): 배너 wrap content 요구를 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 11:14:12 +09:00
klausandSisyphus 49984cb651 feat(banner): 배너 wrap content 높이를 지원한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 11:14:02 +09:00
klausandSisyphus dcc76abf94 docs(banner): Phase 5 검증을 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 10:50:30 +09:00
klausandSisyphus bc15a0997e feat(banner): 배너 자동 전환 동작을 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 10:50:23 +09:00
klausandSisyphus 31b4e93bed docs(banner): 배너 Phase 3과 4 검증을 기록한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 00:08:56 +09:00
klausandSisyphus fe509365e2 feat(banner): 배너 어댑터와 뷰를 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 00:08:41 +09:00
klausandSisyphus 2e5af796e4 feat(banner): 배너 XML 레이아웃 속성을 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 00:08:25 +09:00
klausandSisyphus e2d848b7e5 feat(banner): 배너 디자인 리소스를 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-28 00:08:10 +09:00
klaus 4d0d242162 docs(agent): 커밋 메시지 검증 규칙을 명시한다 2026-05-27 22:41:04 +09:00
klaus 1ffcd16efa docs(banner): Phase 2 검증과 테스트명 규칙을 기록한다 2026-05-27 22:28:19 +09:00
klaus 02f85f808d feat(banner): 배너 카운터와 상태 계약을 추가한다 2026-05-27 22:27:40 +09:00
klaus 91a7eb3f4c feat(banner): 배너 레이아웃 계산 계약을 추가한다 2026-05-27 22:26:50 +09:00
klaus 0c8c9f9b5f feat(banner): 배너 아이템 계약을 추가한다 2026-05-27 22:26:16 +09:00
klaus 8ae09d0514 docs(banner): 배너 컴포넌트 계획을 문서화한다 2026-05-27 21:10:57 +09:00
klaus 413a2f2fc1 docs(agent): 계획 문서 phase task 규칙을 추가한다 2026-05-27 20:56:17 +09:00
klaus 9ff4a8159a docs(agent): 실행 정책 중복을 정리한다 2026-05-27 20:20:13 +09:00
klaus abc0cfe461 chore(opencode): 커밋 정책 스킬을 제거한다 2026-05-27 20:19:25 +09:00
klaus 9a67c45dab docs(agent): 분리된 규칙 문서 참조를 갱신한다 2026-05-27 20:14:15 +09:00
klaus add6b66ea5 docs(agent): 작업 계획과 커밋 규칙 문서를 분리한다 2026-05-27 20:12:43 +09:00
klaus 11fdf89340 docs(workflow): 작업 계획 커밋 규칙 분리를 계획한다 2026-05-27 20:11:35 +09:00
klaus 9e37347877 chore(gitignore): antigravitycli 생성물을 제외한다 2026-05-27 19:40:08 +09:00
klaus ef81cba7eb chore(gitignore): omo 생성물을 제외한다 2026-05-27 14:54:43 +09:00
klaus 799dd7fc92 feat(widget): 오디오 콘텐츠 태그 배지를 추가한다 2026-05-27 14:50:59 +09:00
klaus a8e0f2377d feat(feed): 피드 어댑터와 뷰 테스트를 추가한다 2026-05-21 15:53:40 +09:00
klaus 59ea5de00a feat(feed): 커뮤니티 피드 뷰를 추가한다 2026-05-21 15:53:40 +09:00
klaus 77eef9609a feat(feed): 콘텐츠 피드 뷰를 추가한다 2026-05-21 15:53:40 +09:00
klaus 8e9ce634e3 feat(feed): 라이브 피드 뷰를 추가한다 2026-05-21 15:53:40 +09:00
klaus 4d0d330797 feat(feed): 랭킹 피드 뷰를 추가한다 2026-05-21 15:53:40 +09:00
klaus a5728bcc4d feat(feed): 피드 크기 계산 계약을 추가한다 2026-05-21 15:53:40 +09:00
klaus 3444b1eeef feat(feed): 랭킹 강조 텍스트 계약을 추가한다 2026-05-21 15:53:39 +09:00
klaus a2f3910e27 feat(feed): 피드 아이템 계약을 추가한다 2026-05-21 15:53:39 +09:00
klaus 01765f3e7f feat(feed): 피드 반응 아이콘 리소스를 추가한다 2026-05-21 15:53:39 +09:00
klaus 1baf62a2b7 feat(feed): 피드 공통 배경 리소스를 추가한다 2026-05-21 15:53:39 +09:00
klaus e5f298eaa7 feat(feed): 피드 문자열 리소스를 추가한다 2026-05-21 15:53:39 +09:00
klaus d312ce7cd8 docs(feed): 피드 컴포넌트 요구사항을 문서화한다 2026-05-21 15:53:39 +09:00
klaus 0764bced76 docs(agent): 코드 스타일 토큰 사용 원칙을 문서화한다 2026-05-21 11:32:35 +09:00
klaus c32f9cdd9f feat(widget): 캐릭터 채팅 썸네일 컴포넌트를 추가한다 2026-05-21 11:22:23 +09:00
klaus c58f03be08 feat(widget): 라이브 썸네일 컴포넌트를 추가한다 2026-05-20 17:55:19 +09:00
klaus 960e78afac feat(widget): 시리즈 콘텐츠 카드 컴포넌트를 추가한다 2026-05-20 14:06:33 +09:00
klaus 36ffbc6cdb feat(widget): 콘텐츠 랭킹 위젯을 추가한다 2026-05-20 12:00:23 +09:00
klaus 01fea58e4c feat(widget): 크리에이터 랭킹 위젯을 추가한다 2026-05-20 10:41:07 +09:00
klaus 6fda122091 feat(widget): 오디오 콘텐츠 카드 컴포넌트를 추가한다 2026-05-19 23:52:53 +09:00
klaus 30264935dc feat(widget): 섹션 타이틀 컴포넌트를 추가한다 2026-05-19 21:07:15 +09:00
klaus 4457941193 feat(widget): 공통 탭바와 타이틀바 컴포넌트를 추가한다 2026-05-19 20:29:42 +09:00
klaus 3121d9dca9 fix(nav): 내 탭 선택 아이콘을 교체한다 2026-05-19 17:03:45 +09:00
klaus 99b7a6ce99 feat(main-v2): 메인 하단 내비게이션을 추가한다 2026-05-19 15:55:03 +09:00
klaus 751b031627 docs(workflow): PRD 파일명 한글 규칙을 명시한다 2026-05-19 13:44:48 +09:00
klaus 7f0e7b0ff7 feat(design-token): XML 디자인 토큰 리소스를 추가한다 2026-05-15 19:50:44 +09:00
klaus bf2a5c7489 feat(typography): XML Typography 스타일을 추가한다 2026-05-15 18:58:16 +09:00
klaus bdc5e8020c docs(agent): 코드 스타일 가이드를 분리한다 2026-05-14 20:39:04 +09:00
klaus 698c395f3d docs(agent): 신규 v2 패키지 작성 규칙을 문서화한다 2026-05-14 20:29:52 +09:00
klaus 65960888aa docs(workflow): PRD와 계획 문서 구조를 정리한다 2026-05-14 20:20:31 +09:00
klaus bd3f961ee1 feat(chat-room): 채팅 쿼터 광고/캔 충전 흐름을 연결한다 2026-04-30 12:47:53 +09:00
klaus 723fe6b90c feat(chat-ui): 채팅 쿼터 부족 안내 리소스를 광고 구매 UI로 바꾼다 2026-04-30 12:47:44 +09:00
klaus 17fc70d9ee feat(chat-ui): 채팅 쿼터 안내 액션 모델을 정리한다 2026-04-30 12:47:36 +09:00
klaus fe5af96ff7 feat(chat-quota): 쿼터 구매 계약과 광고 ad unit 구성을 추가한다 2026-04-30 12:47:25 +09:00
klaus 5dd58d0092 docs(plan): 채팅 쿼터 충전 확장 작업 기록을 추가한다 2026-04-30 12:47:14 +09:00
klaus b565a0eb02 docs(plan): 채팅방 분석 문서와 작업 기록 규칙을 추가한다 2026-04-29 16:57:28 +09:00
klaus 4b84e30195 docs(agents): AGENTS 지침 문서를 정리한다 2026-04-29 14:41:20 +09:00
klaus ef8b3fed1c chore(ads): 미사용 ad unit과 작업 기록을 정리한다 2026-04-27 18:25:58 +09:00
klaus 003ebbcc52 fix(live-room): 무료방 입장 전면광고를 제거한다 2026-04-27 18:25:43 +09:00
klaus 9f0adc0593 fix(mypage): 하단 배너 광고 노출을 제거한다 2026-04-27 18:25:33 +09:00
klaus 7fe25f474a feat(talk): 톡 탭에 Yandex 배너 헤더를 추가한다 2026-04-27 15:22:48 +09:00
klaus 4ef880c350 feat(original): 오리지널 탭에 Yandex 배너 헤더를 추가한다 2026-04-27 15:22:42 +09:00
klaus 8295e3d25e feat(character): 캐릭터 탭에 Yandex 인라인 배너를 추가한다 2026-04-27 15:22:11 +09:00
klaus d0dd6c9224 feat(ads): 채팅 탭 Yandex 배너 ad unit 값을 추가한다 2026-04-27 15:21:57 +09:00
klaus 712a2e62e9 feat(settings): 알림 수신 설정에 인라인 배너를 추가한다 2026-04-24 19:44:43 +09:00
klaus 8f6c837e22 feat(notification): 알림 리스트에 인라인 배너를 추가한다 2026-04-24 19:44:35 +09:00
klaus 290b15d007 feat(series-genre): 시리즈 장르별 화면에 인라인 배너를 추가한다 2026-04-24 19:44:27 +09:00
klaus 1139040f28 feat(series-day): 시리즈 요일별 화면에 인라인 배너를 추가한다 2026-04-24 19:44:20 +09:00
klaus ebd72ef739 feat(series-home): 시리즈 홈 섹션 사이에 인라인 배너를 추가한다 2026-04-24 19:44:12 +09:00
klaus c639eaf86a feat(community): 크리에이터 커뮤니티 전체보기에 인라인 배너를 추가한다 2026-04-24 19:44:05 +09:00
klaus c75b089cb7 feat(ads): 커뮤니티 시리즈 알림 배너 ad unit 값을 추가한다 2026-04-24 19:43:58 +09:00
klaus 4e18205cca docs(plan): 커뮤니티 시리즈 알림 배너 작업 기록을 추가한다 2026-04-24 19:43:36 +09:00
klaus 31306583d0 feat(ads): 라이브와 콘텐츠 상세 광고 지면을 추가한다 2026-04-24 18:42:49 +09:00
klaus 4a4cdadef1 docs(plan): 무료방 전면 광고 작업 기록을 추가한다 2026-04-21 14:11:33 +09:00
klaus f6a94e0f7c feat(live-room): 무료방 입장 전면 광고를 추가한다 2026-04-21 14:11:24 +09:00
klaus 30b3dcdce6 feat(mypage): 하단 Yandex 인라인 배너를 추가한다
마이페이지 스크롤 콘텐츠 최하단에서 Yandex 인라인 배너를 노출해 광고 영역을 확보한다.
debug/release ad unit id를 분리해 배포 전환 지점을 한곳에 고정한다.
2026-04-21 13:23:06 +09:00
klaus af0d788796 feat(ads): Yandex Mobile Ads SDK 설정을 추가한다 2026-04-21 12:08:24 +09:00
klaus 40d8092880 chore(ads): DARO 광고 제거를 반영한다 2026-04-20 19:05:25 +09:00
klaus 002d20dc0f docs(plan): 무료 라이브 라이트 팝업 작업 기록을 갱신한다 2026-04-20 16:04:12 +09:00
klausandSisyphus d8221dc784 fix(live-room): 무료 라이브 라이트 팝업 노출 조건을 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-20 16:03:42 +09:00
klaus 68e8941cc1 feat(mypage): 최하단에 배너 광고 추가 2026-04-20 15:27:44 +09:00
klaus 8271f117a4 fix(payment): 신규 인텐트 nullability를 실제 계약에 맞춘다 2026-04-20 11:46:56 +09:00
klaus 21e73e013f refactor(image): blur 호출부를 로컬 구현으로 전환한다 2026-04-20 11:46:47 +09:00
klaus b589329398 fix(image): Coil 2 대응 BlurTransformation 구현을 추가한다 2026-04-20 11:46:38 +09:00
klaus 272cd502be chore(ads): Daro 초기화와 배포 설정을 반영한다 2026-04-20 11:46:29 +09:00
klaus 1288fc3878 build(ads): Daro 저장소와 의존성 구성을 추가한다 2026-04-20 11:46:21 +09:00
klaus 84fa71c64f Apply .gitignore 2026-04-17 16:16:49 +09:00
klaus f9b0c274b2 chore(opencode): 1.4.3으로 업데이트 2026-04-13 14:56:59 +09:00
klaus 9654c41fb9 fix(live-room): 종료 경합의 중복 재조회와 오류 토스트를 막는다 2026-04-13 13:41:17 +09:00
klaus c5411899bc fix(home): 오디션 배너 노출을 중단한다 2026-04-02 12:32:11 +09:00
klaus 336d411627 refactor(mypage): 기능 버튼 렌더링 책임을 분리한다 2026-04-02 12:22:08 +09:00
klaus 8d8d5e340f fix(mypage): 기능 버튼 배치와 쿠폰 분기를 정리한다 2026-04-02 12:12:59 +09:00
klaus 9ef2cb1731 fix(mypage): 쿠폰 버튼 노출과 인증 분기를 조정한다 2026-04-02 11:53:06 +09:00
klaus 14817ef344 chore(version): 버전코드를 237로 상향한다 2026-03-30 21:52:13 +09:00
klaus 96b385342a feat(live-room): 라이브 캡쳐 녹화 허용 설정을 생성 및 시청 정책에 반영한다 2026-03-30 21:50:28 +09:00
klaus 2d97328eb7 fix(live-room): 스탭 해제 후 권한 표시를 즉시 동기화한다 2026-03-30 18:25:19 +09:00
klaus 6393a62410 chore(version): versionCode 235, versionName 1.54.1 2026-03-30 17:01:35 +09:00
klaus d074f98d24 fix(live-room): 스탭 캡쳐 녹화 허용 정책을 동기화한다 2026-03-30 16:58:37 +09:00
klaus a6e3f71ca5 chore(version): versionCode 234, versionName 1.54.0 2026-03-29 04:25:40 +09:00
klaus c78849e994 fix(live-room): 라이브룸 입장 직후 UI 미갱신을 방지한다 2026-03-29 04:23:57 +09:00
klaus 5609560b77 fix(live-room): 방장 캡쳐 녹화 허용 정책을 반영한다 2026-03-28 20:04:20 +09:00
klaus 4bb91c605a fix(mypage): 비한국 사용자 본인인증 버튼 숨김 시 정렬을 유지한다 2026-03-28 18:40:58 +09:00
klaus 9dfad913bc fix(member-info): 구서버 멤버정보 누락 필드 하위 호환을 보장한다 2026-03-28 18:28:09 +09:00
klaus 4815cac49b fix(api): 콘텐츠 설정 PATCH 제외 API 파라미터를 제거한다 2026-03-27 22:31:13 +09:00
klaus 25d549b06f docs(plan): 연령제한 표시 조건 수정 검증 기록을 추가한다 2026-03-27 18:25:10 +09:00
klaus 0c0801561e fix(content): 연령제한 노출 조건을 공통 정책으로 통일한다 2026-03-27 18:24:47 +09:00
klaus 0fcd929c6f fix(content): 국가별 성인 콘텐츠 접근 동기화를 정리한다 2026-03-27 17:33:52 +09:00
klaus 6aa7b9e98c fix(live-room): 스크린샷 dead path 제거로 녹화 음소거 정합을 맞춘다 2026-03-24 17:21:42 +09:00
klaus 8c0690b1e5 fix(live-room): 캡처/녹화 시 라이브룸 보안 음소거를 동기화한다 2026-03-24 16:16:14 +09:00
klaus 08524bd79a fix(live-room): 매니저 SNS 아이콘 노출 방식을 동적 렌더링으로 통일한다 2026-03-24 13:35:15 +09:00
klaus a893d85632 fix(live-room): 채팅 얼림 문구 국제화와 버전 코드를 반영한다 2026-03-20 16:51:41 +09:00
klaus 41f6ddd61b fix(live-room): 채팅 얼림 버튼 배치와 경고 문구를 정리한다 2026-03-20 15:58:27 +09:00
klaus 3a14bad2a4 fix(live-room): 채팅 얼림 토글 UI와 안내 문구를 정렬한다 2026-03-20 14:27:24 +09:00
klaus a4ba3088b0 feat(live-room): 라이브룸 채팅 삭제 기능 구현을 반영한다 2026-03-20 10:51:16 +09:00
klaus b17a0dcc0e fix(live-room): 채팅 얼림 상태 터치 경고 동작을 복구한다 2026-03-19 19:03:05 +09:00
klaus 26522cea3f feat(live-room): 채팅창 얼리기 기능을 추가한다
채팅 입력 제어와 룸 상태 동기화를 통합해 지연 입장자도 동일 상태를 적용한다.
2026-03-19 18:00:43 +09:00
klaus 543c4959e7 fix(live): 라이브룸 후원·하트 랭킹 왕관 UI를 동일화한다 2026-03-18 14:28:19 +09:00
klaus 544a67c8ec fix(live-room): 라이브룸 채팅 랭킹 왕관 표시 규격을 조정한다 2026-03-18 14:06:17 +09:00
klaus f0977d9433 chore(version): versionCode 227, versionName 1.52.1 2026-03-17 16:13:54 +09:00
klaus 13ac1fb435 fix(profile): 프로필 후원 랭킹 전체보기 왕관 UI 위치와 크기를 통일한다 2026-03-17 16:11:25 +09:00
klaus 76c45b62d7 fix(profile): 프로필 후원 랭킹 왕관 UI 위치와 크기를 통일한다 2026-03-17 15:55:48 +09:00
klaus 667be467a4 chore(version): versionCode 226, versionName 1.52.1 2026-03-17 11:59:06 +09:00
klaus 388ba05700 fix(community): 게시글 고정 표시인 pin 크기를 20x20으로 수정 2026-03-17 11:58:49 +09:00
klaus 2620bb5b93 fix(community): 커뮤니티 고정글 메뉴 동작을 정리한다 2026-03-16 20:37:47 +09:00
klaus eba4a444bc docs(agents): 주석 작성 규칙을 추가한다 2026-03-16 17:55:26 +09:00
klaus 4ec828b892 chore(version): versionCode 225, versionName 1.52.0 2026-03-14 00:13:22 +09:00
klaus 60677e262c fix(deeplink): 커뮤니티 댓글 딥링크 postId 라우팅을 정렬한다 2026-03-13 21:39:20 +09:00
klaus 598a04d084 fix(ui): 탭 상단 로고 영역 간격을 통일한다 2026-03-13 17:56:48 +09:00
klaus 9f27ea8aec fix(image) - 메시지 페이지 이동 아이콘 변경 2026-03-13 17:37:37 +09:00
klaus ba7d1ddee2 fix(deeplink): 예약 라이브 딥링크 라우팅을 메인으로 통일한다 2026-03-13 14:44:07 +09:00
klaus 4a65902217 feat(notification): 알림 수신 설정 화면을 추가한다 2026-03-13 13:56:34 +09:00
klaus 0f371ffd0e fix(deeplink): 푸시 딥링크 우선 분기로 혼합 라우팅을 방지한다 2026-03-13 11:34:16 +09:00
klaus 3287421614 fix(pushnotification): 알림 목록 조회 페이지 인덱스를 보정한다 2026-03-12 18:59:15 +09:00
klaus c0c5d6efc1 feat(pushnotification): 홈 알림 리스트 화면과 딥링크 라우팅을 추가한다 2026-03-12 18:36:01 +09:00
klaus 5bd4e45542 chore(gitignore): IDE 테스트 결과 파일 추적을 제외한다 2026-03-11 13:50:08 +09:00
klaus 418b734c3f refactor(preferences): DataStore 설정 저장 안정성을 높인다 2026-03-11 13:38:15 +09:00
klaus 8e1dabbb80 chore(version): 버전코드를 224로, 버전명을 1.51.1로 올린다 2026-03-09 10:43:57 +09:00
klaus b4d6ef62a1 fix(community): 전체보기 그리드 여백과 배경을 리스트와 맞춘다 2026-03-06 19:20:58 +09:00
klaus 066d1dfe1a fix(profile): 유저 프로필 라이브 카드에서 상세 페이지를 우선 노출한다 2026-03-06 17:38:16 +09:00
klaus 32f83a4612 docs(deeplink): 딥링크 안내 문구 변경 검증 기록을 추가한다 2026-03-06 16:56:04 +09:00
klaus 43c112eb25 fix(liveroom): 딥링크 이동 안내 문구를 단순화한다 2026-03-06 16:55:56 +09:00
klaus 2b5240a565 fix(deeplink): 딥링크 포그라운드 라우팅을 정비한다 2026-03-06 16:54:35 +09:00
klaus 93b620f4a8 feat(community): 커뮤니티 전체보기 리스트 그리드 전환 탭을 추가한다 2026-03-06 14:25:26 +09:00
klaus d8b2d53747 fix(live-room): 라이브룸 팔로우 버튼 룩앤필을 정렬한다 2026-03-05 15:41:21 +09:00
klaus d83c4b12ec fix(live): 종료 라이브 상대시간을 로컬 기준으로 국제화한다 2026-03-05 11:24:01 +09:00
klaus 2e700d4385 fix(live-room): 라이브 룸 팔로우 버튼 알림 상태를 반영한다 2026-03-05 11:01:26 +09:00
klaus 87bad6a959 fix(community): 전체 아이템 말줄임과 폰트를 정렬한다 2026-03-04 16:53:50 +09:00
klaus 0b3b4f8a1a chore(version): versionCode 223, versionName 1.51.0 2026-02-26 02:19:16 +09:00
klaus 5a70869dd8 fix(series-detail): 조회 실패 시 이전 화면으로 복귀한다 2026-02-26 02:17:33 +09:00
klaus 2a44494d88 chore(version): versionCode 222, versionName 1.51.0 2026-02-26 01:22:33 +09:00
klaus 96108aa520 feat(profile): 채널 후원 비밀문구와 내 페이지 노출 조건을 정리한다 2026-02-26 00:42:02 +09:00
klaus de4b301ccb docs(block): 차단 문구 수정 작업 기록을 정리한다 2026-02-25 22:31:11 +09:00
klaus 8153ad52ff fix(block): 사용자 차단 안내 문구를 역할별로 통일한다 2026-02-25 22:30:34 +09:00
klaus 4b2ef742d6 feat(profile): 크리에이터 상세정보에서 닉네임의 크기 32, SNS 아이콘 margin 16 2026-02-25 21:33:45 +09:00
klaus 092fc67b0b feat(profile): 채널 후원 영역과 전체보기 흐름을 추가한다 2026-02-25 20:57:30 +09:00
klaus 5b83ae69dd feat(profile): 크리에이터 상세정보를 노출한다 2026-02-25 15:39:37 +09:00
klaus c74d27f4ab fix(profile): 프로필 SNS 필드를 오픈채팅 기준으로 통일한다 2026-02-24 20:01:38 +09:00
klaus 63a52629a9 fix(live-room-create): 유료 라이브 30캔 미만 생성을 차단한다 2026-02-24 16:21:20 +09:00
klaus 80959abe16 fix(commit): AGENTS 규칙과 커밋 메시지 검사 스크립트를 정합화한다 2026-02-24 15:52:55 +09:00
klaus d048305193 .gitignore에서 docs 제거하여 문서를 버전 컨트롤에 저장하도록 수정 2026-02-24 14:04:47 +09:00
klaus a78a6638da 크리에이터 프로필 수정 시 팬심M 및 X URL 등록 기능 추가
크리에이터 프로필 수정 화면에서 팬심M과 X(구 트위터)의 URL을
입력하고 저장할 수 있도록 기능을 개선했습니다.

- ProfileUpdateRequest 및 ProfileResponse에 관련 필드 추가
- ProfileUpdateViewModel에 URL 관리 및 업데이트 로직 추가
- UI 레이아웃에 팬심M, X 입력 필드 추가 및 다국어 리소스 반영
- ProfileUpdateActivity에서 입력 필드 연동 및 초기값 설정
2026-02-23 11:16:30 +09:00
klaus 99f2715601 versionName 1.50.0, versionCode 220 2026-02-19 16:27:48 +09:00
klaus e5f8d798d5 로그인 후 메인 전환 방식을 안정화한다
로그인과 회원가입 성공 이후 메인 이동 플로우를 통일한다.
태스크를 명시적으로 재구성해 기기별 종료처럼 보이는 현상을 줄인다.
메인 전환 인텐트에 Activity 컨텍스트를 사용하도록 정리한다.
2026-02-10 17:57:24 +09:00
klaus d2ab5610c3 구글 로그인 회피 로직을 강화한다
승인 계정 우선 조회 후 전체 계정 재시도를 추가한다.
다른 계정 로그인 진입을 위해 구글 전용 옵션 경로를 제공한다.
Android 14 이상에서 Play 서비스 버전을 점검하고 업데이트를 유도한다.
2026-02-10 17:47:46 +09:00
klaus 5e43411854 구글 로그인 환경 점검을 추가한다
구글 로그인 시작 전에 필수 설정과 Google Play 서비스 상태를 확인한다.
사용자가 해결 가능한 오류에서는 시스템 다이얼로그를 표시한다.
인증 예외 로그에 예외 타입을 포함해 원인 추적성을 높인다.
2026-02-10 17:32:04 +09:00
klaus 39c09ef8e5 로그인 성공 후 메인 화면 이동을 통일한다
이메일 로그인과 소셜 로그인의 성공 이후 이동 동작을 일관되게 맞춘다.
모든 로그인 방식에서 동일한 화면 전환 플래그를 적용한다.
2026-02-10 17:17:08 +09:00
klaus 8c7602bb1a 라이브룸 V2V 번역 자막 기능을 추가한다
라이브룸에서 진행자 언어와 기기 언어가 다를 때 자막 토글을 제공한다.
룸 정보 응답에 V2V 워커 토큰과 진행자 언어 코드를 포함한다.
Agora V2V 에이전트 참여와 종료 API 연동을 추가한다.
2026-02-09 18:19:21 +09:00
klaus 1dcf16ba2a versionName 1.49.0, versionCode 219 2026-02-04 22:14:24 +09:00
klaus 181eb28828 라이브 상세 - 제목과 방패 줄 맞춤 2026-02-04 19:33:46 +09:00
klaus ae66f80c3c 크리에이터 채널 - 본인 채널에서 후원랭킹이 보이지 않던 버그 수정 2026-02-04 19:30:03 +09:00
klaus b32a3e5ea3 라이브 방이 19금일 때 제목 앞에 🔞 대신 방패(ic_shield)가 표시되도록 수정 2026-02-04 18:00:41 +09:00
klaus 48f7bf631e 댓글 입력 비어 있을 때 전송 방지 2026-02-04 17:10:30 +09:00
klaus fc43022a95 라이브 룸 - 라이브 크리에이터 프로필 영역에 팔로우 버튼 제거 2026-02-04 16:55:24 +09:00
klaus 9e867c3e16 라이브 텍스트 필드의 키보드가 올라가면 아이폰과 동일하게 화면이 위로 밀려 올라가게 수정 2026-02-04 16:47:19 +09:00
klaus b62dba096b 지금 라이브 중 아이템 - 닉네임과 제목을 가운데 정렬 하여 아이템의 크기와 관계 없이 비율이 맞아 보이도록 수정 2026-02-04 14:07:50 +09:00
klaus 553f49a469 비밀 후원을 보낼 때와 받을 때의 디자인 통일 2026-02-04 14:02:46 +09:00
klaus deb0ce2482 내 프로필 후원 영역 항상 표시
내 프로필에서는 후원 랭킹이 없어도 후원 영역을 노출한다
2026-02-04 11:36:12 +09:00
klaus 21c87f95ef 후원 랭킹 기간 선택 추가
프로필 후원 랭킹 조회와 프로필 갱신 요청에 기간 값을 전달한다.
2026-02-03 19:09:55 +09:00
klaus 84803c171c 라이브 상세, 라이브 룸 - 19금 표시를 이모지로 변경 2026-02-03 14:19:06 +09:00
klaus 94b48cef84 라이브 성별 제한 옵션 추가
라이브 생성/수정 요청에 genderRestriction을 추가한다.

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

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

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

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

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

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

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

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

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

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

제목 50자, 본문 72자, 빈 줄, 광고 금지 규칙을 준수한다.
2025-12-01 10:48:39 +09:00
klaus 2740522f05 fix(ui): 홈/메인 문자열 리소스화 및 영·일 번역 추가 2025-11-28 18:40:42 +09:00
klaus 6e3edd1e96 fix(auth): 로그인/회원가입 문자열 리소스화 및 영·일 번역 추가 2025-11-28 18:01:48 +09:00
klaus 0326fa89ea 커밋 메시지 구성 내용 추가 2025-11-28 17:59:52 +09:00
klaus dcdf3b21bc 국/영/일 다국어 문자열 리소스 추가 2025-11-28 17:32:36 +09:00
klaus 748da3ec0c add new file AGENTS.md 2025-11-28 17:23:49 +09:00
klaus 78c9b24bb9 feat(content-list-all): 정렬의 vertical padding을 12 -> 16으로 변경 2025-11-21 00:51:37 +09:00
klaus 6dc7c2578b feat(series-list-all): 시리즈 표시 어댑터를 최신 시리즈 표시 어댑터인 HomeSeriesAdapter로 변경 2025-11-21 00:23:56 +09:00
klaus 533da80986 feat(series-list-all): 완결시리즈 전체보기 페이지 추가 2025-11-20 18:26:49 +09:00
klaus b7107e3069 feat(latest-audio-content-all): 테마 UI 변경, 아이템 2단으로 변경 2025-11-20 02:49:33 +09:00
klaus bea9d8a709 feat(audio-content-all): 테마 UI 변경 2025-11-20 02:46:45 +09:00
klaus 1dc39cf786 feat(audio-content-all): 최신순/인기순 정렬 추가 2025-11-20 02:44:27 +09:00
klaus a15b478ac6 feat(audio-content-all): theme 추가 2025-11-20 02:32:42 +09:00
klaus 5fa4c42119 build: versionCode 204, versionName 1.44.0 2025-11-19 18:08:32 +09:00
klaus 43a734bcc4 feat(series-main-by-genre): margin 간격 16 -> 24로 수정 2025-11-17 23:56:13 +09:00
klaus 5c4cb7a8f9 feat(live-room-agora): rtcEngine!!.setParameters("{\"che.audio.aiaec.working_mode\":0}") 추가하여 에뮬레이터에서 소리가 나가지 않던 버그 수정 2025-11-17 22:27:36 +09:00
klaus cd8d2c255c feat(series-main): 추천시리즈, 요일별 시리즈 동일한 레이아웃을 사용하여 아이템 크기와 내용이 동일하게 표시되도록 수정 2025-11-17 21:32:03 +09:00
klaus b759e110f8 feat(live-room): 왕하트 안내 메시지 표시 시간 3초 -> 5초로 수정 2025-11-17 18:03:37 +09:00
klaus 0dd2bcf07a feat(live-room): 왕하트 애니메이션 수정
- 수신자 가운데 하트 크기 sizeDp 고정에서 0 -> sizeDp까지 서서히 커지도록 수정
2025-11-17 17:16:09 +09:00
klaus 77e9c9eb5d feat(live-room): 왕하트 애니메이션 수정
- 하트 비의 하트 개수를 80~100개 랜덤으로 수정
2025-11-17 17:00:43 +09:00
klaus bbb7858508 feat(live-room): 왕하트 애니메이션 수정
- 기존 가운데에서 한 번 폭발 후 비 내리는 애니메이션에서 가운데 + 랜덤 위치로 총 7번 폭발 후 비 내리는 애니메이션으로 수정
2025-11-17 16:57:27 +09:00
klaus 868b2d309a fix(home): fetchHome 후 불필요하게 콘텐츠 랭킹을 최신화하던 코드 제거 2025-11-17 16:03:13 +09:00
klaus 0cdc415a64 feat(chat-character): 작품별 탭 다시 추가 2025-11-13 23:04:41 +09:00
klaus 9b3d672e78 feat(chat-character): 캐릭터 신규 이미지 표시 UI 추가 2025-11-13 23:02:24 +09:00
klaus 0cfa5f8a32 feat(series-main): 시리즈 전체보기 장르별 탭 UI 및 데이터 2025-11-13 20:57:18 +09:00
klaus 907b718a3a feat(series-main): 시리즈 전체보기 페이지 추가
- 홈, 요일별, 장르별 탭 추가
- 홈 리스트 UI 및 데이터
- 요일별 UI 및 데이터
2025-11-13 18:27:04 +09:00
klaus fba6d86018 fix(home): 추천 콘텐츠 섹션 제목 - 추천 콘텐츠로 고정 2025-11-12 20:32:50 +09:00
klaus 51b81f2ab6 feat(series-all): 오직 보이스온에서만(오리지널 시리즈) 전체보기 추가 2025-11-12 17:47:18 +09:00
klaus ff16c70362 remove(series): 사용하지 않는 메서드 제거 2025-11-12 16:52:55 +09:00
klaus f928fac9da fix(audio-content): 전체보기 페이지 UI/API 구현 2025-11-12 15:26:02 +09:00
klaus a2262eff3f feat(home): 보온 주간 차트 콘텐츠 정렬 기준 추가
- 매출, 판매량, 댓글 수, 좋아요 수
2025-11-11 23:12:37 +09:00
klaus 62125f0873 feat(chat-character): 추천 캐릭터 섹션 추가 및 새로고침 API 반영 2025-11-11 17:17:18 +09:00
klaus f97f9296b6 feat(chat-character): 큐레이션 영역 제거 2025-11-11 16:26:30 +09:00
klaus 3353ebb777 feat(home): 홈 추천 콘텐츠 섹션 추가 및 API 연동 2025-11-11 16:25:52 +09:00
klaus 81760ec99d fix: 사용하지 않는 이전 콘텐츠 메인 연관 파일 제거 2025-11-10 21:10:16 +09:00
klaus 27f0d01e81 fix(home): 사용하지 않는 큐레이션 영역 제거 2025-11-10 19:57:17 +09:00
klaus 1bf653a5d8 fix(home): 홈에 포인트 대여 콘텐츠 섹션 추가 및 데이터 연동
- 무료 콘텐츠 아래 동일 UI로 섹션 추가
- 제목 ‘포인트’ 컬러 강조(무료 섹션과 동일)
- GetHomeResponse.pointAvailableContentList 사용해 데이터 바인딩
- 섹션 우측 ‘전체보기’ 텍스트 추가(클릭 액션 TODO)
2025-11-10 19:38:48 +09:00
klaus c35b267658 fix(login): 키보드 높이에 따라 화면을 위로 미는 로직이 BaseActivity, LoginActivity 두 군데에 있어서 2중으로 적용되는 버그 수정
- LoginActivity에 있는 키보드 높이에 따른 화면 Resize로직 제거
2025-11-10 18:51:21 +09:00
klaus 26f8d3dc45 version: versionCode 200, versionName 1.43.1 2025-11-06 18:35:31 +09:00
klaus 6620184fa0 temp(ai-chat): 작품별 탭 제거 2025-11-06 18:34:51 +09:00
klaus 1e8a96a52b fix(live-room): BIG_HEART_DONATION 메시지 heartMessage 3초, HEART_DONATION 1.5초 표시 적용
왜: BIG_HEART_DONATION 수신 시 heartMessage 표시 시간이 요구사항(3초)에 맞지 않았음. 무엇: heartMessage 표시 로직을 닉네임+표시시간 큐로 변경하고, HEART(1.5초)/BIG_HEART(3초)를 각 호출부에서 반영. 영향: 애니메이션 로직 변경 없음.
2025-11-06 17:35:08 +09:00
klaus c0d998345d fix(live-room): Path로 그리는 하트 크기 133dp -> 200dp, 표시 시간 0.15초에서 0.3초로 수정 2025-11-06 17:11:49 +09:00
klaus ed2258208b fix(live-room): 하트/캔 카운트 동시 업데이트 시 오차 수정
문제: LiveData.postValue 사용으로 연속 호출 시 병합(coalescing)으로 인해 로스트 업데이트가 발생하여 하트/캔 카운트 누락.
해결: ViewModel에서 메인 스레드 보장 후 setValue(value 할당)로 즉시 갱신하도록 변경. 비메인 스레드 호출 가능성에 대비해 mainHandler로 메인 재호출 처리.
영향: 빠르게 다수의 하트/캔 메시지가 도착해도 각 호출이 정확히 합산되며 오차 제거. 기존 서버 스냅샷 동기화(postValue)는 그대로 유지.
2025-11-06 16:37:17 +09:00
klaus f4244d5913 fix(live-room): Path로 그리는 하트 모양 보정 2025-11-06 16:15:28 +09:00
klaus b3a17b26dc perf(live-room): BIG_HEART 메시지 수신 경로를 Path 드로잉으로 전환하여 메모리 절감 2025-11-06 16:04:22 +09:00
klaus a52f9425e8 fix(live-room): BIG_HEART 메시지 수신 되면 WaterWaveView 대신 임시 하트 뷰를 중앙에 표시 후 폭발 실행 2025-11-06 15:08:00 +09:00
klaus 48eb959ab2 fix(live-room): 잘못 사용 되어 효과가 없는 mutex 제거 2025-11-06 13:25:15 +09:00
klaus 0f30cf3880 fix(chat): IME 인셋 병합으로 키보드 표시 시 입력 영역 가림 문제 수정
- BaseActivity의 WindowInsets 리스너에서 systemBars와 ime 인셋의 각 방향별 최대값을 루트 패딩에 반영
- Edge-to-Edge 환경에서 하단 패딩이 키보드 높이만큼 확보되도록 개선
- ChatRoomActivity의 deprecated 설정 없이도 동작 유지
2025-11-05 11:56:36 +09:00
klaus 80431b7e83 refactor(live-room-like-heart): 하트 비의 하트와 폭발시 생기는 하트 파편을 동일한 모양으로 리팩토링 2025-11-05 01:07:03 +09:00
klaus c4fc075844 feat(live-room-like-heart): 폭발 후 하트 비/우박 애니메이션 반영 2025-11-05 00:57:30 +09:00
klaus a24b1a3b4e feature(live-room-like-heart): 롱프레스 왕하트 애니메이션 추가
- 물 채우기 애니메이션이 끝난 후 폭발 이펙트 추가
- 왕하트를 받은 크리에이터 및 다른 사람은 1초 동안 하트에 물이 채워지는 애니메이션이 수행된 후 폭발 이펙트가 실행된다.
2025-11-04 22:47:32 +09:00
klaus 601405349e feature(live-room-like-heart): 롱프레스 왕하트 애니메이션 변경
- 기존: 하트가 33.3dp 부터 커지는 애니메이션
- 변경: 하트가 133.3dp으로 고정되어 있고 물 채우기 애니메이션
2025-11-04 20:20:58 +09:00
klaus 332bf3256c fix(like-heart): 터치/클릭 충돌 해결 및 길이 기반 롱프레스 분기
- 1초 미만 터치 시 `handleHeartClick()` 실행되도록 수정
- 1초 경과 후에만 중앙 하트 표시 및 스케일 업데이트 시작
- ACTION_CANCEL 시 예약 러너블 취소, 중앙 하트 제거, 클릭/롱프레스 미실행
- 2초 이상 유지 시 기존 BIG HEART 트리거 로직 유지
- 가드 추가: `isLongPressBlockedByAvailability` 케이스 안전 처리
- 러너블/타이밍 추가: `showCenterHeartRunnable`, `longPressVisualStartTime`
2025-11-03 19:00:09 +09:00
klaus 6653ca2c11 feat(live-room): 하트를 길게(2초)간 누르면 표시 되는 왕하트(100캔) 추가, 애니메이션 제외 2025-11-03 16:23:44 +09:00
klaus d6e9a63b1f feat(object-box): 사용하지 않는 object box 모델 삭제 2025-11-03 11:17:57 +09:00
klaus 5cc9f83a64 build(version): versionCode 199, versionName 1.43.1 2025-11-01 23:55:01 +09:00
klaus da04cbcec0 feat(chat-작품별): 이미지 표시할 때 crossfade를 제거 2025-11-01 23:54:06 +09:00
klaus 1eff6702d7 feat(git): gitignore에 .kotlin/ 폴더는 git에서 관리하지 않도록 추가 2025-11-01 23:52:08 +09:00
klaus 6242c19397 feat(ai-chat): 임시로 제거했던 작품별 탭 다시 추가 2025-11-01 23:38:29 +09:00
klaus 194c4bad84 feature(agora): rtc version 4.5.2 2025-10-31 14:17:04 +09:00
klaus 1b7ba7825e feature(version): versionCode 198, versionName 1.43.0 2025-10-30 17:21:05 +09:00
klaus 5689dd10a5 feature(home): 지금 라이브 중인 라이브의 이미지를 크리에이터의 프로필 이미지가 표시되도록 수정 2025-10-30 17:02:29 +09:00
klaus 648064eac7 feature(version): versionCode 197, versionName 1.43.0 2025-10-30 16:01:10 +09:00
klaus 1ca6d068d0 live-room(agora): rtm version 1.5.3 -> 2.2.6 2025-10-30 14:54:21 +09:00
klaus f08c481807 refactor(agora): 코드 파악을 좀 더 쉽게 할 수 있도록 코드 재배치 2025-10-27 23:07:44 +09:00
klaus f64b28af1b feat(live-room): 사용하지 않는 후원현황 채팅 제거 2025-10-27 18:13:07 +09:00
klaus 2a50d0f5a0 build(live-room): agora rtc voice-sdk library version up
- voice-sdk:4.6.0
2025-10-24 01:19:39 +09:00
klaus 149d7358f0 build, fix(app): targetSdk 35 업그레이드 점검 및 Android 15 정확 알람 호환성 보완, Android 15 대응 보완
- 정확 알람 예외 처리 및 백그라운드 서비스 시작 회피
- setAlarmClock 호출부 SecurityException 처리 추가(1회/반복 알람)
- 401 응답 시 startService → stopService로 변경해 O+/15 백그라운드 서비스 제약 회피
2025-10-24 00:45:11 +09:00
klaus a86e55eeae build(app): library upgrade
media3-session:1.8.0
media3-exoplayer:1.8.0
mockito-core:5.20.0
mockk:1.14.6
2025-10-24 00:28:21 +09:00
klaus 3979d37e76 build(app): library upgrade
firebase-bom:33.16.0
androidx.room:2.8.3
kotlinx-coroutines-android:1.10.2
af-android-sdk:6.17.4
2025-10-24 00:19:00 +09:00
klaus d8d05b57cb build(app): library upgrade
media:1.7.1
core-ktx:1.16.0
appcompat:1.7.1
recyclerview:1.4.0
material:1.13.0
constraintlayout:2.2.1

webkit:1.14.0
lifecycle-livedata-ktx:2.9.4
lifecycle-viewmodel-ktx:2.9.4

gson:2.13.2
retrofit:3.0.0
converter-gson:3.0.0
adapter-rxjava3:3.0.0
logging-interceptor:5.2.1
tedpermission-normal:3.4.2
2025-10-24 00:02:40 +09:00
klaus f1d718a45f build(app): bump compileSdk/targetSdk to 35
- compileSdk 35, targetSdk 35로 상향
- edge-to-edge를 적용하고 전체 화면에 insets를 추가 적용하여 이전과 동일하게 statusbar, navigationbar를 침범하지 않도록 처리
2025-10-23 23:32:58 +09:00
klaus d33ab59378 fix(in-app-purchase): 인 앱 결제 완료 후 충전내역으로 이동하도록 코드 수정 2025-10-23 14:10:29 +09:00
klaus f8e4a4fd45 build: versionCode 196, versionName 1.43.0 2025-10-23 14:09:39 +09:00
klaus 6d099e0aab build: versionCode 195, versionName 1.43.0 2025-10-23 12:06:11 +09:00
klaus c5eb9767aa fix(iap): 인 앱 결제 라이브러리 버전 8.0.0 적용, 결제 보완사항 적용 — 즉시 소비, ITEM_ALREADY_OWNED 처리, obfuscatedAccountId 설정
- 구매 성공 직후 consume 처리하여 재구매 불가(ITEM_ALREADY_OWNED) 이슈 완화
- ITEM_ALREADY_OWNED 응답 시 미소비 구매 자동 정리 및 안내 메시지
- BillingFlowParams에 obfuscatedAccountId 설정으로 계정 연계 강화
- 서비스 연결 문제에 대한 사용자 메시지 보강
2025-10-22 23:40:14 +09:00
klaus 24672b7cf2 build(gradle): jvmTarget를 compilerOptions+jvmToolchain으로 마이그레이션
Kotlin 2.x에서 deprecated된 `kotlinOptions.jvmTarget` 사용을 제거하고
최신 DSL(`kotlin { jvmToolchain(17); compilerOptions { jvmTarget = JVM_17 } }`)로 전환했습니다.

왜: Kotlin Gradle Plugin 2.x에서 `kotlinOptions.jvmTarget`가 deprecated되어 빌드 경고가 발생했습니다.
무엇: `app/build.gradle`의 `kotlinOptions { jvmTarget = ... }` 제거 후, 최상위 `kotlin` 블록을 추가하여
- `jvmToolchain(17)` 설정
- `compilerOptions { jvmTarget.set(JVM_17) }` 적용
영향: 컴파일 타깃과 JDK 툴체인을 명시적으로 17로 고정하여 빌드 일관성을 확보하고 경고를 제거합니다.
2025-10-22 21:05:08 +09:00
klaus db6de22273 사용하지 않는 databinding 설정 제거 2025-10-22 20:51:19 +09:00
klaus 8cdb82765f feat(build): agp 8.13.0, gradle-wrapper 8.14.3 업그레이드 2025-10-22 20:41:19 +09:00
klaus 172d7c0b80 feat(build): kotlin 2.2.20, agp 8.11.1 업그레이드 2025-10-22 20:08:40 +09:00
klaus cf86dd3f30 fix(room): Kotlin 2.1/KSP 2.0 환경에서 KSP 오류 해결을 위해 Room 2.7.0으로 업그레이드
Kotlin을 2.1.21, KSP를 2.1.21-2.0.2로 올린 뒤 발생한
`unexpected jvm signature V` 예외를 해결하기 위해 Room(compiler, runtime, ktx, rxjava3)
버전을 2.6.1 → 2.7.0으로 업그레이드.

빌드가 정상 완료되며 KSP 태스크도 성공적으로 수행됨을 확인함.
2025-10-22 19:39:50 +09:00
klaus 23c05b91d5 build(room): KSP room.schemaLocation 설정 및 exportSchema=true로 스키마 export 활성화
프로젝트가 이미 KSP를 사용하고 있어 KSP 인수 기반으로 Room 스키마 export를 활성화했습니다.
- app/build.gradle: ksp { room.schemaLocation 등 } 추가
- Room DB 클래스 3종: exportSchema=true
- app/schemas 디렉터리 버전 관리
2025-10-22 19:23:58 +09:00
klaus 7ff3d7f1e5 refactor(root-gradle): deprecated 문법 신규 문법으로 전환
- task -> tasks.register로 전환
- rootProject.buildDir -> rootProject.layout.buildDirectory로 수정
2025-10-22 18:11:37 +09:00
klaus 912518c1ae refactor(config): buildConfig 설정 위치 권장 설정 위치로 변경
- 기존: gradle.properties android.defaults.buildfeatures.buildconfig=true

- 변경: build.gradle buildFeatures { buildConfig = true }
2025-10-22 16:33:46 +09:00
klaus 9b825ee244 refactor(db): ObjectBox 제거 및 Room으로 마이그레이션
- 최상위/app Gradle에서 ObjectBox 플러그인 제거
- PlaybackTracking을 Room Entity/DAO/Database로 전환
- Repository를 Room 기반으로 수정 및 Koin DI 주입 변경
2025-10-22 16:25:32 +09:00
klaus bc581d763b fix(build): Room KAPT→KSP 마이그레이션 및 configuration cache 비활성화로 Kotlin 2.0 빌드 오류 해결
- Room을 2.6.1로 업데이트하고 KAPT를 KSP로 전환
- room-rxjava3 의존성 추가(RxJava3 반환 타입 지원)
- ObjectBox 플러그인과 충돌 회피를 위해 configuration cache 비활성화
- AGP 8.4.2 + Kotlin 2.0.21 환경에서 빌드 성공 확인
2025-10-22 13:50:42 +09:00
klaus dd236d8f19 feat(live-reservation-all): 주간 캘린더 라이브러리 제거 및 개별 구현 2025-10-22 12:12:02 +09:00
klaus ff236ee6a1 remove audio visualizer 2025-10-21 15:31:57 +09:00
klaus 66a6f992eb feat: versionCode 194, versionName: 1.42.1 2025-10-21 11:17:19 +09:00
klaus c6438bef67 fix(home): 인기 캐릭터 -> 인기 캐릭터 채팅 2025-10-20 22:33:02 +09:00
klaus ee5490939b fix(ChatRoom): 채팅 quota 구매 캔 개수 표시 수정
- 기존: 30결제하고 바로 대화 시작 -> 수정: 10(채팅 12개) 바로 대화 시작
2025-10-20 21:44:25 +09:00
klaus 65a2b47045 fix(GetHomeResponse): Character 클래스가 잘못 import 되어 있던 것 수정 2025-10-20 20:15:44 +09:00
klaus a56c21f856 feat(user-profile): 팔로워 수 문구 팔로워 OO에서 팔로워 OO명으로 변경 2025-10-20 19:20:23 +09:00
klaus 7e501c794d feat(user-profile): 팬 Talk 답변 글 배경색 변경 2025-10-20 19:18:02 +09:00
klaus c07fb33968 feat(user-profile): 더보기 버튼 흰색으로 변경 2025-10-20 18:58:14 +09:00
klaus 7ecb36a7be feat(home): 인기 캐릭터 색션 추가 2025-10-20 18:57:09 +09:00
klaus 1cec07f8c5 feat(user-profile): 팔로우/팔로잉 버튼 변경 2025-10-20 14:28:05 +09:00
klaus ddcf191ade feat(user-profile): 최신콘텐츠 좋아요, 댓글 아이콘 크기 24x24 -> 18x18로 변경 2025-10-20 14:07:01 +09:00
klaus 945e3bd239 feat(temp): 작품별 탭 임시 제거 2025-10-17 14:44:49 +09:00
klaus 09ed73300d feat(user-channel): 팬 Talk 섹션 아이템 UI 수정 2025-10-17 09:22:41 +09:00
klaus 83fa3b870c feat(home): 인기 크리에이터 섹션 아이템 팔로우 버튼 표시 조건 추가
- 크리에이터 != 나 인 경우에만 팔로우/팔로잉 버튼 표시
2025-10-17 04:42:59 +09:00
klaus cb67787925 feat(user-channel): 유저 채널 상단 툴바 오른쪽 상단 공유/메뉴 아이콘 정렬 수정
- LinearLayout으로 감쌈
- 메뉴 아이콘이 없어도 공유 아이콘이 오른쪽 상단에 위치할 수 있도록 정렬
2025-10-16 23:54:42 +09:00
klaus ad053ef889 feat(user-channel): 유저 채널 라이브 아이템 터치 이벤트 추가 2025-10-16 23:52:25 +09:00
klaus ae92921b7b feat(user-channel): 유저 채널 UI 수정
- 최신 콘텐츠 아이템 표시
- 후원 순위 아이템 사이즈 수정
- 섹션 제목 사이즈 업
2025-10-16 23:30:58 +09:00
klaus 9ba053b807 feat(user-channel): 유저 채널의 라이브 아이템 UI 수정 2025-10-16 19:00:46 +09:00
klaus 2b8b581082 feat(user-channel): 유저 채널의 프로필 이미지 사이즈와 섹션 순서 변경 2025-10-16 00:13:29 +09:00
klaus 0b775ed380 fix(payverse-webview): webView 세팅 조정을 통해 네이버페이가 동작하지 않던 버그 수정
- 참고: line 315 ~ 325
2025-10-15 15:39:34 +09:00
klaus a90f4b1c5a fix(creator-community-write): 이미지를 선택하면 recordAudio영역이 보이도록 수정 2025-10-13 11:06:30 +09:00
klaus 5bc2b385fa feat(can): 사용 하지 않는 price 값 제거
feat(webview): payverse:// 스킴은 앱이 있으면 앱을 실행하도록 처리
2025-10-03 00:04:43 +09:00
klaus 21f57444c8 feat(can-payment): 다국적 통화 표기 지원 및 결제 금액 표시 개선
- KRW 고정 표기에서 벗어나 PG/해외 결제 등 다양한 통화 표기를 정확히 지원하기 위함
2025-10-02 17:14:49 +09:00
klaus 662f18bceb feat(can-charge): 이롬넷(Payverse) 통합결제 추가 2025-10-01 01:47:42 +09:00
klaus 2635b7d3c3 versionCode 191, versionName 1.42.1 2025-09-25 12:06:14 +09:00
klaus aac3910b43 feat(original): 작품별 상세 UI
- 블러 처리한 배경의 세로 크기 절반으로 축소
2025-09-24 17:00:35 +09:00
klaus 0319981650 feat(original): UI 변경
- 캐릭터 / 작품 정보 탭 추가
- 작품 정보 탭 구성
  - 작품 소개
  - 원작 보러 가기
  - 상세 정보
    - 작가
    - 제작사
    - 원작
2025-09-19 18:35:18 +09:00
klaus 44e209d7b1 fix(ImagePickerCropper): openDocument 제거, excludeGif가 true이고 GIF 선택시 "GIF는 지원하지 않습니다." 메시지 반환 2025-09-18 22:02:54 +09:00
klaus 0f170c6daa fix(프로필 수정): gif 선택이 불가능 하도록 수정 2025-09-18 01:26:43 +09:00
klaus 67109bfe3c fix(Manifest): com.yalantis.ucrop.UCropActivity 추가 2025-09-18 01:02:18 +09:00
klaus d22907c7d5 fix(이미지 선택): 이미지 선택 및 크롭 로직 수정 2025-09-18 00:17:20 +09:00
klaus 02155065f7 fix(liveroom-create): 경고 제거 2025-09-17 19:01:30 +09:00
klaus 3c21b36e88 fix: 라이브 생성 이미지 선택
- 이미지 선택 및 Crop 방법 변경
2025-09-17 18:49:36 +09:00
klaus 93fa042522 feat(character): 신규 캐릭터 전체보기 페이지 GRID
- 3단 구성에서 2단구성으로 변경
2025-09-17 02:45:45 +09:00
klaus dcde2b125e feat(chat-original): 원작 상세 화면 및 캐릭터 무한 스크롤 로딩 구현 2025-09-15 19:19:00 +09:00
klaus f15c6be1a4 feat(chat-original): ChatFragment에 작품별 탭 및 리스트 UI/API 연동 추가
- ChatFragment에 '작품별' 탭 추가 및 프래그먼트 스위칭 로직 반영
- /api/chat/original/list API, 모델, 레포지토리, ViewModel 추가
- OriginalTabFragment/Adapter/레이아웃 구현 (3단 그리드, 간격 16dp, 이미지 라운드 16dp, 아이템 이미지의 레이아웃 비율을 306:432)
- 스크롤 끝 감지를 구현하여 무한 스크롤을 지원
2025-09-15 16:21:54 +09:00
klaus 05208d3031 feat(chat-character): 신규 캐릭터 전체보기 화면 및 API 연동 추가 2025-09-13 02:15:01 +09:00
klaus 2b892fe783 feat(character): 본인인증 하지 않은 유저가 캐릭터 상세보기로 들어갈 때 본인인증 팝업 띄움 2025-09-12 01:13:08 +09:00
klaus c3c19db730 feat(icon): 앱 아이콘 변경 2025-09-12 01:12:29 +09:00
klaus b70c8058e8 feat(splash): 스플래시 페이지 수정 2025-09-11 22:16:00 +09:00
klaus cdc59d0877 fix(main): 라이브 탭 <-> 채팅 탭 순서 변경 2025-09-11 20:06:16 +09:00
klaus 88d13ce77a fix(character): 인기 캐릭터
- TextView 숫자 하단 여백(descent) 제거
2025-09-11 20:04:13 +09:00
klaus f830c98b8e fix(character-detail): 캐릭터 정보
- 캐릭터 이름과 MBTI 사이 간격 8로 수정
2025-09-11 14:58:30 +09:00
klaus 8de0dc2242 feat(chat): Talk 탭에 RecyclerView 스크롤 페이지네이션 추가
- /api/chat/room/list 호출에 page 파라미터 적용 (0부터 시작)
- ViewModel에 currentPage/lastPageReached 상태 추가 및 append 로직 구현
- Fragment에 스크롤 리스너로 바닥 근접 시 다음 페이지 자동 로드
- 빈 데이터 시 마지막 페이지로 간주하여 추가 로딩 중단
2025-09-11 14:38:29 +09:00
klaus 56e99912d4 "fix(chat-room): 쿼터 UI를 totalRemaining 대신 nextRechargeAtEpoch 기준으로 갱신 2025-09-10 13:51:07 +09:00
klaus 9ed3c046b3 fix(chat-room): 채팅방
- 쿼터 상태 조회, 쿼터 구매 API URL 변경
2025-09-10 12:03:49 +09:00
klaus 65791c55ca feat(ui): enforce 2:3 aspect ratio and center chatroom background
- item_character_gallery.xml: set iv_image to 2:3
- activity_chat_room.xml: apply H,2:3 ratio and center frame by constraining top/bottom to parent
- item_chat_background_image.xml: set picker item to 2:3
- align dim view constraints to match background area
2025-09-05 18:30:58 +09:00
klaus 0422746267 fix(chat-room settings): 배경 사진 -> 배경 이미지 로 변경 2025-09-05 18:27:22 +09:00
klaus cc3aca34f5 fix(character-detail): 캐릭터 정보 추가
- mbti, 나이, 성별 추가
2025-09-05 17:43:04 +09:00
klaus e39bdb6b03 fix(character-detail): 상단 툴바 제목을 "캐릭터 정보"로 고정 2025-09-05 14:16:25 +09:00
klaus 27a36d2d44 fix: place_holder 변경 2025-09-05 12:52:33 +09:00
klaus 60b7bb7e7e fix(character): 캐릭터 이미지 RoundedCorner 16dp 적용 2025-09-05 12:48:26 +09:00
klaus 8ebaaefd6f fix(character-main): 큐레이션 섹션 데이터 이름 수정
- CurationSection.kt
- id -> characterCurationId
2025-08-29 14:47:05 +09:00
klaus 201ab488b2 fix(character-main): 최근 대화 캐릭터
- 터치시 채팅방이 아닌 캐릭터 상세 페이지로 이동
2025-08-28 20:00:22 +09:00
klaus 8b241709e1 fix(chat): 대화 설정
- 대화 초기화 오른쪽에 30캔 안내 추가
2025-08-28 01:53:45 +09:00
klaus d9cb12e882 fix(chat): 채팅방 입장 시 서버 멤버 정보로 캔 배지 동기화
- ChatRoomActivity에서 getMemberInfo 호출 추가
- 응답 성공 시 SharedPreferenceManager.can/point 갱신 및 헤더 배지 즉시 반영
- 네트워크 실패 시 UI 흐름 방해 없이 조용히 무시 처리
2025-08-28 00:46:52 +09:00
klaus 5c78c567ca fix(chat): 대화 초기화 성공 시 로컬 데이터 삭제 및 로딩 다이얼로그 적용
- ChatMessageDao: deleteMessagesByRoomId(roomId) 추가
- ChatRepository: clearMessagesByRoom(roomId) 추가
- ChatRoomActivity:
  - clearLocalPrefsForRoom(roomId) 구현
  - reset 플로우에 Prefs/DB 삭제 체인 연결
  - onResetChatRequested()에서 LoadingDialog 표시 및 doFinally로 닫힘 보장
2025-08-28 00:23:14 +09:00
klaus e3bcc6d3a6 사용하지 않는 함수 삭제 2025-08-27 16:50:54 +09:00
klaus 05e8874d81 fix(chat): 대화 초기화 성공 시 방별 로컬 데이터(배경/공지/메시지) 삭제 처리
- ChatMessageDao: deleteMessagesByRoomId(roomId) 추가
- ChatRepository: clearMessagesByRoom(roomId) 추가
- ChatRoomActivity: clearLocalPrefsForRoom(roomId) 구현 및 reset 플로우에 Prefs/DB 삭제 체인 연결
- 요구사항: 대화 초기화 API 성공 시 해당 방의 배경 데이터와 로컬 메시지 등 모든 관련 데이터 제거
2025-08-27 16:49:27 +09:00
klaus 88e3ae7b51 fix(chat): 배경 선택 다이얼로그에서 초기 선택 복원이 되지 않는 문제 수정
- 선택 상태를 URL 비교에서 이미지 ID 우선 방식으로 변경
- URL만 저장된 기존 데이터에 대해 목록 로드 후 URL→ID 마이그레이션 추가
- SharedPreferences에 chat_bg_image_id_room_{roomId} 키 도입(호환 위해 URL 키 유지)
2025-08-27 15:53:43 +09:00
klaus 02df0b6774 feat(chat): 메시지 괄호 지문 색상을 회색으로 변경 2025-08-27 14:10:30 +09:00
klaus a941d0bfab feat(chat): 채팅방 배경 사진 변경 기능 추가
- ChatRoomMoreDialog에서 배경 사진 변경 Picker 연결
- my-list API 추가 및 Repository 위임 추가
- 배경 선택 Dialog(3열 Grid, 4:5 비율) 및 선택 상태 UI 구현
- SharedPreferences로 roomId별 배경 URL 저장/로드
- ChatRoomActivity에 배경 저장/적용 헬퍼 추가 및 기본 프로필 적용 로직 구현
2025-08-27 02:37:20 +09:00
klaus 2e837bec5d feat(chat-quota): 쿼터 연동 및 카운트 다운 / 쿼터 구매 UX 개선(+5초 표시 보정)
- TalkApi: /api/chat/quota/me, /api/chat/quota/purchase 엔드포인트 추가
- Repository: getChatQuotaStatus(), purchaseChatQuota() 추가, sendMessage 응답 타입을 SendChatMessageResponse로 전환
- Model: ChatQuotaStatusResponse/ChatQuotaPurchaseRequest 추가, SendChatMessageResponse/ChatRoomEnterResponse 기본값 추가
- UI(Adapter): QuotaNotice 뷰타입/레이아웃 추가, 안정 ID/부분 갱신(payload) 적용, Change 애니메이션 비활성화로 깜빡임 최소화
- UI(Activity): 쿼터 0 시 입력창 숨김 + 안내 노출, 00:00:00 도달 시 /quota/me 조회
- 카운트다운 계산: epoch 기반 남은 시간 계산 + 표시용 +5초(DISPLAY_FUDGE_MS) 가산
- 구매 성공 시 로컬 30캔 차감 및 헤더 배지 즉시 갱신
2025-08-26 21:36:31 +09:00
klaus 9b1a83bd69 feat(chat-room): 대화 설정 다이얼로그 구현 및 채팅방 초기화 API 연동
- MoreDialog UI 구성 및 동작(배경 스위치/변경, 대화 초기화, 신고하기)
- 방별 배경 표시 SharedPreferences 저장 및 화면 반영
- TalkApi에 resetChatRoom 엔드포인트 추가, Repository 메서드 추가
- ChatRoomActivity와 다이얼로그 연동, 초기화 플로우 구현
2025-08-26 13:37:58 +09:00
klaus b3553f80c6 feat(chat): 채팅방 상단 캔 배지 및 더보기 전체화면 다이얼로그 추가
- 헤더 우측에 캔 배지(tv_can_badge)와 더보기(iv_more) 추가
- 캔 배지 스타일 적용(배경 #263238, 텍스트 white, v5/h8 패딩, can 아이콘)
- 더보기 클릭 시 전체화면 다이얼로그 표시(플레이스홀더 UI)
2025-08-26 12:11:43 +09:00
klaus 5d76ff1590 feat(chat): AI 유료/이미지 메시지 및 구매 플로우 추가
- ServerChatMessage/ChatMessage에 messageType/imageUrl/price/hasAccess 필드 반영
- TalkApi/Repository: 유료 메시지 구매 API 연동 및 성공 시 로컬 DB 반영
- ChatRoomActivity: 구매 팝업 SodaDialog 적용(취소/잠금해제) 및 구매 성공 시 메시지 교체
- ChatMessageAdapter: 이미지 렌더링(라운드 10dp), 유료 오버레이(가격+"눌러서 잠금해제") 처리,
  구매/캐러셀 오픈 콜백 추가
- 구매된 이미지 클릭 시 전체화면 캐러셀 지원
- item_chat_ai_message.xml: 메시지 UI 최대 90% 폭, 시간 텍스트 배치 개선, 이미지 4:5 비율 적용
- 그룹 메시지 간 간격 절반 적용(ItemDecoration)
- Room DB v2 마이그레이션: messageType/imageUrl/price/hasAccess 컬럼 추가로 재입장 시 표시 문제 해결

왜:
- 유료/이미지 메시지 기능 제공 및 일관된 구매 경험 필요
- 재입장 시 이미지/유료 정보 누락 문제(DB 정합) 해결
- 시간 잘림/배치 문제와 그룹 간격 시인성 개선
2025-08-25 17:22:56 +09:00
klaus 6c57c5a98a feat(character-gallery): 구매 이미지 전체화면 Carousel 뷰어 추가
구매된 이미지를 탭하면 전체화면 DialogFragment로 열리고,
ViewPager2 기반 Carousel로 좌우 슬라이딩 탐색이 가능하도록 구현.
2025-08-23 01:48:57 +09:00
klaus 770c4179a3 fix(gallery): 구매 다이얼로그를 AlertDialog에서 SodaDialog로 교체
디자인 일관성 및 공통 컴포넌트 적용을 위해 갤러리 탭의 구매 확인 다이얼로그에
SodaDialog를 사용하도록 변경
2025-08-22 22:17:31 +09:00
klaus 9164942395 feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용
Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제.
Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
2025-08-22 22:12:36 +09:00
klaus e3ed816fb3 feat(gallery): 캐릭터 이미지 구매 기능 추가
갤러리 아이템의 구매 버튼 클릭 시 확인 다이얼로그를 표시하고,
확인 시 /api/chat/character/image/purchase API를 호출하여 이미지 URL을 갱신.
구매 성공 시 isOwned=true 처리 및 보유 비율/개수 업데이트.
2025-08-22 21:49:44 +09:00
klaus 13ee098cfc feat(character-gallery): 갤러리 탭 UI/페이징 및 API 연동, DI 적용
- API: CharacterApi에 이미지 리스트 API 추가(characterId, page, size)
- VM: 페이징(loadInitial/loadNext), 요청 중복 방지, 마지막 페이지 판단, 누적 리스트 관리
- UI: ProgressBar(배경 #37474F/진행 #3BB9F1, radius 999dp, 비활성) + 좌/우 텍스트 구성
- Grid 3열 + 2dp 간격, item 4:5 비율, 잠금/구매 버튼 UI 적용
- UX: tv_ratio_right에서 ownedCount만 #FDD453로 강조(white 대비)
2025-08-22 17:03:01 +09:00
klaus f917eb8c93 fix(character-detail): characterId 전달 및 상세 탭 전환 로직 수정
fix(character-detail): 탭 전환 시 프래그먼트 캐싱하여 재로딩 방지

CharacterDetailFragment에 newInstance(characterId) 도입 및 ARG 전달 구조 추가.
Fragment에서 잘못된 intent 참조 제거하고 arguments → activity.intent 순으로 안전하게 조회.
Activity 초기 진입 시 상세 탭 로딩 경로 정리 및 characterId 유효성 검사 시 종료 처리 보강.

replace 기반 교체를 add/show/hide 구조로 전환.
TAG_DETAIL/TAG_GALLERY로 인스턴스를 식별하여 FragmentManager 복원/재사용.
탭 이동 시 기존 인스턴스 표시만 수행하여 onViewCreated 재호출/네트워크 재요청 방지.
2025-08-22 15:23:17 +09:00
klaus 989a0f361b feat(character-detail): 캐릭터 상세
- 탭 UI 추가
2025-08-22 03:39:36 +09:00
klaus 52c1f61109 feat(report): 캐릭터 댓글 신고 사유를 라디오 버튼으로 변경 및 비활성 시각화
- 댓글 신고 사유 리스트 변경
- 댓글 신고 사유 선택 UI를 RadioGroup/RadioButton으로 전환
- 선택 전 신고 버튼 비활성화 및 alpha 적용으로 시각적 비활성화 처리
- 선택 시 버튼 활성화 및 alpha 복구
2025-08-22 03:04:50 +09:00
klaus 7dd6d46a5f fix(talk-tab): 채팅방 리스트
- 채팅방 사이 간격 24
- 이미지 원형으로 변경
2025-08-22 02:34:05 +09:00
klaus 3a1943ba87 refactor(character-comment): 캐릭터 댓글/답글 리스트
- 배경색 변경
- 댓글 사이 간격 조정
2025-08-20 18:38:16 +09:00
klaus ab1dd04a60 refactor(character-comment): 답글 리스트 MVVM 적용 및 ViewModel 추가
- CharacterCommentReplyViewModel 추가: 로딩/토스트/페이지네이션/CRUD 로직 이관
- AppDI Koin 모듈에 Reply ViewModel 등록
- CharacterCommentReplyFragment에서 Repository 직접 접근 제거 및 바인딩 로직 추가
2025-08-20 16:49:51 +09:00
klaus ccd88dad47 refactor(chat/character): 댓글 리스트 화면에 ViewModel 도입 및 Fragment-Repository 직접 의존 제거
CharacterCommentListViewModel을 추가하여 댓글 조회/등록/삭제/신고 및 페이지네이션 로직을 ViewModel로 이전.
Fragment는 UI 업데이트와 사용자 입력 처리에 집중하도록 리팩토링.
Koin DI에 ViewModel 등록.
2025-08-20 16:22:34 +09:00
klaus fdc9ba80e0 fix(comment): 답글 더보기 Bottom Sheet 적용 및 삭제/신고 API 연동
답글 리스트에서 PopupMenu를 Bottom Sheet로 통일하고, 내 답글은 삭제, 타인 답글은 신고 메뉴만 노출하도록 변경.
삭제는 원 댓글 삭제와 동일한 API(deleteComment)를 사용하며, 신고는 reportComment로 연동.
2025-08-20 15:55:59 +09:00
klaus d1c62fd2b6 fix(comment): 캐릭터 댓글 신고 BottomSheet가 표시되지 않는 문제 수정
- childFragmentManager 대신 parentFragmentManager로 신고 BottomSheet 표시
- BottomSheet dismiss 직후 show 트랜잭션 충돌/우선순위 이슈 완화
2025-08-20 15:33:47 +09:00
klaus 3e2cdd502c fix(character-comment): 캐릭터 댓글 수 표시 수정
- 서버에서 받아온 댓글 수를 표시하도록 수정
2025-08-20 14:00:46 +09:00
klaus c78aed2551 fix(comment): 캐릭터 댓글 더보기에서 삭제 API 연동 및 UI 반영
- Bottom Sheet 삭제 선택 시 deleteComment API 호출 추가
- 성공 시 목록에서 항목 제거
- 오류 시 사용자에게 에러 토스트 노출
2025-08-20 03:19:23 +09:00
klaus e881178f2a feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
feat(character-comment): 답글 작성 API 연동 및 성공 시 낙관적 UI 반영

- CharacterCommentReplyFragment에 listReplies API 연동
- 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드
- isLoading 플래그로 중복 요청 방지
- 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가

- CharacterCommentReplyFragment에서 createReply API 호출로 스텁 제거
- 요청 중 로딩 다이얼로그 표시, 성공 시 입력 초기화 및 리스트에 즉시 추가
- 에러 처리(토스트) 적용
2025-08-20 03:07:35 +09:00
klaus b995a0b151 feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
- CharacterCommentReplyFragment에 listReplies API 연동
- 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드
- isLoading 플래그로 중복 요청 방지
- 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가
2025-08-20 02:48:01 +09:00
klaus ec315c4747 feat(character-comment): 캐릭터 댓글 리스트 등록/목록/신고 API 연동 및 DI 등록
fix(character-comment): 캐릭터 댓글 리스트 무한 스크롤에서 cursor null 시 추가 호출 방지

- CharacterCommentApi/Repository 추가
- AppDI에 API/Repository 등록
- CharacterCommentListFragment: 등록 버튼 클릭 시 API 호출로 전환, 커서 페이징 목록 로드 적용, 신고 API 연동
- 로딩/에러 처리 및 중복 로드 방지 플래그 추가

- 스크롤 리스너에 canLoadMore 조건 추가(초기 또는 cursor 존재 시에만 호출)
- loadMore()에 종료 가드 추가(adapter 비어있지 않고 cursor null이면 반환)
- 댓글 1개인 경우 동일 내용 반복 로딩 문제 해결
2025-08-20 02:37:14 +09:00
klaus 52ff0c82cb feat(character-comment): 신고 BottomSheet 추가 및 삭제 확인 팝업 도입
- 신고 BottomSheet(제목/단일선택 리스트/신고 버튼) 구현 및 더보기→신고 흐름 연동
- 삭제 버튼 클릭 시 확인 다이얼로그 표시 후 확정 시 리스트에서 제거
- 신고/삭제 API 호출부는 스텁으로 남겨둠(후속 연동 예정)
2025-08-20 01:22:56 +09:00
klaus d4ec2fbdef feat(character-comment): 답글 페이지 UI 및 페이징 스텁 구현
- 댓글 리스트 아이템 터치 시 답글 페이지로 전환 연결
- 상단 뒤로 가기/닫기, 입력 폼, divider, 원본 댓글, 들여 쓰기된 답글 목록 구성
- RecyclerView 최하단 도달 시 더미 데이터 추가 로드(무한 스크롤 스텁)
- 답글 등록/수정/삭제 동작 스텁 처리
- 추가 파일
  - layout: fragment_character_comment_reply.xml, item_character_comment_reply.xml
  - 코드: CharacterCommentReplyFragment, CharacterCommentReplyAdapter
- 변경 파일
  - CharacterCommentListBottomSheet: openReply() 추가
  - CharacterCommentListFragment: 아이템 클릭 시 답글 페이지 진입
2025-08-20 00:54:00 +09:00
klaus a9742a07c0 feat(character-comment): 캐릭터 댓글 리스트 BottomSheet UI 및 페이징 스텁 구현
- CharacterDetail 댓글 섹션 터치 시 BottomSheet 표시
- 헤더/입력폼/Divider/리스트/더보기 BottomSheet 구성
- RecyclerView 하단 도달 시 더미 데이터 추가 로드(Stub)
- 상대시간 표기(분/시간/일/년 전)
- API 연동은 이후 작업 예정 (스텁)
2025-08-20 00:42:15 +09:00
klaus df1746976c feat(character-detail): 캐릭터 상세 댓글 섹션 추가 및 데이터 바인딩
- 댓글 입력 필드 stroke(흰색 1dp stroke와 radius 5dp) 추가
- 입력 박스 내부 우측에 전송 아이콘(ic_message_send) 추가
- 배경 드로어블(#263238, radius 10dp) 추가
- CharacterCommentResponse에 comment(nullable) 필드 추가
- CharacterDetailActivity에서 latestComment/totalComments 바인딩 및 UI 분기 처리
2025-08-19 18:37:12 +09:00
klaus 61cfbe249c fix(character-detail): 더보기 버튼 미표시 문제 수정 (줄 수 측정 시점 조정)
세계관/성격 텍스트의 줄 수를 maxLines=3 적용 이전에 측정하도록 순서 변경.
측정 후 더보기 가시성 결정, 그 다음 접힘 레이아웃 적용.
확장 상태 플래그 및 아이콘/문구 초기화 추가.
2025-08-18 19:13:26 +09:00
klaus f9b50089dd fix(chat): 캐릭터 상세
- 세계관 -> [세계관 및 작품 소개]
- 성격 -> [성격 및 특징]
- 전체보기 -> 더보기
2025-08-18 16:37:29 +09:00
klaus 95983dcf5b fix(chat): 최근 대화한 캐릭터
- 캐릭터 이미지 원형으로 변경
2025-08-18 16:31:46 +09:00
klaus 16e8941c15 fix(chat): 캐릭터 상세
- 캐릭터 이미지 딤 제거
- 캐릭터 정보: 이미지 아래로 이동
2025-08-15 01:03:43 +09:00
klaus cd4a098bff fix(chat): 동시간대 메시지 정렬을 messageId 오름차순으로 안정화
createdAt만 사용하던 정렬 로직을 다중 키로 변경하여
동일 시간에 messageId 오름차순이 보장되도록 수정.
- 로컬 초기 로드: createdAt -> messageId -> localId asc
- 서버 초기/증분 로드: createdAt -> messageId asc
2025-08-15 00:45:22 +09:00
klaus 4a0940ad26 fix(chat-room): 프로필 이미지 circle로 변경 2025-08-15 00:37:08 +09:00
klaus dd7251f18b fix(chat-room): 채팅 아이템 UI, 메시지 입력 창 UI
- 채팅 아이템이 화면을 벗어나는 버그 수정
- 메시지 입력창 글자크기 14sp, rounded corner 32dp
2025-08-15 00:29:56 +09:00
klaus 3d727f07fa fix(chat-room): header_container
- 이름과 캐릭터 타입을 세로로 표시
2025-08-14 23:01:31 +09:00
klaus 92883ee577 fix(chat-room): 메시지 전송 API URL 수정
기존
/api/chat/room/{roomId}/messages

변경
/api/chat/room/{roomId}/send
2025-08-14 22:40:27 +09:00
klaus 2790bea1d8 fix(chat-room): stable IDs 설정 시점을 setAdapter 이전으로 이동
- ChatMessageAdapter: onAttachedToRecyclerView에서 setHasStableIds 호출 제거
- ChatRoomActivity: 어댑터 생성 직후 setHasStableIds(true) 설정 후 RecyclerView에 연결

원인: 옵저버 등록 이후 setHasStableIds 변경으로 런타임 예외 발생
검증: 단위 테스트 모두 통과, 빌드 성공
2025-08-14 22:36:50 +09:00
klaus 3f87b35816 refactor(chat-room): 페이징 커서 fallback/저장 로직을 createdAt→messageId로 정합성 수정
- 왜: 서버 계약에 따라 cursor 의미가 단독 messageId로 확정됨. createdAt 기반 커서는 페이징 경계에서 중복/누락을 유발할 수 있음
- 무엇: ChatRoomActivity.loadMoreMessages()/loadInitialMessages()에서 cursor 계산 및 nextCursor 대체 저장을 messageId 기준으로 변경. Repository/API 타입은 그대로 유지
2025-08-14 21:27:17 +09:00
klaus bd86d1610a fix(chat-room): api url 수정
- /api/chat/rooms/... -> /api/chat/room/...
2025-08-14 20:29:36 +09:00
klaus 7f1b1b1ed3 feat(chat-room): 안내 메시지 접힘 상태 저장시 사용하는 key
- string 오류로 인해 제대로 표시 되지 않던 버그 수정
2025-08-14 20:25:35 +09:00
klaus 09b8979ba0 feat(chat-room): sendMessage 응답 다건 변경 반영
- TalkApi.sendMessage: ApiResponse<List<ServerChatMessage>>로 변경
  - ChatRepository.sendMessage: Single<List<ServerChatMessage>>로 변경. 로컬 SENDING→SENT 업데이트 후, 응답 메시지 전체를 DB에 저장
  - ChatRoomActivity: 구독부에서 List를 처리하며 mine == false(AI) 메시지들만 순서대로 append. 타이핑 인디케이터는 성공/실패 시 동일하게 제거
2025-08-14 20:23:21 +09:00
klaus 02747c539b test(chat-room): 타이핑 인디케이터 표시/중복/숨김 테스트 추가
- showTypingIndicator 중복 호출 시 중복 삽입 방지 검증
- hideTypingIndicator 안전성 검증(표시되지 않은 경우도 안전)
- NPE 회귀 방지

fix(adapter): RecyclerView 미부착 상태에서 notify 호출로 NPE 발생 방지
2025-08-14 19:19:38 +09:00
klaus c1012586ce fix(chat-room): 접근성 라벨 및 다국어 문자열 적용
- 레이아웃 contentDescription 하드코딩 제거 및 strings 리소스화
- ChatMessageAdapter 접근성 문구를 리소스 기반으로 변환
- values-en 추가로 안내/버튼/접근성/상태 문구 영문화
- 타이핑 인디케이터 접근성 라벨 추가
2025-08-14 18:50:32 +09:00
klaus c9b6623eac perf(chat): DiffUtil 및 stableIds 적용으로 채팅 리스트 갱신 최적화
- ChatMessageAdapter에 DiffUtil 기반 submitList 도입으로 불필요한 전체 바인딩 제거
- RecyclerView 연결 시점에만 stableIds 활성화하여 테스트 환경 NPE 회피
- AI 프로필 이미지 중복 로딩 방지(tag 비교)로 네트워크/디코딩 비용 절감
- onViewRecycled에서 애니메이션/리스너/이미지 정리로 메모리 안정성 향상
2025-08-14 18:13:40 +09:00
klaus d662bd0b65 feat(chat-ui): 메시지 그룹화, 시간 포맷팅, Repository 테스트 추가 2025-08-14 18:08:01 +09:00
klaus ec60d4f143 fix(settings): 로그아웃 시 로컬 채팅 메시지 전체 삭제 연동
- SettingsViewModel에 ChatRepository 주입 및 삭제 로직 처리
- DI(Koin) 수정으로 SettingsViewModel에 ChatRepository 바인딩
- 삭제 실패 시에도 사용자 로그아웃 흐름 유지
2025-08-14 17:30:44 +09:00
klaus 373752f592 add(gitignore): .idea/deviceManager.xml 추가 2025-08-14 17:15:50 +09:00
klaus 933e650183 feat(chat-room): 채팅 API 연동 및 전송/페이징 플로우 구현 완료
- TalkApi에 입장/전송/점진 로딩 엔드포인트 구현(9.1)
- ChatRepository를 통한 서버 연동 및 로컬 동기화 추가
- ChatRoomActivity에서 입장/전송/페이징 연동, 타이핑 인디케이터/에러 처리 반영(9.2)
2025-08-14 17:14:43 +09:00
klaus 6a6aa271ef feat(chat): 톡 목록 스키마 반영 및 채팅방 진입 연결
- TalkRoom 필드 변경 및 신규 스키마 적용
- 어댑터 바인딩/DiffUtil 수정, 프로필 이미지 28dp 라운드 처리
- 아이템 클릭 시 ChatRoomActivity로 이동(roomId 전달)
- item_talk 배경 제거, 최근 캐릭터 썸네일 모서리 28dp로 통일
2025-08-14 14:46:12 +09:00
klaus 012437e599 feat(character-main): 최근 대화한 캐릭터
- 이미지 표시 및 클릭 이벤트 적용
2025-08-14 01:04:53 +09:00
klaus d3a64d8359 feat(chat-room): Coil 기반 프로필 이미지 로딩 유틸 도입 및 적용
채팅방의 프로필 이미지 로딩을 공용 유틸(loadProfileImage)로 통일하고
플레이스홀더/에러 처리 및 둥근 모서리 변환을 기본 적용했습니다.

- ImageLoader.kt 추가: loadProfileImage(ImageView, url, cornerRadiusDp)
- ChatMessageAdapter: AI 프로필 이미지 로딩에 유틸 적용
- ChatRoomActivity: 헤더 프로필 이미지 로딩에 유틸 적용 (배경 이미지는 기존 유지)
2025-08-14 00:05:18 +09:00
klaus 7451fccff9 feat(chat-room): 시간 포맷팅 유틸 formatMessageTime 도입 및 어댑터 리팩토링
UTC timestamp를 로컬 타임존/로케일 기준 "오전/오후 h:mm" 형식으로 변환하는
공용 유틸(TimeUtils.kt)을 추가하고, ChatMessageAdapter에서 기존 파일 레벨
함수를 제거하여 공용 유틸을 사용하도록 리팩토링했습니다.

- TimeUtils.kt 추가: formatMessageTime(timestamp: Long, locale: Locale)
- ChatMessageAdapter: private 함수 제거 및 import 정리
2025-08-13 23:57:39 +09:00
klaus 1882139fac feat(chat-room): 7.1 로컬 우선 표시 및 오프라인 대체 처리 추가
- 진입 시 로컬 최근 20개 메시지 즉시 표시
- enterChatRoom 응답으로 최신 상태로 전체 갱신
- 네트워크 실패 시 로컬 UI 유지 및 토스트 노출
2025-08-13 23:46:45 +09:00
klaus 7fc72da905 feat(chat-room): 7.3 로컬 DB 동기화 및 메시지 상태/정리 로직 구현
- ChatMessageDao에 상태 업데이트/정리 보조 쿼리 추가
- ChatRepository에 로컬 저장, 상태 업데이트, 오래된 메시지 정리 API 추가
- Activity 전송/상태 변경 시 DB 반영 및 로딩 후 정리 트리거
2025-08-13 23:36:50 +09:00
klaus 9fa270da10 feat(chat-room): 7.2 점진적 메시지 로딩 구현 및 중복 방지 처리
- 상단 스크롤 시 loadMoreMessages로 이전 메시지 로드
- 커서(timestamp) 기반 페이징 및 hasMore/nextCursor 상태 갱신
- messageId 기반 중복 제거, prepend 시 스크롤 위치 보정
2025-08-13 23:30:41 +09:00
klaus 637595e8cd feat(chat-room): 7.1 초기 데이터 로딩 구현 및 ServerChatMessage 매퍼 추가
- enterChatRoom API 연동하여 캐릭터/메시지 초기 로딩
- ServerChatMessage -> ChatMessage 매퍼 추가(toDomain)
- ChatRoomActivity에서 어댑터에 초기 메시지 세팅 및 헤더 갱신
- hasMore/nextCursor 상태 갱신 및 오류 처리
2025-08-13 23:26:01 +09:00
klaus ceae25ea06 feat(chat-room): 메시지 입력/전송/실패 처리(6.1~6.3) 구현
- 왜: 채팅방에서 메시지 입력/전송 및 오류 대응 UX 완성을 위해 6.x 과업을 구현했습니다.
- 무엇:
  - 6.1 입력창 UI
    - EditText placeholder 리소스(@string/chat_input_placeholder) 적용, 최대 200자 제한
    - imeOptions(actionSend|flagNoEnterAction)로 IME 전송 액션 지원
    - 전송 버튼 활성/비활성 상태 관리(TextWatcher), 접근성 라벨(@string/action_send)
    - 입력창 포커스/클릭 시 키보드 표시, 전송 후 키보드 숨김
  - 6.2 전송 플로우
    - onSendClicked()/sendMessage() 도입: 즉시 SENDING 상태로 사용자 메시지 추가
    - 타이핑 인디케이터 표시/숨김 제어(ChatMessageAdapter.show/hideTypingIndicator)
    - 성공 시뮬레이션 후 SENT로 상태 업데이트 및 AI 응답 메시지 추가
    - TODO: 실제 TalkApi POST 연동 지점 주석 추가
  - 6.3 전송 실패 처리
    - FAILED 상태 시 사용자 메시지에 재전송 버튼 노출(item_chat_user_message.xml: iv_retry)
    - 어댑터 콜백을 통한 onRetrySend(localId) 처리 → 재시도 시 SENDING → SENT(성공 시)로 전환
    - strings: action_retry 추가, 접근성 라벨 적용
2025-08-13 23:10:32 +09:00
klaus 0cf0d2e790 feat(chat-room-ui): 5.1~5.5 구현 - Activity 구조/헤더/안내/배경 및 스크롤
왜: 채팅방 UI tasks 5를 완료하여 기본 화면 구성을 완성하고 사용자 경험을 개선하기 위함

무엇: \n- 5.1 기본 Activity 구조 구현 (roomId 처리, setupView 골격)\n- 5.2 RecyclerView 설정 및 무한 스크롤/자동 스크롤/상단 prepend 보정 로직\n- 5.3 헤더 영역: 뒤로가기, 프로필(CoIL), 이름, 타입 배지(기존 배경 리소스)\n- 5.4 안내 메시지: SharedPreferences로 접기 상태 저장, 캐릭터 타입별 안내, strings 리소스 사용\n- 5.5 배경 프로필 이미지 로딩 및 딤 처리 적용(레이아웃 구성 활용)

추가: 관련 문서 docs/ (5.1/5.2/5.3/5.4/5.5, notice strings) 작성 및 정리
2025-08-13 21:37:42 +09:00
klaus 45b76da1e8 feat(chat-room-ui): ChatMessageAdapter 구현 2025-08-13 21:08:01 +09:00
klaus 9bb8dcd881 feat(chat-room-ui): 사용자 메시지, AI 메시지 아이템 레이아웃, 타이핑 인디케이터 아이템 레이아웃 및 애니메이션 추가
item_chat_user_message.xml
- 오른쪽 정렬된 메시지 버블 구현
- 버블 왼쪽에 시간 텍스트(tv_time) 배치
- bg_chat_user_message 배경 및 패딩 적용
- 텍스트 접근성과 가독성 향상을 위한 속성 설정

item_chat_ai_message.xml
- 왼쪽 정렬된 메시지, 프로필 이미지와 이름, 오른쪽 시간 표시 구조 구현
- 그룹화 대응을 위한 조건부 표시(View visibility) 구조 마련
- bg_chat_ai_message 배경과 가독성 개선 속성 적용

item_chat_typing_indicator.xml, typing_dots_animation.xml
- AI 메시지와 동일한 좌측 정렬 구조에 3개 점 애니메이션 영역 구현
- 600ms alpha 애니메이션 반복으로 로딩 상태 시각화
- 추후 ViewHolder에서 점별 startOffset 설정을 통해 순차 반짝임 완성 예정
2025-08-13 20:30:07 +09:00
klaus 760cbb8228 feat(chat-room-ui): implement main chat room layout (task 3.1) 2025-08-13 20:14:51 +09:00
klaus 4a214523c6 feat(chat): 채팅 문자열 리소스 추가 - task 2.3 완료 (chat_notice_clone, chat_notice_character, chat_input_placeholder) - requirements 6.1/6.2, 4.2 충족 - 파일: app/src/main/res/values/strings.xml 2025-08-13 19:58:11 +09:00
klaus 6345b1dbee feat(chat): 타이핑 인디케이터 애니메이션 추가\n\n- task 2.2 완료: typing_dots_animation.xml(alpha, 600ms, reverse, infinite) 생성\n- 사용자 메시지 전송 후 AI 응답 대기 시 점(•••) 순차 반짝임 효과 제공\n- 파일: app/src/main/res/anim/typing_dots_animation.xml\n\n왜: 사용자 메시지 전송 직후 로딩 상태를 시각적으로 표시하기 위함\n무엇: 세 점에 동일 애니메이션을 적용하고 startOffset(0/200/400ms)으로 시퀀싱하여 반짝임 구현\n관련: .kiro/specs/chat-room-ui/tasks.md 2.2, design.md 453~464 2025-08-13 19:56:59 +09:00
klaus 228acadf5a feat(chat-ui): 채팅 메시지 배경 drawable 추가 (Task 2.1)
- 사용자/AI/입력/안내 배경 리소스 생성
- 기존 라운드 리소스 재활용 및 불투명도 적용
- 요구사항 2,6 및 디자인 가이드 반영
- docs: Task 2.1 수행 내역 문서 추가 (docs/chat-room-ui-2.1-drawables.md)
2025-08-13 19:41:33 +09:00
klaus 6388895e6e feat(chat-room): ChatRepository 도입 및 TalkApi에 입장/메시지 조회 API 추가
- Repository 패턴 구현: 로컬 DB(Room) + 네트워크(TalkApi) 통합
- enterChatRoom, loadMoreMessages, clearAllMessagesOnLogout 제공
- TalkApi에 /enter, /messages 엔드포인트 추가
- Entity↔도메인 매퍼 추가
- Koin 모듈에 ChatRepository 바인딩
2025-08-13 17:30:04 +09:00
klaus 725c4335e1 feat(chat-talk-room): Room Database 설정 및 Entity 생성
refactor(chat-talk-room): 패키지 chat.room → chat.talk.room 마이그레이션 및 DI 모듈 분리

왜: 기능 영역 명확화(talk) 및 DI 책임 분리로 유지보수성과 확장성을 높이기 위함
무엇:
- 모델/응답/enum 파일들을 chat.room → chat.talk.room 으로 이동
- Room DB 패키지를 chat.room.db → chat.talk.room.db 로 이동
- AppDatabase 클래스명을 역할에 맞게 ChatMessageDatabase로 변경

문서:
- docs/chat-talk-room-package-migration-and-di-module.md 추가
- docs/chat-room-room-database.md 내용 클래스명/경로 갱신
2025-08-13 17:10:06 +09:00
klaus 64deadda0b feat(chat-room): 1.1 데이터 모델 생성 및 채팅 메시지 모델 서버-로컬 분리
왜: 서버 스키마와 클라이언트 전용 필드가 혼재되어 혼란을 야기하던 문제를 해결하고, 유지보수성과 확장성을 높이기 위함.

무엇:
- tasks 1.1 수행 (데이터 모델 클래스 생성)
  - ChatMessage 데이터 클래스 생성 (로컬/UI/도메인용)
  - MessageStatus enum 생성 (SENDING, SENT, FAILED)
  - MessageType enum 생성 (USER_MESSAGE, AI_MESSAGE, NOTICE, TYPING_INDICATOR)
  - CharacterType 기존 enum 재사용 (chat/character/detail/CharacterDetailResponse.kt)
  - ChatRoomEnterResponse, ChatMessagesResponse 데이터 클래스 생성
- 채팅 메시지 모델 서버-로컬 분리 및 응답 모델 정리
  - ServerChatMessage DTO 추가 (서버 응답 전용: messageId, message, profileImageUrl, mine, createdAt)
  - ChatMessageMappers 추가: ServerChatMessage.toLocal(isGrouped: Boolean = false)
  - ChatRoomEnterResponse, ChatMessagesResponse에서 messages 타입을 List<ServerChatMessage>로 정리
- 문서
  - docs/chat-room-data-models.md 갱신 (서버/로컬 분리 사항 반영)
  - docs/chat-room-message-model-separation.md 신설 (분리 배경/가이드)

추가 참고:
- 시간 포맷 유틸은 후속 태스크(8.1)에서 테스트와 함께 구현 예정
2025-08-13 05:23:12 +09:00
klaus 558f74d861 feat(chat): 캐릭터 상세에서 채팅방 생성 후 ChatRoomActivity로 네비게이션 추가
- ChatRoomActivity에 EXTRA_ROOM_ID 및 newIntent 추가
- CharacterDetailActivity에서 chatRoomId 수신 시 화면 이동 처리
- 이벤트 소비 유지로 중복 네비게이션 방지
2025-08-13 02:21:43 +09:00
klaus 4eedecd1ce feat(chat-character): 채팅 톡 탭
- 데이터가 없으면 "대화 중인 톡이 없습니다" 메시지 표시
2025-08-13 01:23:56 +09:00
klaus 08f9d398c4 feat(chat-character): 캐릭터 상세
- 원작의 UI 레벨을 세계관 하위로 이동
2025-08-13 01:17:27 +09:00
klaus f102c84ea6 feat(chat-character): 캐릭터 탭 모든 액션
- 로그인과 본인인증이 되어 있어야 가능하도록 수정
2025-08-13 01:09:34 +09:00
klaus 0c3bca0f9e feat(chat-character): 캐릭터 상세 페이지 API 연동 및 UI 상태 처리
- CharacterApi에 캐릭터 상세 조회 엔드포인트 추가
- CharacterDetailRepository 생성 및 Koin DI 등록
- CharacterDetailViewModel에서 실제 API 호출/로딩/에러 상태 관리
- CharacterDetailActivity에서 loadMock 제거 후 load 호출, Koin 주입으로 전환
- 로딩 다이얼로그 및 에러 토스트 처리 로직 추가
2025-08-13 00:52:24 +09:00
klaus ff1e134fe4 feat(character list): 캐릭터 탭
- 배너 리스트 추가
- 배너, 캐릭터 클릭시 캐릭터 상세 페이지로 이동
2025-08-13 00:05:39 +09:00
klaus d8b48fe362 feat(character list): 캐릭터 이미지 배경색 제거 2025-08-12 23:39:22 +09:00
klaus ac2482a645 feat(character detail): 캐릭터 상세 페이지 UI 추가 2025-08-12 22:15:52 +09:00
klaus 5090809be8 gitignore 규칙 추가
- .kiro/
2025-08-11 15:44:05 +09:00
klaus 80c593bc11 fix: 채팅방 리스트 API URL 수정
- /api/chat/talk/rooms -> /api/chat/room/list
2025-08-11 14:55:31 +09:00
klaus 18b61ab74f fix: 채팅 탭 data class
- SerializedName 추가
2025-08-11 11:24:10 +09:00
klaus ea22c7244c feat(ui): 캐릭터 탭
- loadingDialog, Toast 라이브 데이터 옵저버 연결
2025-08-05 02:07:46 +09:00
klaus b1c9c3e124 feat(ui): 톡 탭
- api, viewmodel, repository 연결
- 채팅방 리스트 UI 추가
2025-08-05 02:01:19 +09:00
klaus 93fc837b7a feat(ui): 캐릭터 탭
- 섹션별로 데이터가 있으면 보여주고 없으면 UI를 제거하도록 로직 추가
2025-08-04 23:38:51 +09:00
klaus f0eda41c7c feat(ui): viewmodel, repository, api 추가 2025-08-04 22:24:13 +09:00
klaus 47717002e8 feat(ui): banner 추가 2025-08-04 22:10:27 +09:00
klaus 7b7513561d refactor: item decoration 추가 2025-08-04 22:04:19 +09:00
klaus 33bdaa7dbd refactor: 캐릭터 탭 내부에서 사용하는 Adapter 코드를 ViewBinding 코드로 리팩토링 2025-08-04 21:02:05 +09:00
klaus b919691689 feat(character): 캐릭터 탭 UI 및 기본 기능 구현 2025-08-04 20:27:33 +09:00
klaus e90222e8db feat(ui): 채팅 탭 내 TabLayout 캐릭터, 톡 탭 추가 2025-08-01 19:25:14 +09:00
klaus 3cf57c1f91 feat(ui): 채팅 탭 추가 2025-08-01 14:47:51 +09:00
klaus f6e7229246 chore: .gitignore 파일에 .idea 관련 파일 추가 2025-08-01 14:34:58 +09:00
klaus f55e74c8dc feat: git 제외 파일 및 폴더
- docs
- junie guidelines
2025-07-31 20:16:25 +09:00
klaus e25276658d feat: 마이페이지
- 내 채널 보기 추가
2025-07-30 14:52:59 +09:00
klaus d088c6f6b3 # 고객센터 UI 개선 및 버전 업데이트 (v1.41.0)
## 변경사항
- 앱 버전을 1.40.0(179)에서 1.41.0(181)으로 업데이트
- 고객센터 화면에 전용 로고 이미지 추가 및 UI 개선
  - 플레이스홀더 이미지를 고객센터 전용 로고로 교체
  - 텍스트 마진 조정 (13.3dp → 16dp)
- 마이페이지 화면 UI 개선
  - 본인인증 버튼 텍스트 간소화 ("본인인증 완료" → "인증완료")
  - 레이아웃 구조 개선 (패딩/마진 조정)
  - RecyclerView 스크롤 경험 개선 (clipToPadding 속성 추가)
2025-07-28 17:33:03 +09:00
klaus 9361610647 feat: 마이페이지
- 상단에 최신 공지사항 추가
2025-07-25 22:18:22 +09:00
klaus 7ed5e921bd feat: 마이페이지
- 최근 들은 콘텐츠 추가
2025-07-25 21:36:34 +09:00
klaus 39be49b481 feat: 마이페이지
- 신규 UI 적용
2025-07-25 16:52:34 +09:00
klaus 3b7b5f98bd fix: 메인 라이브 - 최근 종료한 라이브
- 이미지 사이즈 72 -> 84
2025-07-21 20:07:46 +09:00
klaus 9be1b86c5d fix: 메인 홈 - 인기 크리에이터
- 팔로우/팔로잉 배경색 변경
2025-07-21 19:52:23 +09:00
klaus cfe9d3ab11 fix: 메인 라이브 - 최근 종료한 라이브
- 비로그인 상태에서 터치시 로그인 페이지로 이동
2025-07-21 18:56:44 +09:00
klaus accb413636 feat: 메인 홈 - 오디션
- 비로그인 상태에서 터치시 로그인 페이지로 이동
2025-07-21 18:51:01 +09:00
klaus bdac7b7899 feat: 메인 홈 - 인기 크리에이터
- 팔로우/언팔로우 기능 추가
2025-07-21 18:48:09 +09:00
klaus 58bc42cc0f feat: 메인 라이브 - 최근 종료한 라이브
- 사용 하지 않는 데이터 제거
2025-07-21 18:24:53 +09:00
klaus 44d7ce65ae feat: 메인 라이브
- 신규 UI 적용
2025-07-21 18:00:31 +09:00
klaus c55cc68f5c feat: 메인 라이브, 메인 홈
- 섹션 제목 크기 26 -> 24
- 오디션 배너 변경
- 추천 채널 아이템 bg 톤다운
2025-07-19 04:02:29 +09:00
klaus d7cc874684 feat: 메인 라이브
- 최근 종료한 라이브, 라이브 다시 듣기, 라이브 예약 아이템 사이즈 조절
2025-07-19 02:17:47 +09:00
klaus f1164bbd30 feat: 메인 라이브 - 지금 라이브 중
- bg => #263238로 변경
- 가로 => 168 -> 144
- 세로 => 238 -> 204
2025-07-19 01:26:21 +09:00
klaus 5f6d26c83e feat: 메인 라이브
- 최근 종료한 라이브 - 라이브 아이콘 제거
- 커뮤니티 - 이미지 사이즈 수정 (53.3 -> 64)
2025-07-18 23:07:19 +09:00
klaus fcd341a1f4 feat: 메인 라이브
- 예약 라이브 - 유료 라이브 금액 나오지 않던 버그 수정
- 팔로잉 채널 - 위치 커뮤니티와 지금 라이브 중 사이로 이동
2025-07-18 22:51:21 +09:00
klaus 6e5a4cff45 feat: 메인 라이브
- 변경된 커뮤니티 게시글 아이템 UI 적용
2025-07-18 21:37:16 +09:00
klaus 45fd75ab36 feat: 메인 홈
- 오디션 리스트를 보여주지 않고 터치시 오디션 페이지로 이동하도록 수정
2025-07-18 21:06:13 +09:00
klaus 2f9bace3de feat: 메인 라이브
- 라이브 다시 듣기 UI 추가
2025-07-18 20:43:30 +09:00
klaus 964f697466 feat: 메인 라이브
- 개편된 지금 라이브 중 UI 적용
2025-07-18 19:21:20 +09:00
klaus bb23f9cf93 feat: 메인 라이브
- 최근 종료한 라이브 UI 추가
2025-07-18 18:57:11 +09:00
klaus 440104a7d1 feat: 메인 라이브
- 라이브 예약 중 UI 변경
2025-07-17 20:49:44 +09:00
klaus 0c7c7946c6 feat: 메인 라이브
- 새로운 UI의 기본 골격 적용
2025-07-16 22:07:07 +09:00
klaus 386f9aae32 feat: 메인 홈
- 섹션 간의 간격 수정
- 기존: 밑에 있는 섹션에서 marginTop="48dp"
- 변경: 위에 있는 섹션에서 marginBottom=48dp"
2025-07-16 16:24:07 +09:00
klaus b5d0309f2b feat: 메인 홈
- 돋보기 터치시 검색 페이지 연결
2025-07-16 14:13:18 +09:00
klaus 3e525b05a5 feat: 메인 홈
- UI 수정
2025-07-15 21:42:56 +09:00
klaus 141e7fe416 feat: 메인 홈
- 다른 페이지로 이동시 로그인 안되어 있으면 로그인 페이지로 이동
2025-07-15 20:41:35 +09:00
klaus db2e3bc8f2 feat: 메인 홈
- 추천 채널 UI 추가
2025-07-15 20:20:54 +09:00
klaus 66a6f4bbab feat: 메인 홈
- 큐레이션 UI 추가
2025-07-15 19:01:29 +09:00
klaus a328ea9c3c feat: 메인 홈
- 무료 콘텐츠 UI 추가
2025-07-15 18:44:14 +09:00
klaus 76b8b74d41 feat: 메인 홈
- 보온 주간 차트 UI 추가
2025-07-15 18:34:46 +09:00
klaus 5c4141dad9 feat: 메인 홈
- 요일별 시리즈 UI 추가
2025-07-15 17:54:53 +09:00
klaus e787872cc5 feat: 메인 홈
- 오디션 배너 UI 추가
2025-07-15 16:27:14 +09:00
klaus af818bda93 feat: 메인 홈
- 오직 보이스온에서만 UI 추가
2025-07-15 16:08:25 +09:00
klaus ccc774da0d feat: 메인 홈 - 최신 콘텐츠
- 데이터가 1개만 있을 때도 2줄 영역을 차지하던 버그 수정
2025-07-15 15:28:50 +09:00
klaus 32d61d9808 feat: 메인 홈
- 이벤트 배너 UI 추가
2025-07-15 06:34:41 +09:00
klaus 83a30fa088 feat: 메인 홈
- 최신 콘텐츠 UI 추가
2025-07-15 06:27:33 +09:00
klaus f24cd97afa feat: 메인 홈
- 인기 크리에이터 UI 추가
2025-07-15 05:39:04 +09:00
klaus 388770889f feat: 메인 홈
- 라이브 UI 추가
2025-07-15 05:04:21 +09:00
klaus e3121fc49b feat: 스플래시 변경 2025-07-14 21:47:59 +09:00
klaus f1958995f6 feat: 하단 탭 아이콘 변경 2025-07-07 20:23:23 +09:00
klaus ba7b681e48 feat: 커뮤니티 전체보기
- gif 재생 되도록 추가
2025-07-03 14:33:45 +09:00
klaus e4012a1301 feat: 커뮤니티 글쓰기/수정
- 이미지 gif 등록 기능 추가
2025-07-03 13:15:01 +09:00
klaus 6ff0d8bd61 fix: 사용하지 않는 퍼미션 제거
- GET_ACCOUNTS
2025-06-16 16:09:37 +09:00
klaus 898afc78ef fix: 커뮤니티 댓글
- 무료 커뮤니티 글, 내 커뮤니티 글 에서 비밀댓글 체크박스가 보이지 않도록 수정
2025-06-13 21:06:03 +09:00
klaus c527f55721 feat: 팔로워 리스트
- 프로필 이미지를 터치하면 프로필 다이얼로그 표시
2025-06-13 19:36:38 +09:00
klaus 89277c5668 feat: 커뮤니티 댓글 리스트
- 비밀댓글 태그 추가
2025-06-13 17:07:16 +09:00
klaus 28388497b8 feat: 커뮤니티 댓글
- 유료 커뮤니티 구매시 비밀 댓글 쓰기 기능 추가
2025-06-13 16:52:40 +09:00
klaus 09a2a96596 refactor: 콘텐츠 상세
- cleanup code를 실행하여 불필요한 코드 제거
2025-06-12 16:16:52 +09:00
klaus d3f6a02be2 feat: 쿠폰 등록, 인기 단편 전체보기
- 쿠폰 등록 후 캔 내역 페이지가 아닌 바로 이전 페이지로 이동하도록 수정
- 인기 단편 전체보기에 포인트 사용 여부 표시
2025-06-10 20:49:06 +09:00
klaus c8cc0457e4 feat: 쿠폰 등록 안내 문구 수정 2025-06-10 19:57:56 +09:00
klaus 4d9e68d60b feat: 시리즈 상세 - 콘텐츠 리스트
- 포인트 사용 가능 여부 표시
2025-06-10 18:24:46 +09:00
klaus 74585bfb7f feat: 크리에이터 채널 - 콘텐츠 리스트
- 포인트 사용 가능 여부 표시
2025-06-10 14:54:20 +09:00
klaus ea766afba9 feat: 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 포인트 사용이 가능한 콘텐츠의 썸네일 우측 상단에 포인트 사용 가능 표시
2025-06-10 12:26:04 +09:00
klaus f10d848797 feat: 콘텐츠 메인 - 채널별 인기 콘텐츠
- 포인트 사용이 가능한 콘텐츠의 썸네일 우측 상단에 포인트 사용 가능 표시
2025-06-10 12:03:35 +09:00
klaus 3bda97b0a7 feat: 콘텐츠 수정
- 태그 수정 기능 추가
- 포인트 사용여부 수정 기능 추가
2025-06-04 20:03:42 +09:00
klaus 19c39f636d feat: 콘텐츠 업로드
- 포인트 사용 가능 여부 추가
2025-06-02 15:22:29 +09:00
klaus 8b7894a370 feat: 라이브 후원 메시지 글자 수 조정
- 200자 -> 1000자
2025-05-23 19:26:55 +09:00
klaus d1056bda99 feat: 구매 확인 Dialog
- 포인트 사용이 가능한 경우 포인트를 같이 표시하도록 수정
2025-05-20 18:40:41 +09:00
klaus 5dbf9bd987 fix: 앱 실행시 처음 실행하는 유저 정보 조회
- point를 가져와서 SharedPreferences에 저장
2025-05-20 18:03:28 +09:00
klaus 23494d0936 feat: 포인트 소멸 안내 메시지 추가 2025-05-20 17:43:15 +09:00
klaus 116d4b3ecf feat: 포인트 내역 UI 추가 2025-05-20 00:29:00 +09:00
klaus 8b8f5b80b8 fix: 로그아웃시 UserDefaults에서 푸시토큰을 삭제하지 않도록 수정 2025-05-17 21:59:28 +09:00
klaus 0b9abf39f1 refactor: 라이브 연속 참여 시간 계산시 initialDelay와 period에 있는 같은 값을 period 변수로 선언 2025-05-17 21:43:45 +09:00
klaus 9260d271a7 feat: 라이브 30분 연속 청취시 트래킹 API 호출 기능 추가 2025-05-17 16:57:12 +09:00
klaus 1720173a16 fix: fcm 데이터 수신 수정
- data-only, notification+data 방식 모두 동일하게 딥링크가 적용되도록 수정
2025-05-09 11:56:17 +09:00
klaus 60190e099a feat: fcm 데이터 수신 방식 수정
- data-only 메시지만 수신 방식에서 notification + data로 수신 방식 변경
2025-05-08 19:44:32 +09:00
klaus db4bd56df2 versionCode 165, versionName "1.36.0" 2025-04-29 16:51:55 +09:00
klaus affb6865a8 fix: 라이브 - only 메뉴판 on/off 시 변경 되지 않던 버그 수정 2025-04-29 14:18:24 +09:00
klaus cab10717e9 fix: 콘텐츠 PlayCount 기록 기준을 12초에서 4초로 수정 2025-04-29 13:56:59 +09:00
klaus ee870c4366 versionCode 164, versionName "1.35.0" 2025-04-25 15:56:34 +09:00
klaus d61854f972 fix: 콘텐츠 상세 - 우측 상단에 포인트 아이콘 크기 수정 2025-04-24 20:32:49 +09:00
klaus e45df2bac5 versionCode 163, versionName 1.35.0 2025-04-24 13:36:45 +09:00
klaus 009e2080fc feat: 콘텐츠 상세 - 포인트가 사용 가능한 경우 커버이미지 우측 상단에 포인트 아이콘으로 표시 2025-04-24 11:18:39 +09:00
klaus f265732741 feat: 마이페이지 - 포인트 내역 추가 2025-04-23 21:57:28 +09:00
klaus d11326233f feat: 포인트 지급 팝업 - 확인 버튼 터치시 팝업만 닫히도록 수정 2025-04-22 22:55:49 +09:00
klaus 5536236100 feat: 회원가입 or 소셜 로그인 시 pushToken 추가 2025-04-22 18:11:44 +09:00
klaus b077a361b9 feat: 포그라운드 상태에서 FCM data-only 수신 시 인앱 팝업 표시 2025-04-22 18:06:47 +09:00
klaus c0ad98f285 콘텐츠 상세
- 새로고침시 구매자의 데이터가 계속 추가되는 버그 수정
2025-04-16 18:53:16 +09:00
klaus 11307eae3b versionCode 162, versionName "1.34.2" 2025-04-16 18:41:35 +09:00
klaus 14da5f6a19 라이브 정보 수정
- 배경만 단독으로 변경되지 않는 버그 수정
2025-04-16 18:35:50 +09:00
klaus 065f7ee038 회원탈퇴
- 소셜로그인 탈퇴 안내 문구 추가
2025-04-15 19:42:44 +09:00
klaus 710015d89e 라이브 입장 메시지 on/off 스위치 추가
- 라이브 정보 수정 가장 아래에 입장 메시지 on/off 스위치 추가
2025-04-15 17:15:23 +09:00
klaus c5a173138c 한정판 콘텐츠 상세
- 해당 콘텐츠를 업로드 한 크리에이터가 콘텐츠 구매자를 볼 수 있는 UI 추가
2025-04-14 15:03:58 +09:00
klaus dfb2c903a4 콘텐츠 상세
- 10초 전/후로 이동 기능 추가
2025-04-08 13:19:52 +09:00
klaus 01dc0cabbe 회원가입
- 소셜 로그인과 같은 크기의 버튼 추가
2025-04-07 20:46:02 +09:00
klaus d4796257b3 카카오 로그인 추가 2025-04-07 19:20:57 +09:00
klaus a9885874ee 구글 로그인 추가 2025-04-04 18:40:22 +09:00
klaus 44e3f0c171 versionCode 157, versionName 1.33.0 2025-04-02 11:04:41 +09:00
klaus 62b15609ff 콘텐츠 상세
- 이전화/다음화 버튼 추가
2025-04-01 20:58:21 +09:00
klaus bddf7b750b 구간반복 기능 추가 2025-04-01 14:25:45 +09:00
klaus c7af522cfb 검색 UI 추가 2025-03-31 12:47:52 +09:00
klaus e4b0dbae82 콘텐츠 메인 탭 홈
- 레거시 검색 UI 제거
2025-03-27 01:26:57 +09:00
klaus c0c5d1afec 콘텐츠 메인 탭 홈
- 왼쪽 상단 로고 제거
- 오른쪽 상단 충전 페이지 이동 아이콘 추가
- 오른쪽 상단 모닝콜, 보관함 아이콘 제거
2025-03-27 01:12:51 +09:00
klaus 4d87544b7b pid를 심어놓은 광고를 타고 들어온 경우 항상 AppLaunch 이벤트를 실행하는 코드 추가 2025-03-26 17:19:35 +09:00
klaus b50df2cdf3 라이브
- 크리에이터의 경우 라이브 만들기 버튼이 바로 보이도록 수정
2025-03-24 21:25:31 +09:00
klaus c3d5c12e6b 라이브
- 라이브 중인 아이템 터치시 비로그인 상태에서는 로그인 페이지로 이동하는 기능 추가
2025-03-24 21:17:46 +09:00
klaus 0077f172b6 콘텐츠 메인 홈
- 아이콘과 글자크기 수정
2025-03-24 21:12:35 +09:00
klaus 089534fb47 - 라이브 우측 상단의 메시지 아이콘 색상 변경 2025-03-24 21:07:04 +09:00
klaus bd851f6afd 콘텐츠 메인 홈
- 비 로그인 상태에서 인기순위 날짜 보이지 않도록 수정
2025-03-24 20:53:10 +09:00
klaus 2d69f27a25 로그인
- 로그인 액션시 키보드가 바로 숨겨지도록 수정
2025-03-22 06:53:10 +09:00
klaus 489b968ea3 라이브 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 06:47:56 +09:00
klaus 7ab2779805 비밀번호 재설정
- 입력창 UI 수정
2025-03-22 05:26:41 +09:00
klaus 964b92f83a 오디션 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 05:21:36 +09:00
klaus 4445a745bf 고객센터
- 카테고리 배경색, dropdown 아이콘 변경
2025-03-22 05:03:12 +09:00
klaus 3b3327be7b 마이 페이지
- 로그인 했을 때만 데이터를 조회하도록 수정
2025-03-22 04:55:53 +09:00
klaus 70fe5a4441 콘텐츠 메인
- 로그인 하지 않고도 페이지를 조회할 수 있도록 수정
2025-03-22 04:11:01 +09:00
klaus 22f90b2e40 회원가입, 로그인 입력창 속성 수정 2025-03-22 00:29:23 +09:00
klaus dfe3b291a1 회원가입 단계 간소화 2025-03-21 04:42:08 +09:00
klaus 9331ba1276 로그인 UI
- 입력창 크기 및 UI 수정
2025-03-21 02:11:45 +09:00
klaus 7c39d6c53a 온보딩 페이지 제거 2025-03-20 18:38:53 +09:00
klaus e941f7c940 라이브 탭
- 상단에 메시지 버튼 추가
2025-03-20 16:26:15 +09:00
klaus e0e935cf29 메인
- 메시지 탭 제거
2025-03-20 15:18:12 +09:00
klaus 74c2db6ceb 콘텐츠 메인 홈
- 콘텐츠 마켓 -> 보이스온 으로 변경
2025-03-20 13:36:41 +09:00
klaus 506f446b60 채널 상세
- 19금 콘텐츠 보기 여부 적용
2025-03-19 23:23:03 +09:00
klaus 68a777c8df 시리즈 상세, 시리즈 리스트, 시리즈 전체회차 듣기
- 19금 콘텐츠 보기 여부 적용
2025-03-19 22:35:45 +09:00
klaus 10c215d9bd 콘텐츠 전체보기
- 19금 콘텐츠 보기 여부 적용
2025-03-19 22:28:10 +09:00
klaus 28b7aaae9f 콘텐츠/팬 Talk 댓글 수정 오류
- 댓글 수정 모드 상태에서 다른 댓글 쓰기 혹은 삭제 등의 액션을 해서 댓글 수의 변화가 있을 때 내 글이 아닌 다른 사람의 글이 수정 모드로 보이는 버그 수정
2025-03-18 23:10:20 +09:00
klaus e1950eba2b 콘텐츠
- 남성향, 여성향 콘텐츠 설정 적용
- 19금 콘텐츠 보기 여부 설정 적용
2025-03-18 16:43:04 +09:00
klaus b08eb896a7 노티플라이
- 앱푸시를 보내면 빈 알림이 표시되는 버그 수정
2025-03-12 18:09:54 +09:00
klaus 8c013f7126 파이어베이스 트래킹
- login 트래킹 위치 수정
- marketing_pid 값 추가
2025-03-12 16:26:22 +09:00
klaus 01d96a19b9 Notifly 설정 추가
- logout
- 메인 화면에 진입할 때마다 데이터 업데이트
2025-03-12 16:05:41 +09:00
klaus 10208fada8 Notifly 기본 설정 추가 2025-03-12 03:08:12 +09:00
klaus d430f5d543 UTM 기록 이벤트 변경
- Firebase 이벤트 상수인 CAMPAIGN_DETAILS를 사용했을 때 데이터가 기록되지 않아 커스텀 이벤트인 ad_partner_campaign_detail로 UTM 파라미터 기록
2025-03-12 00:58:29 +09:00
klaus 536c76b9bf UTM Campaign 파라미터를 Analytics에 설정된 파라미터 상수값 사용 2025-03-11 23:44:07 +09:00
klaus c0c31a23cc Firebase 트래킹 추가
- 회원가입, 로그인
2025-03-10 16:09:55 +09:00
klaus 6065b353fd 라이브, 콘텐츠, 채널 공유하기
- sns 공유시 보여줄 og data 제거
- shorturl이 적용되지 않은 상태에서 url이 너무 길기 떄문데 임시 적용
2025-03-07 14:42:36 +09:00
klaus 7885200af4 라이브, 콘텐츠, 채널 공유하기
- 파라미터 키, 값 각각 인코딩 적용
2025-03-07 02:50:36 +09:00
klaus 415383393a 앱스플라이어 딥링크
- 가장 먼저 실행되는 Application 영역으로 이동
2025-03-07 02:24:33 +09:00
klaus f790264e44 versionCode 148
versionName "1.30.0"
2025-03-07 00:45:02 +09:00
klaus d8afdecc89 라이브, 콘텐츠, 채널 공유 재추가
- AppsFlyer OneLink로 공유링크 생성
2025-03-07 00:41:29 +09:00
klaus 46e1efff2a Firebase Analytics
- UTM 기록
2025-03-06 00:55:03 +09:00
klaus b39857cf24 딥링크만 처리하는 액티비티 추가 2025-03-05 04:13:01 +09:00
klaus c5e60785da 앱스플라이어 딥링크
- series, live, content, channel 딥링크의 경우 해당 페이지로 이동하는 기능 추가
2025-03-05 01:14:24 +09:00
klaus e7cc1df201 앱스플라이어 딥링크
- marketingPid는 값이 있을 때만 SharedPreference에 저장하도록 수정
2025-03-05 00:52:54 +09:00
klaus d9fad70201 앱 메인 - pid 업데이트 API
- 메인 페이지 접속 후 3초 이후에 업데이트 되도록 수정
2025-03-04 14:27:51 +09:00
klaus 95c77d531d 앱 메인
- pid 업데이트 API 적용
2025-03-04 13:29:03 +09:00
klaus 658f304ce5 회원가입
- marketing pid 추가
2025-03-04 12:16:49 +09:00
klaus 82f71f9a07 앱스플라이어
- applink, urischeme 설정
2025-03-02 18:01:22 +09:00
klaus 1de374de3e 앱스플라이어
- sdk 추가
- 딥링크 & 디퍼드 딥링크에서 값 받아와서 SharedPreference에 저장
- test applink, test urischeme 설정
2025-03-02 17:45:55 +09:00
klaus 9cef92199d 파이어베이스 다이나믹 링크 제거 2025-03-02 15:52:57 +09:00
klaus fb60574f3d 콘텐츠 플레이어 - 재생목록
- 아이템을 터치했을 때 현재 index도 같이 변경되도록 수정해서 이전/이후 재생시 현재 아이템 기준으로 이전/이후 콘텐츠가 재생되도록 수정
2025-02-26 17:19:47 +09:00
klaus 2d92c6a849 콘텐츠 플레이어
- 재생목록 보일 때 제목과 크리에이터 영역까지 재생목록이 차지하도록 수정
2025-02-26 16:57:54 +09:00
klaus cbda2b196a 콘텐츠 플레이어
- 10초 이전/이후로 이동하는 기능 추가
2025-02-25 21:50:41 +09:00
klaus 9d042ff75f 콘텐츠 플레이어 - 재생 목록
- 현재 재생 중이지 않은 콘텐츠 터치시 터치한 콘텐츠를 재생하는 기능 추가
2025-02-25 21:25:06 +09:00
klaus 5d7db2d7e9 콘텐츠 플레이어
- 현재 재생 중인 콘텐츠 배경 변경
2025-02-25 18:30:20 +09:00
klaus 7d15179be0 콘텐츠 메인 홈
- 테마 탭 순서 변경
2025-02-25 13:40:47 +09:00
klaus 490bcd87af 메타(페이스북) SDK 추가 2025-02-25 13:34:08 +09:00
klaus 4623d0abd2 콘텐츠 메인 홈
- 크리에이터가 아닌데 콘텐츠 업로드 버튼이 보이는 버그 수정
2025-02-24 16:22:08 +09:00
klaus 1b47e38f79 콘텐츠 메인 새로운 단편 전체보기 제목 변경
- 무료 콘텐츠 조회시 -> 새로운 무료 콘텐츠
- 기본 -> 새로운 단편
2025-02-22 12:34:31 +09:00
klaus 09ca0487f8 콘텐츠 메인
- 콘텐츠 미니 플레이어 추가
2025-02-22 12:12:01 +09:00
klaus 3a7df3f16e 콘텐츠 메인 새로운 단편 전체보기 제목 변경
- 무료 콘텐츠 조회시 -> 새로운 무료 콘텐츠
- 기본 -> 새로운 단편
2025-02-22 11:44:32 +09:00
klaus 63646b0d19 콘텐츠 메인 라이브 다시듣기 - 새로운 라이브 다시듣기 전체보기
- 다시보기 -> 다시듣기
2025-02-22 11:41:27 +09:00
klaus 9b511b9d18 스플래시 문구 변경 2025-02-22 11:40:23 +09:00
klaus 0180a384e1 콘텐츠 대여 기간 안내 변경
- 15일 -> 5일
2025-02-22 11:09:37 +09:00
klaus c79eb90500 콘텐츠 메인 시리즈 탭 - 완결 시리즈
- URL 변경
2025-02-22 04:28:35 +09:00
klaus 6129982df4 콘텐츠 메인 단편 탭 - 새로운 콘텐츠 전체보기
- 새로운 콘텐츠가 없으면 '새로운 콘텐츠가 없습니다.' 문구 출력
2025-02-19 18:29:51 +09:00
klaus 8ed9e08a60 콘텐츠 메인 시리즈 탭 - 일간 랭킹
- 아이템 사이즈, 아이템 사이 간격 수정
2025-02-19 03:53:09 +09:00
klaus c14150191f 콘텐츠 메인 시리즈 탭 - 완결 시리즈 전체보기
- 한줄에 아이템 3개씩 보이도록 수정
- 크리에이터 표시
2025-02-19 02:34:43 +09:00
klaus b7598627b1 콘텐츠 메인 시리즈 탭
- 일간 랭킹 아이템 이미지 사이즈 고정
2025-02-19 02:00:07 +09:00
klaus 475ddc21b3 콘텐츠 메인 홈 탭
- 카테고리 아이콘, 글씨 사이즈 업
2025-02-19 01:55:59 +09:00
klaus 61cb208f2f 콘텐츠 메인 다시듣기 탭
- 순위 제거
2025-02-19 01:55:35 +09:00
klaus d0e01a83e3 콘텐츠 메인 ASMR 탭
- 순위 제거
2025-02-19 01:40:53 +09:00
klaus 7bb8f9c5af 콘텐츠 메인 단편 탭
- 태그별 추천 콘텐츠 영역 추가
2025-02-18 23:44:39 +09:00
klaus 304e6e166a 콘텐츠 메인 무료 탭
- 채널별 추천 무료 콘텐츠 UI 추가
2025-02-18 02:08:06 +09:00
klaus e75602fc8d 콘텐츠 메인 단편 탭
- 큐레이션 추가
2025-02-17 23:40:37 +09:00
klaus e46c34558e 콘텐츠 메인 탭 - 새로운 콘텐츠 전체보기
- 전체 개수 추가
2025-02-17 21:40:16 +09:00
klaus 0e70ed2661 콘텐츠 메인 탭 - 채널별 **
- 콘텐츠 가격과 러닝 타임 표시
2025-02-17 18:42:17 +09:00
klaus 04569f1e7e 콘텐츠 메인 탭 - 채널별 **
- RecyclerView와 GridLayout으로 변경
2025-02-17 18:05:59 +09:00
klaus 189e757100 콘텐츠 메인 시리즈 탭
- 완결 시리즈가 일간 순위에 잘못 표시되던 버그 수정
2025-02-17 09:14:25 +09:00
klaus 623ff545b5 콘텐츠 메인 탭 - 채널별 ***
- 채널 표시 간격 22dp로 수정
2025-02-17 09:04:56 +09:00
klaus bbd972d860 콘텐츠 메인
- 콘텐츠 배너 상단 마진 제거
2025-02-17 08:40:04 +09:00
klaus bbf0b26025 사용하지 않는 로그 제거 2025-02-15 03:05:14 +09:00
klaus e5acc5468f 콘텐츠 메인 다시듣기 탭
- 채널별 라이브 다시듣기 추가
2025-02-15 03:04:29 +09:00
klaus 28ec227658 콘텐츠 메인 ASMR 탭
- 채널별 추천 ASMR 추가
2025-02-15 02:53:40 +09:00
klaus 022b51488f 새로운 ASMR, 라이브 다시듣기 전체보기 페이지 추가 2025-02-15 01:13:22 +09:00
klaus 2573a50190 새로운 알람 전체보기 페이지 추가 2025-02-15 00:43:01 +09:00
klaus d612bbb0f2 완결 시리즈
- 페이지 추가
2025-02-14 19:09:37 +09:00
klaus ef32eb70dd 새로운 콘텐츠 전체보기
- 무료 플래그를 추가하여 무료콘텐츠만 조회가 가능하도록 수정
2025-02-14 18:31:36 +09:00
klaus b331048dec 콘텐츠 메인 - 무료 탭
- 크리에이터 소개 전체보기 페이지 추가
2025-02-14 15:31:01 +09:00
klaus f5979ef745 콘텐츠 메인 - 시리즈 탭 - 채널별 추천 시리즈
- 채널 터치 액션 추가
2025-02-14 14:21:24 +09:00
klaus 2ff2c2224a 콘텐츠 메인 - 시리즈 탭
- 장르별 추천 시리즈 장르 터치 액션 추가
2025-02-14 13:58:12 +09:00
klaus e1028ada43 콘텐츠 메인 - 시리즈 탭
- 오리지널 오디오 드라마 전체보기 페이지 추가
2025-02-14 05:22:08 +09:00
klaus cc10bce487 콘텐츠 메인 - 시리즈 탭
- 처음 로딩시 섹션 숨김 처리
2025-02-14 04:44:25 +09:00
klaus 46ae544cfd 콘텐츠 메인
- 무료 탭 UI 구성
2025-02-14 01:56:28 +09:00
klaus b2bf9a4a4a 콘텐츠 메인
- 다시듣기 탭 UI 구성
2025-02-13 22:53:10 +09:00
klaus 0ed812c6f8 콘텐츠 메인
- ASMR 탭 UI 구성
2025-02-13 22:47:47 +09:00
klaus a375839506 콘텐츠 메인
- 선택된 탭으로 탭이 스크롤 되도록 수정
2025-02-13 19:12:14 +09:00
klaus 519d0cd02a 콘텐츠 메인
- 선택된 탭으로 탭이 스크롤 되도록 수정
2025-02-13 19:08:18 +09:00
klaus 363d611e0f 콘텐츠 메인
- 단편 탭 UI 구성
2025-02-13 16:30:40 +09:00
klaus f75134c7e7 콘텐츠 메인
- 시리즈 탭 UI 구성
2025-02-13 13:27:45 +09:00
klaus 42e4c4649b 콘텐츠 메인
- 탭 구성
2025-02-12 01:55:10 +09:00
klaus 5469d288ba 콘텐츠 메인 업데이트
- 홈 UI 업데이트
2025-02-10 03:08:10 +09:00
klaus c7b238f975 본인인증 완료
- 남성이면 남성향, 여성이면 여성향으로 콘텐츠 보기 설정이 변경되도록 수정
2025-02-06 20:28:50 +09:00
klaus 09209c150f 본인인증 완료
- 앱 재시작 되도록 수정
2025-02-03 18:54:23 +09:00
klaus 2802448fe9 오디션 이용방법 링크 추가 2025-01-20 22:16:21 +09:00
klaus a167e976a8 콘텐츠 배너
- 시리즈에 연결되는 배너 타입 추가
2025-01-17 14:35:54 +09:00
klaus 8c848bd1ef ScrollView 내부의 최상위 레이아웃을 ConstraintLayout에서 LinearLayout으로 변경 2025-01-15 23:45:29 +09:00
klaus d860cd0552 versionName 1.26.0, versionCode 138 2025-01-09 05:32:32 +09:00
klaus 7d2bed2c8a versionName 1.26.0, versionCode 137 2025-01-09 04:53:27 +09:00
klaus 35f90884d2 콘텐츠 메인, 채널 검색
- 검색창 힌트 글자색 eeeeee -> 555555 변경
2025-01-09 04:32:23 +09:00
klaus 2667e29bb9 콘텐츠 메인
- 채널 검색 추가
2025-01-09 03:54:02 +09:00
klaus d00a5475ce 콘텐츠 메인
- 사용하지 않는 UI 제거
2025-01-09 03:01:29 +09:00
klaus 809c924bfc 푸시를 눌러서 온 오디션
- 콘텐츠 탭으로 이동하지 못하던 버그 수정
2025-01-09 01:34:49 +09:00
klaus bb944f7903 오디션 - 오디션 알림
- 푸시 알림을 누르면 오디션 탭으로 이동
2025-01-08 23:10:20 +09:00
klaus 273ddb8b97 오디션
- 오디션 알림 받기 설정 추가
2025-01-08 22:11:49 +09:00
klaus 4180736065 오디션 상세
- 오디션 캐릭터 리스트 영역이 오디션 캐릭터 제목 밑으로 오도록 수정
2025-01-08 03:56:40 +09:00
klaus 7b129309e5 커뮤니티 댓글
- 대댓글의 닉네임과 댓글의 글자색상, 글자크기를 원댓글과 동일하게 수정
2025-01-08 03:44:19 +09:00
klaus ece1d89780 콘텐츠 댓글
- 대댓글의 닉네임과 댓글의 글자색상, 글자크기를 원댓글과 동일하게 수정
2025-01-08 03:38:21 +09:00
klaus 26930c23df 오디션 정보, 오디션 캐릭터 정보
- 줄간격 5로 설정
2025-01-08 03:32:53 +09:00
klaus 1b64b84bdc 오디션 지원 버튼 텍스트
- 오디션 지원하기 -> 오디션 지원
2025-01-08 02:58:31 +09:00
klaus b3351ceb8b 마이페이지
- 내 보관함 이동 버튼 추가
2025-01-08 02:14:01 +09:00
klaus ae378409b9 시리즈 상세
- 크리에이터 프로필 터치시 채널로 이동
2025-01-08 01:21:08 +09:00
klaus 43f2c8b1e0 알람, 콘텐츠 보관함
- 아이콘 크기 72 -> 84로 변경
2025-01-08 01:10:22 +09:00
klaus 20b627202e 오디션, 오디션 상세
- 오디션 리스트, 오디션 캐릭터 리스트 아이템 사이 간격 16.7 -> 25로 변경
2025-01-08 00:53:54 +09:00
klaus 9cafb13b50 콘텐츠 메인
- 인기 크리에이터 추가
2025-01-05 17:21:11 +09:00
klaus 2bec9d4595 오디션 리스트
- ON과 OFF 사이에 구분 줄 추가 및 간격 수정
2025-01-05 13:21:12 +09:00
klaus bf6884c60c 오디션 지원 다이얼로그
- 동의하기 글자와 체크 이미지 정렬
2025-01-04 02:18:45 +09:00
klaus 8441e4e5dd 오디션 이미지
- Coil을 사용하지 않고 Glide로 변경
2025-01-04 02:01:58 +09:00
klaus a4d1d69a97 오디션 투표
- 안내 팝업의 문구 가운데 정렬
2025-01-04 01:37:57 +09:00
klaus b72d692221 오디션 배역 리스트
- 모집 완료 된 배역을 터치 되지 않도록 수정
2025-01-04 01:30:10 +09:00
klaus 0f9a03fef9 오디션 지원 리스트
- 닉네임과 재생 ProgressBar의 간격 2.3 -> 8dp 로 수정
2025-01-04 01:27:27 +09:00
klaus 87f02918f8 채널 상세
- 크리에이터가 아닌 유저의 채널에 접근했을 때는 팬톡과 유저 정보만 보이도록 수정
2025-01-04 01:22:16 +09:00
klaus b5546b4957 오디션 배역 상세
- 오디션 지원하기 버튼이 오디션 지원현황을 가리지 않도록 최하단 마진을 0에서 33dp로 수정
2025-01-03 21:47:56 +09:00
klaus 83e08b3437 오디션 지원자
- recyclerview.setHasFixedSize(false)로 지정하여 데이터가 0개에서 1개가 될 때 갱신되지 않는 버그 수정
2025-01-03 20:00:24 +09:00
klaus 931a9433f3 오디션 지원자
- 오디오 재생시 시간과 ProgressBar 추가
2025-01-03 19:47:10 +09:00
klaus 679e9ed349 오디션 지원자 투표
- 재생/일시정지, 투표 터치 영역 변경
2025-01-03 17:45:22 +09:00
klaus 5e6225d14c 오디션 지원자 투표
- 안내 팝업 Dialog 추가
2025-01-03 17:42:19 +09:00
klaus b0a97ab941 오디션 지원자 콘텐츠 재생/정지 기능 추가 2025-01-03 11:30:42 +09:00
klaus 92b201a6fa 커뮤니티 콘텐츠 재생
- 플레이어(재생목록)의 재생이 멈추도록 코드 수정
2025-01-03 10:11:41 +09:00
klaus ee396b3102 오디션 투표
- 안내 팝업 추가
- 투표시 데이터가 변경되지 않던 버그 수정
2025-01-03 08:50:28 +09:00
klaus 4e14765e94 오디션 지원 기능 추가 2025-01-03 07:48:34 +09:00
klaus c6ef5970a5 오디션 배역 상세
- 지원 리스트 UI 작성
- 투표 API 적용
2025-01-03 04:54:46 +09:00
klaus 968428cfe0 배역 상세 페이지 추가 2025-01-02 17:02:15 +09:00
klaus 5d6ea6774b 오디션 상세 페이지 추가 2024-12-31 12:49:16 +09:00
klaus 4331792b75 오디션 탭 추가 2024-12-31 07:31:37 +09:00
klaus 2506ba4353 서비스가 종료되었더라도 재생버튼을 누르면 다시 실행되도록 수정 2024-12-18 20:29:26 +09:00
klaus 187f1f9d37 최근 사용한 앱에서 태스크 종료
- service가 종료되도록 코드 추가
2024-12-18 18:24:02 +09:00
klaus 6dd9520bda 재생 목록 만들기 / 수정
- 설명 입력창 minHeight 제거
2024-12-18 17:40:14 +09:00
klaus 5f0323e0a9 미디어 알림 삭제 후 재생 버튼 터치시 재생이 되지 않던 버그 수정 2024-12-18 17:01:41 +09:00
klaus f78a13bd2c MediaSession 알림 터치시 메인 페이지로 이동 2024-12-18 16:47:57 +09:00
klaus 46ec9ff999 재생 목록 수정
- 콘텐츠 수정시 이전 콘텐츠가 재생목록에서 제거되지 않던 버그 수정
2024-12-18 15:46:23 +09:00
klaus a7f67dc72e 플레이어 재생 완료
- 다음 재생할 콘텐츠가 없을 때 서비스를 종료해 플레이어에서 재생 및 이전/다음 콘텐츠 재생이 안되는 버그 수정
2024-12-18 15:23:08 +09:00
klaus 8867fe9a1c 구매목록
- orderType 파라미터 제거
2024-12-18 04:27:31 +09:00
klaus 6da8846460 재생 목록 수정
- 콘텐츠가 삭제 되지 않는 버그 수정
2024-12-18 01:26:41 +09:00
klaus 057e21570b 재생 목록 플레이어
- 재생 목록 리스트 추가
2024-12-13 23:21:58 +09:00
klaus 8c8b8c1747 개별 콘텐츠 재생 <-> 재생목록 재생
- 기존에 재생 중인 콘텐츠(재생목록)를 멈추고 신규로 재생한 콘텐츠(재생목록)가 단독으로 재생되도록 수정
2024-12-13 22:58:53 +09:00
klaus cb1dadab9d 플레이어
- 커버이미지 roundCorder 12dp
2024-12-13 22:47:26 +09:00
klaus 29595670af 메인페이지
- 하단에 미니 플레이어 추가
2024-12-13 22:30:05 +09:00
klaus 6da3192fe8 로그아웃, 라이브 입장
- 플레이어 서비스 중단 로직 추가
2024-12-13 20:58:57 +09:00
klaus c83a865032 재생목록 상세
- 하단에 미니 플레이어 추가
2024-12-13 20:53:49 +09:00
klaus 316c4399ce 재생목록 플레이어
- 재생, 이전, 다음 기능 추가
2024-12-13 13:28:27 +09:00
klaus 40e82a3796 불필요한 클래스 제거 2024-12-12 12:37:06 +09:00
klaus a7a7eb3e3f 콘텐츠 플레이어
- BottomSheet 로 수정
- 콘텐츠 url 생성 api
2024-12-07 00:17:25 +09:00
klaus a4b1ef0005 재생 목록 수정 페이지 추가 2024-12-05 18:52:28 +09:00
klaus 326ad01983 재생 목록 만들기 - 콘텐츠 리스트
- 소장 콘텐츠만 나오도록 수정
2024-12-04 19:14:23 +09:00
klaus 6fbe7da71e 재생 목록 상세
- 삭제 기능 추가
2024-12-04 18:10:50 +09:00
klaus 4012b44344 재생 목록 상세 콘텐츠
- 크리에이터 닉네임 추가
2024-12-04 13:52:21 +09:00
klaus 2ee62ac900 내 보관함
- 탭 왼쪽 정렬
2024-12-04 11:50:07 +09:00
klaus d9e39f88a8 재생목록 상세
- 콘텐츠 리스트 표시
2024-12-04 11:45:27 +09:00
klaus 848f0b44f6 새 재생목록 만들기
- 콘텐츠 추가 로직
- 재생목록 생성 API 연동
2024-12-04 11:14:43 +09:00
klaus 40335fb7ff 새 재생목록 만들기
- 새로운 콘텐츠 추가 페이지 추가
2024-12-03 23:49:25 +09:00
klaus ad5a84c3b8 새 재생목록 만들기
- 페이지 추가
2024-12-03 14:59:17 +09:00
klaus f7073ec422 폰트 파일 변경
- 공식 페이지에서 다시 가져옴
2024-12-03 14:58:56 +09:00
klaus 943660f98e 콘텐츠 보관함(구매목록)
- 중복구현 제거
2024-11-29 15:41:57 +09:00
klaus 5640a28fdb 내 보관함
- 구매목록 UI 연결
2024-11-29 15:34:18 +09:00
klaus ab89b6e21a 재생목록과 구매목록이 탭이 있는 내 보관함 페이지 추가 2024-11-29 15:10:10 +09:00
klaus b38ada0b73 재생목록 상세 페이지 2024-11-29 12:15:32 +09:00
klaus f72f894727 재생목록 리스트 페이지 2024-11-28 17:58:10 +09:00
klaus cf4854c78d 콘텐츠 플레이어
- 파일명, 클래스명 수정
2024-11-26 12:44:04 +09:00
klaus 3004018ea9 콘텐츠 플레이 리스트
- 플레이어 UI 추가
2024-11-26 02:30:03 +09:00
klaus 6b6280a782 팬토크
- 내 글 수정 모드 상태에서 해당 글을 삭제하면 수정모드가 풀리지 않아 다른 사람 글을 수정할 수 있는 것처럼 보이는 버그 수정
2024-11-26 01:02:49 +09:00
klaus f9577909ff 콘텐츠 댓글
- 내 댓글 수정 모드 상태에서 해당 댓글을 삭제하면 수정모드가 풀리지 않아 다른 사람 댓글을 수정할 수 있는 것처럼 보이는 버그 수정
2024-11-26 00:18:27 +09:00
klaus 49f9310fc3 라이브 비밀 미션
- 채팅 이모지 변경
2024-11-21 02:30:53 +09:00
klaus abc12e38b5 프로필 수정
- 비밀번호 변경, 닉네임 변경, 관심사 선택 글자색, 배경색을 버튼색(#3bb9f1)로 수정
2024-11-21 01:24:24 +09:00
klaus 57c66955f6 firebase fetch 간격 수정
- 300초 -> 60초
2024-11-21 01:07:12 +09:00
klaus 5112117155 점검중 플래그 추가 2024-11-21 00:58:52 +09:00
klaus 8d67571319 라이브 상세
- website, blog, instagram, youtube icon 색상 button 색으로 변경
2024-11-21 00:43:24 +09:00
klaus 1885482055 비밀 미션
- 비밀 미션 채팅 배경색 : 보라색으로 고정
2024-11-21 00:31:56 +09:00
klaus b67df96c85 비밀 미션
- 비밀 미션 이용 최소 금액 : 10캔 으로 설정
2024-11-21 00:26:08 +09:00
klaus 71f5dc9ef1 versionCode 125, versionName: 1.24.0 2024-11-20 23:43:35 +09:00
klaus 6efb5a679d 의존성 라이브러리 version up 2024-11-15 21:57:13 +09:00
klaus 9ae34fa667 라이브 방
- 비밀 미션 채팅 배경색 변경
2024-11-12 02:35:31 +09:00
klaus 3136b47838 하트 랭킹 추가 2024-11-12 00:32:21 +09:00
klaus c15e9c203e 후원 히스토리
- 내 후원과 다른 사람의 후원 배경색 분리 (59548f)
2024-11-11 14:11:25 +09:00
klaus c72c1e16fd 비밀후원 -> 비밀미션 2024-11-08 23:20:29 +09:00
klaus 03712558e8 콘텐츠 구매확인 다이얼로그
- 대여만 가능시 100% 가격을 표시하도록 수정
2024-11-08 21:03:25 +09:00
klaus 6689932393 콘텐츠 상세
- 소장만, 대여만 가능시 구매하기 버튼 배경색 변경
- 소장만, 대여만 가능시 구매하기 버튼을 터치하면 바로 구매확인 다이얼로그 표시
2024-11-08 16:47:15 +09:00
klaus 140f933db7 콘텐츠 업로드
- 소장만 가능한 콘텐츠 업로드 기능 추가
2024-11-08 16:11:35 +09:00
klaus 6f1dcb4632 콘텐츠 업로드
- 대여 가격 안내 문구 60% -> 70%로 수정
2024-11-08 00:39:22 +09:00
klaus b89e563023 라이브방 하트 메시지
- 글자색과 배경색 변경
2024-11-07 16:30:09 +09:00
klaus 5a37ba8be0 라이브방
- 룰렛설정, 룰렛, 후원 버튼 터치시 키보드가 내려가도록 설정
2024-10-30 15:21:59 +09:00
klaus 37d47efe2c 라이브 후원 - 비밀후원 체크박스
- 체크박스가 선택된 이미지로 변경되지 않던 버그 수정
2024-10-30 12:02:31 +09:00
klaus f6c5be24d8 콘텐츠, 커뮤니티 댓글 입력창
- 길게 입력하면 줄바꿈 되어 화면 내에서 입력한 글이 모두 보이도록 수정
2024-10-29 14:44:19 +09:00
klaus 0c8241fba7 콘텐츠 대여 가격
- 소장가격의 60%에서 70% 표기로 변경
2024-10-29 14:33:40 +09:00
klaus dc2cda58b2 라이브
- 우측 하단 옵션 버튼 순서 변경
2024-10-29 13:54:36 +09:00
klaus 936074081c 라이브 후원
- 비밀후원을 체크하면 힌트 메시지에 '비밀' 추가
- 후원메시지 최대 길이 50 -> 200 변경
2024-10-29 00:30:34 +09:00
klaus a556378ffe 라이브방 - 라이브 후원랭킹 리스트
- 방장은 일반후원 / 비밀후원 캔을 나눠서 보이도록 수정
2024-10-28 23:49:30 +09:00
klaus 960dda8d40 콘텐츠 상세 - 콘텐츠 설명글
- textSize: 14
- color: 909090
- font: gmarket_sans_medium
2024-10-28 13:54:13 +09:00
klaus a401a2e13a versionCode 120, versionName 1.22.2 2024-10-28 13:47:19 +09:00
klaus f51f7ef412 라이브방
- 하트 알림 바(채팅) 배경, 닉네임, 글자색 변경
- 하트 알림 바 위치 수정 : 채팅 -> 공지 밑
2024-10-27 23:36:28 +09:00
klaus 04151168ca 라이브방
- 채팅창 너비 화면 가득 채움
2024-10-27 21:51:13 +09:00
klaus 99a93001bc 알람 권한 설정
- 사용 하지 않는 권한을 런타임 권한으로 물어 문제가 생기는 버그 수정
2024-10-27 18:28:20 +09:00
klaus e6339bb4c2 개수 제한 콘텐츠
- 내 콘텐츠는 sold out 되더라도 재생이 가능하도록 수정
2024-10-24 23:52:50 +09:00
klaus 7209f972d2 라이브 방 - 채팅 EditText
- inputType에서 textMultiLine를 제거하여 imeOptions의 actionSend설정이 적용되도록 수정
2024-10-24 01:45:47 +09:00
klaus 0714918338 라이브 방 - 하트 후원 채팅
- 채팅 배경색 변경
2024-10-23 01:39:03 +09:00
klaus 94d581a4f3 라이브 방 - 하트 후원 애니메이션
- 룰렛의 활성화/비활성화시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 22:16:13 +09:00
klaus 577e864b6a 라이브 방 - 하트 후원 애니메이션
- 스피커/리스너 변경시 키보드를 숨기고 하트 후원 애니메이션 시작 위치 재계산
2024-10-22 21:43:56 +09:00
klaus 96a3ef44f6 라이브 방
- 하트 후원시 채팅으로 알림
2024-10-17 15:01:05 +09:00
klaus 3a33153361 라이브 방
- 하트 후원 안내 팝업 추가
2024-10-16 19:08:39 +09:00
klaus ad0c18dceb 라이브 방
- 하트 총 개수 조회 기능 추가
2024-10-16 18:31:14 +09:00
klaus e964679154 라이브 방
- 하트 후원 API 연결
- 하트 후원 성공시 하트 애니메이션 호출
2024-10-16 18:06:38 +09:00
klaus 6c9ace146d 라이브 방
- 하트 애니메이션 추가
2024-10-16 15:54:08 +09:00
klaus c7409e4dec 라이브 방
- 채팅창 너비 축소
- 오른쪽 하단 옵션 버튼 baseline이 채팅창 baseline과 동일하게 설정
- 좋아요(누르면 1캔 후원) 버튼 추가
- 좋아요 개수 UI - 후원 캔 왼쪽에 추가
2024-10-16 02:25:55 +09:00
klaus a4ff89cec0 사용하지 않는 아이콘 제거 2024-10-15 23:54:52 +09:00
klaus b489f46910 푸시 알림 아이콘 - 앱 로고 아이콘으로 변경 2024-10-15 23:53:31 +09:00
klaus 4a167a00bd 권한 수정 2024-10-15 19:02:10 +09:00
klaus 2aca7620e7 권한 수정 2024-10-14 17:51:43 +09:00
klaus 1f7f3bfdb1 큐레이션 콘텐츠
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 02:21:39 +09:00
klaus 90ff8ceb72 콘텐츠 메인 - 추천시리즈, 모닝콜, 숏플, 라이브 다시보기
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품만 조회되도록 수정
2024-10-14 01:34:35 +09:00
klaus e8b69cc6b9 가이드 이미지 변경 2024-10-13 22:04:35 +09:00
klaus 9143e74a72 소다로 살다 -> 보이스 모닝콜 등록 변경 2024-10-13 21:55:51 +09:00
klaus 7bb6e2ae45 스플래시 이미지
- 글자 크기 변경
2024-10-13 21:21:02 +09:00
klaus 6f4f500aec 소다라이브 -> 보이스온 2024-10-12 01:17:48 +09:00
klaus d328bdb4d1 스플래시 변경 2024-10-12 01:03:26 +09:00
klaus 7cb3d19e85 콘텐츠 전체 보기
- 콘텐츠 제목 영역과 가격 표시 영역이 겹치지 않도록 수정
2024-10-11 19:40:25 +09:00
klaus 7c320b7f23 콘텐츠, 라이브 메인
- 보이스 모닝콜 메뉴 추가
- 라이브 다시듣기 메뉴 라이브 메인으로 이동
2024-10-11 14:11:56 +09:00
klaus f2cca1e14b 설정
- 19금 콘텐츠 보기 설정 UI depth 추가
- 콘텐츠 보기 설정으로 제목 변경
2024-10-10 15:42:19 +09:00
klaus f3553c3c59 라이브, 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 민감한 콘텐츠(19금) 설정 추가
2024-10-10 13:36:33 +09:00
klaus 5daddc5fef 설정
- 19금 콘텐츠 보기 설정 UI 추가
2024-10-10 13:10:40 +09:00
klaus 8cee9fb019 라이브 메뉴 설정 페이지 추가 2024-10-08 16:13:30 +09:00
klaus 38e4122570 크리에이터 채널
- 메뉴 설정 버튼 추가
2024-10-07 23:41:59 +09:00
klaus 3c178dbb96 룰렛 설정 완료 메시지
- 채널에서 새 룰렛을 저장할 때 성공메시지 수정
2024-10-04 13:58:40 +09:00
klaus 4537a95d2d 불필요한 파일 삭제 2024-10-03 00:50:50 +09:00
klaus fd4dfc2dff 시리즈 소개
- 연재 요일 표시에서 마지막에 있는 요일 제거
2024-10-02 18:06:04 +09:00
klaus 2d9ec631aa 스플래시 2024년 10월 2024-10-02 15:23:27 +09:00
klaus b59172d608 라이브 방 - 시그니처 이미지
- 로딩이 완료된 시점부터 이미지 표시 시간을 계산하여 최대한 오차가 없도록 조정
2024-09-30 22:03:49 +09:00
klaus 790e42035f 크리에이터 채널
- 룰렛 설정 메뉴 추가
2024-09-30 21:10:35 +09:00
klaus 7782fe389e 라이브 수정
- 테두리 색 변경
2024-09-25 14:52:56 +09:00
klaus 84b64d3283 라이프 참여자 리스트
- 스피커 표시 최대 5명
2024-09-24 19:39:32 +09:00
klaus 66a83a118f 콘텐츠 메인
- 배너 indicator 색상 변경
2024-09-24 14:08:30 +09:00
klaus 3494ff0491 콘텐츠 상세
- 시간표시 슬라이더 색상변경
2024-09-24 14:01:50 +09:00
klaus d51fc88813 콘텐츠 업로드
- 미리듣기 최소 시간 15초로 변경
2024-09-24 13:47:14 +09:00
klaus 1930014498 라이브 비밀후원
- 방장인 경우 비밀후원도 후원 총액에 반영되도록 수정
2024-09-24 12:14:47 +09:00
klaus 4189ee4e54 라이브
- 큰 음소거 이미지 변경
2024-09-20 16:12:13 +09:00
klaus a191e295da 라이브
- 최대 스피커 수 방장 포함 6명으로 변경
2024-09-20 16:10:48 +09:00
klaus 0709c68653 크리에이터 채널
- 공유버튼 변경
2024-09-20 15:34:00 +09:00
klaus 084c306159 시리즈 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 12:11:33 +09:00
klaus 2ed77a3332 콘텐츠 상세 - 크리에이터 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 11:44:13 +09:00
klaus 5b2c5a6e2f 팔로잉/팔로워 리스트 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-20 01:57:27 +09:00
klaus 5063ce815d 크리에이터 채널 - 팔로우와 알림설정
- 팔로잉 상태에서 알림 켜기/끄기 상태 추가
2024-09-13 16:11:39 +09:00
klaus 78323103fd 라이브 정보 수정
- 연령 제한 설정 추가
2024-09-11 23:26:45 +09:00
klaus 772005910c 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 16:27:32 +09:00
klaus 1f36d8ef25 커뮤니티 댓글, 팬토크, 콘텐츠 댓글
- 프로필 이미지 터치시 차단, 신고가 가능한 유저 프로필 표시
2024-09-07 01:28:50 +09:00
klaus 1b8d65c3ea 라이브 방 - 유저 차단 팝업
- 리스너가 차단할 때 팝업 문구 수정
2024-09-05 20:01:16 +09:00
klaus 07df9ced75 PG 결제 - 카카오페이 결제
- 구매자 정보 추가
2024-09-05 16:07:27 +09:00
klaus 9dfd73b090 라이브 방
- 차단한 유저의 채팅이 보이지 않도록 수정
2024-09-04 22:29:55 +09:00
klaus 0ebb34a2df 차단 리스트 페이지 추가 2024-09-04 12:31:32 +09:00
klaus 6869cd62ea 팔로잉 리스트
- 팔로우 한 채널이 없는 경우 '팔로우 중인 채널이 없습니다.' 표시
2024-09-02 18:01:18 +09:00
klaus b7b73bb409 마이 페이지
- 팔로잉 리스트 버튼 추가
2024-09-02 17:47:28 +09:00
klaus 7bf3728cf9 커뮤니티 댓글
- 비밀 댓글 체크박스 보이지 않도록 수정
2024-09-02 14:32:12 +09:00
klaus cc01312ae5 콘텐츠 댓글
- 일반 댓글의 닉네임이 잘리는 버그 수정
2024-08-30 19:00:35 +09:00
klaus cf94615e71 라이브 방 비밀후원, 콘텐츠 비밀댓글
- 체크박스 네모로 변경
2024-08-30 18:17:06 +09:00
klaus 51cb36b2e6 라이브 방
- 후원현황과 후원하기 버튼 순서 변경
2024-08-30 15:07:35 +09:00
klaus 345df7a7b7 콘텐츠 댓글 리스트
- 비밀댓글은 닉네임 옆에 '비밀댓글' 마크 추가
2024-08-30 15:03:46 +09:00
klaus 4961727237 콘텐츠 댓글 리스트
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 14:42:20 +09:00
klaus 6640130ef9 콘텐츠 상세
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 13:11:56 +09:00
klaus e38778d9e7 캔 결제수단 추가
- 카카오페이
2024-08-27 19:33:58 +09:00
klaus 63e9b705a9 스플래시 2024/09 2024-08-27 00:06:41 +09:00
klaus 38864ca666 포그라운드 서비스 시작시 foregroundServiceTypes 추가 2024-08-26 15:24:45 +09:00
klaus adab4aee48 라이브 후원
- 비밀 후원 기능 추가
2024-08-24 00:37:53 +09:00
klaus 3bad5c5e55 콘텐츠 메인
- 우측 최상단에 콘텐츠 보관함, 알람 아이콘 추가
- 배너와 추천 시리즈 순서 변경
2024-08-21 21:49:42 +09:00
klaus 7607c10bdc @Keep 어노테이션을 추가하여 난독화에서 제외되도록 수정 2024-08-21 20:22:39 +09:00
klaus 4349f2bd3a targetSdk 34로 변경
커스텀 액션에서 패키지를 명시
2024-08-21 12:44:51 +09:00
klaus 62abd3c900 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-14 18:34:39 +09:00
klaus 63193c82a1 versionCode 96, versionName 1.15.1 2024-08-12 17:22:45 +09:00
klaus 6431577bf1 알람
- 날짜 선택 추가
2024-08-12 16:58:23 +09:00
klaus b56a0d58bf 알람
- 볼륨 설정 추가
2024-08-09 15:22:17 +09:00
klaus 00774739ed 팬토크
- 응원글 등록/수정/삭제시 전체 데이터를 지우고 다시 조회하도록 수정
2024-08-08 02:19:26 +09:00
klaus b98cc8ed79 크리에이터 커뮤니티 오디오 녹음
- 파일명 수정
2024-08-08 02:02:21 +09:00
klaus 81faf3f7ee 크리에이터 커뮤니티 게시글 오디오 재생
- 크리에이터 커뮤니티 게시글 오디오 재생시 백그라운드에서 재생 중인 오디오 콘텐츠 일시정지
2024-08-06 12:14:15 +09:00
klaus 14c72e8259 크리에이터 커뮤니티 게시글 전체리스트
- 오디오 콘텐츠 재생기능 추가
2024-08-06 00:11:35 +09:00
klaus 94d719a814 크리에이터 커뮤니티 게시글 등록- 오디오 녹음 다이얼로그
- x버튼을 눌렀을 때 올리지 않을 파일을 삭제하는 로직 추가
- 드래그 해서 화면이 꺼지지 않도록 수정
2024-08-05 21:31:03 +09:00
klaus 66fecf1509 크리에이터 커뮤니티 게시글 등록- 오디오 녹음
- 녹음 시간 : centiseconds 까지 표시
- 음질 조정 - sampleRate: 48k, bitrate: 256k
2024-08-05 17:54:18 +09:00
klaus e6e3df701d 크리에이터 커뮤니티 게시글 등록
- 오디오 녹음 기능 추가
2024-08-02 18:03:56 +09:00
klaus 76aaaddb5a 음성메시지
- 속닥 -> 메시지
- 다시 녹음 버튼 글자색 #3bb9f1
2024-08-02 17:05:28 +09:00
klaus fa23263b19 온보딩 이미지 변경 2024-07-31 23:32:10 +09:00
klaus 2ba2014f82 알람 설정
- 처음 설정시 알람 id값을 받아오지 않아 알람이 울리지 않던 버그 id값을 받아오도록 수정
2024-07-31 22:08:31 +09:00
klaus ee6090c103 알람 권한 요청 코드 추가 2024-07-31 19:59:48 +09:00
klaus 063158e9e0 회사정보 고객센터 이용시간 추가 2024-07-31 18:30:20 +09:00
klaus a1ac80c180 versionCode 90, versionName 1.14.0 2024-07-31 12:10:47 +09:00
klaus 42a8c43ff5 앱 가이드 이미지 변경 2024-07-30 22:46:00 +09:00
klaus 1fcd3cc309 앱 아이콘 변경 2024-07-30 22:16:56 +09:00
klaus 66cdb621ca 스플래시 변경 2024-07-30 21:34:15 +09:00
klaus a618903f0f 알람
일회성 알람의 경우 알람이 울리면 날짜를 다음날로 변경하고 isEnable을 false로 변경한다.
2024-07-29 12:20:58 +09:00
klaus 4fc39f10b1 알람
setExactAndAllowWhileIdle -> setAlarmClock 로 변경
2024-07-29 11:42:52 +09:00
klaus b393ddcd74 label 수정
알람 -> 소다로 살다
2024-07-26 16:05:23 +09:00
klaus 7aa80154fa label 수정
알람 -> 소다로 살다
2024-07-26 15:03:09 +09:00
klaus 262f42c194 PG 심사를 위해
- 소장 기간을 PG심사용 계정이면 (이용기간 1년)으로 나오도록 수정
2024-07-26 14:45:01 +09:00
klaus 18d16a2211 sdk 31이상에서 PendingIntent.FLAG_IMMUTABLE FLAG 추가 2024-07-26 01:43:00 +09:00
klaus 7c91795fb5 알람
- 추가 슬롯 구매하기 기능 추가
2024-07-26 00:52:34 +09:00
klaus 856e13d37b 알람
- 알람음 -> 콘텐츠로 변경
2024-07-25 18:21:33 +09:00
klaus 2223919a98 알람
- 소장한 콘텐츠만 알람으로 지정 가능하다는 안내문구 추가
2024-07-25 18:20:24 +09:00
klaus 7b4063420e 알람
- 최대 3개까지 등록되도록 수정
2024-07-25 16:24:43 +09:00
klaus 839ff7463e 알람 기능 추가 2024-07-25 16:10:38 +09:00
klaus 7587b8bc25 PG 결제 - 휴대폰
- 헥토파이낸셜로 변경
2024-07-19 15:06:38 +09:00
klaus 756675e622 versionCode 82 2024-07-05 21:18:47 +09:00
klaus 2f059cc783 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 21:32:23 +09:00
klaus 573d5a89f9 회원가입
- 개인정보 처리방침 터치시 이용약관이 나오는 버그 수정
2024-07-02 18:39:30 +09:00
klaus 40472bdced PG 수정 - payment method
- 디지털 카드 -> 카드
- 디지털 계좌이체 -> 계좌이체
2024-07-02 14:49:20 +09:00
klaus ac7124a7e6 PG 수정
- 헥토 검증 API 추가
2024-06-29 18:59:51 +09:00
klaus 888674b776 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-06-29 18:33:30 +09:00
klaus c9f6088754 라이브 방
- 공유하기 추가
2024-06-27 19:28:49 +09:00
klaus 687aada611 콘텐츠 업로드
- 한정판 업로드 추가
2024-06-18 16:13:56 +09:00
klaus 20a1e8f1d7 versionCode "80"
versionName "1.12.3"
2024-06-05 19:16:04 +09:00
klaus 357d6c3406 크리에이터 커뮤니티
- 커뮤니티 글이 보이지 않던 버그 수정
2024-06-05 19:13:12 +09:00
klaus 40ff959289 콘텐츠 리스트
- 소장중 / 대여중 / Sold Out 표시
2024-06-04 19:56:46 +09:00
klaus 22825d1520 회사정보 주소 수정 2024-05-30 13:26:40 +09:00
klaus 827a8258ec 크리에이터 커뮤니티 게시물
- 구매하지 않은 유료 게시물도 내용은 보이도록 수정
2024-05-30 13:24:40 +09:00
klaus 8505be882c 스플래시 06월 2024-05-28 16:23:28 +09:00
klaus d0eb4103ba 크리에이터 커뮤니티
- 유료 & 구매하지 않은 게시글만 이미지가 Blur 처리 되도록 수정
2024-05-28 12:23:54 +09:00
klaus ba83027dc9 회사정보 대표 이메일 추가 2024-05-25 00:09:40 +09:00
klaus 7b3fa0b224 콘텐츠 메인 탭
- 하단에 회사정보 추가
2024-05-24 23:38:16 +09:00
klaus eb7f83433c 마이페이지 탭
- PG심사용 계정의 경우 캔 표시 영역 뷰 제거
2024-05-24 00:19:24 +09:00
klaus 9d5ca7c36d 커뮤니티 유료 게시글 조회, 구매 기능 추가 2024-05-23 23:13:25 +09:00
klaus a9511dcb51 커뮤니티 게시글 등록
- 유료 게시글 등록은 이미지가 있어야 등록이 가능하도록 수정
2024-05-23 01:27:29 +09:00
klaus 5d42e2b16e 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 설정 추가
2024-05-23 00:05:35 +09:00
klaus 6fac6ecd4d 확률을 입력하지 않아서 crash 나는 버그 수정 2024-05-22 19:33:15 +09:00
klaus b876695adc 회사정보 변경 2024-05-21 01:25:17 +09:00
klaus b220b8bce4 콘텐츠 상세, 콘텐츠 구매
- pg 테스트 계정의 경우 캔이 아닌 원으로 표시되도록 하고 콘텐츠 구매시 바로 결제 후 구매 되도록 수정
2024-05-20 15:37:46 +09:00
klaus ba4d707d45 9970ff -> 3bb9f1 2024-05-17 15:35:26 +09:00
klaus 2920d2a7ae 9970ff -> 3bb9f1 2024-05-17 15:28:59 +09:00
klaus b8abef6aeb 9970ff -> 3bb9f1 2024-05-17 15:11:41 +09:00
klaus 75fc34cbe3 9970ff -> 3bb9f1 2024-05-17 15:09:47 +09:00
klaus 206fc398c6 라이브, 회원 태그
- 9970ff -> 3bb9f1 색상 변경
2024-05-16 14:23:07 +09:00
klaus b95b77bcb9 룰렛 설정
- 옵션 확률 총합 추가
2024-05-16 12:41:20 +09:00
klaus 62c269cac2 룰렛 설정
- 옵션 추가시 확률이 빈칸으로 표시되어 NumberFormat Exception이 나오던 버그 예외처리
2024-05-16 12:11:05 +09:00
klaus 4b3474ff42 라이브 방 생성
- 크리에이터 입장 가능 설정 추가
2024-05-14 17:06:12 +09:00
klaus d6e9b929e9 라이브 방
- 음소거 버튼 위치 아래로 이동
2024-05-11 04:00:26 +09:00
klaus 13efce42a0 룰렛 설정
- 룰렛 1, 2, 3 버튼 bg, text 색상 변경
2024-05-11 03:55:58 +09:00
klaus 85ccc18485 룰렛 변경
- 확률 수동 설정
- 여러개의 룰렛이 켜져있을 때 선택하여 돌리기
- 후원 히스토리에 룰렛 히스토리 추가
2024-05-10 17:55:20 +09:00
klaus 9df08cdf24 콘텐츠 메인
- 새로운 콘텐츠 아래 새로고침 버튼 제거
2024-05-08 12:17:28 +09:00
klaus 7892eed443 라이브
- 라이브가 없을 때 문구 수정
- 라이브가 없을 떄 문구 자간 및 폰트 사이즈 수정
2024-05-08 12:15:13 +09:00
klaus eca68c06c3 라이브 메인 - 라이브 없을 때 문구
- 🙀마이페이지에서 본인인증을 하거나 라이브를 예약하고 참여해보세요.
2024-05-07 19:27:34 +09:00
klaus 3c82ff1c4e 라이브 메인
- 당겨서 새로고침 제거
- 새로고침 버튼 추가
2024-05-07 19:02:58 +09:00
klaus 8c6aff1623 콘텐츠 메인 - 추천 시리즈, 새로운 콘텐츠
- 새로고침 버튼 추가
2024-05-07 18:58:15 +09:00
klaus 254a1e3381 콘텐츠 메인
- 추천 시리즈 UI 추가
2024-05-07 16:42:14 +09:00
klaus dff4c833f1 . 2024-05-04 02:47:03 +09:00
klaus 5fa3a591d4 인 앱 결제
- 서버 호출 후 충전이 완료되면 충전완료 페이지로 이동하도록 수정
2024-05-03 19:47:19 +09:00
klaus 845578a1dd 시리즈 아이템
- 커버이미지 DIM 제거
2024-05-03 13:41:30 +09:00
klaus 84ac72b391 탐색
- 크리에이터가 없으면 섹션제거
2024-05-03 13:36:41 +09:00
klaus db364d9bf7 라이브
- 시그니처 ON/OFF 버튼 추가
- 공유하기 버튼 제거
2024-05-02 15:17:00 +09:00
klaus 3fe01f8def 2024년 5월 인트로 적용 2024-05-01 22:44:16 +09:00
klaus 02c815077e 시그니처 후원
- 시그니처 별로 설정된 시간 만큼 GIF가 재생되도록 기능 추가
2024-05-01 22:14:58 +09:00
klaus 108eb759ec 시리즈 전체보기
아이템 세로 간격 수정
2024-05-01 00:39:17 +09:00
klaus fef49a0d6a 시리즈 전체보기
아이템 사이즈 수정
2024-04-30 22:33:37 +09:00
klaus 87241fa8bd 시그니처 후원
- 위치 가운데로 수정
2024-04-30 19:55:47 +09:00
klaus 29d5192fff 시리즈 콘텐츠
- 대여중/소장중/가격 뱃지가 동시에 표시되는 버그 수정
2024-04-30 19:28:16 +09:00
klaus c86e55719e 인 앱 결제
- 사용하지 않는 함수 제거
2024-04-30 17:18:19 +09:00
klaus 839a8a780c 시리즈 상세 작품소개
- 0원 -> 무료로 변경
2024-04-30 14:57:32 +09:00
klaus 346334a0ba 시리즈 상세 작품소개
- 구분선 색 변경
2024-04-30 13:32:10 +09:00
klaus 7cd4d180c2 시리즈 상세 콘텐츠
- 제목이 가격영역을 침범하는 버그 수정
2024-04-29 11:47:24 +09:00
klaus 320ef4fbc7 시리즈 상세
- 19금과 전체연령가가 반대로 표시되던 버그 수정
2024-04-29 11:41:13 +09:00
klaus b4623141f3 크리에이터 채널
- 시리즈 데이터가 파싱되지 않던 버그 수정
2024-04-27 04:23:38 +09:00
klaus 30d3ed14d7 시리즈 콘텐츠 리스트 아이템
- 출시 날짜 제거
- 재생 시간 제목 윗쪽으로 이동
2024-04-27 03:14:09 +09:00
klaus ae617d5154 크리에이터 채널 시리즈
- 이미지 사이즈
가로 102 -> 116.7, 세로 144 -> 165 변경
2024-04-27 03:10:51 +09:00
klaus 9dd3c568d8 시리즈 상세
- 스크롤시 타이틀 윗쪽에 배경이 살짝 보이던 버그 수정
2024-04-27 02:54:39 +09:00
klaus f31fc7691e 시리즈 상세
- 내용 스크롤 시 뒤로가기 버튼도 같이 스크롤 되도록 수정
2024-04-27 02:01:53 +09:00
klaus 27df922383 시리즈 상세 작품소개
- 키워드 세로 간격 수정
- 상세정보 글자색 흰색으로 변경
2024-04-27 01:57:54 +09:00
klaus d5956c024d 시리즈 상세
- 팔로잉 액션 추가
2024-04-27 01:25:02 +09:00
klaus 29aca74651 시리즈 상세
- 전체회차 보기 사이즈 16으로 변경
- 태그 글자 크기 12로 변경
2024-04-27 01:08:22 +09:00
klaus f41790b302 시리즈 콘텐츠 전체보기
- 제목 - 전체회차 듣기 로 변경
2024-04-27 00:47:44 +09:00
klaus 0b999a874c 시리즈 콘텐츠 전체보기 페이지 추가 2024-04-27 00:34:42 +09:00
klaus 2778638dc9 시리즈 상세보기
- 콘텐츠 링크 추가
2024-04-26 23:27:26 +09:00
klaus cc5fe445fc 시리즈 상세보기 페이지 추가 2024-04-26 23:17:44 +09:00
klaus c310f9c57e 시리즈 전체보기 페이지 추가 2024-04-25 22:05:58 +09:00
klaus cd607425a0 크리에이터 채널
- 시리즈 section 추가
2024-04-25 17:14:19 +09:00
klaus a5df8a1110 크리에이터 채널
- 활동요약표 선 색깔 button색으로 수정
2024-04-22 15:05:16 +09:00
klaus 2bd30aa346 versionCode 57, versionName 1.9.11
인 앱 결제 로직 수정
- 결제 완료 후 서버에서 데이터 처리 후 로컬에서 다시 소비처리를 하도록 수정
2024-04-22 14:40:16 +09:00
klaus a6ce994fd0 versionCode 49, versionName 1.9.2 2024-04-12 19:57:44 +09:00
klaus 7bffd1c3c7 후원하기 팝업
- 캔 충전하기 페이지로 이동하는 버튼 직관적으로 보이도록 화살표에서 충전 글자로 변경
2024-04-12 14:30:30 +09:00
klaus dfd92d6db6 음소거 버튼 위치 - top
크리에이터 팔로우 버튼 margin_top = 5.3
시그니처 후원 움짤 위치 - 우측 중간으로 이동
2024-04-12 14:09:47 +09:00
klaus 5529872bd5 본인인증을 하지 않아도 PG결제가 보이도록 수정 2024-04-05 11:46:18 +09:00
klaus 364a530956 크리에이터 커뮤니티 대댓글
- 부모 댓글과 동일한 글자 색상으로 변경
2024-04-02 15:36:49 +09:00
klaus 0556d5a067 오픈 예정 콘텐츠 상세
- 댓글 창, 좋아요, 공유, 후원 버튼 숨김
2024-04-02 14:47:56 +09:00
klaus be46893555 캔 충전 페이지
- PG, IAP 순서로 탭 변경
2024-04-02 00:33:12 +09:00
klaus f7f789892d okhttp connect, read, write timeout 시간 60초 설정 2024-04-01 16:38:58 +09:00
klaus de0d327168 인트로
- 4월 인트로로 변경
2024-04-01 15:48:06 +09:00
klaus e52075a692 라이브
- 팔로우/팔로잉 버튼 변경
2024-03-28 02:01:24 +09:00
klaus a29c50eae3 콘텐츠 상세
- 한정판이 매진된 경우 Player 가운데 Sold Out 표시
2024-03-28 01:35:21 +09:00
klaus 8a2a497fcf 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 18:59:28 +09:00
klaus 6786988e63 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 16:59:29 +09:00
klaus 2dd5e2d96c 콘텐츠 상세
- 한정판인 경우 대여/소장 선택 다이얼로그를 생략하고 바로 구매확인 다이얼로그가 나오도록 수정
2024-03-27 16:40:15 +09:00
klaus 03320d9cec 콘텐츠 상세
- 한정판 UI 추가
2024-03-27 00:04:28 +09:00
klaus 2101cdeb86 인 앱 결제 추가 2024-03-25 17:40:17 +09:00
klaus f0a8ca5823 Toast show 로직 수정 2024-03-23 06:05:08 +09:00
klaus daed389264 인 앱 결제 로직 수정
versionName 1.8.16, versionCode 41
2024-03-23 05:37:25 +09:00
klaus 6a558ad25c versionName 1.8.7, versionCode 32 2024-03-22 12:07:56 +09:00
klaus 6e3a4e1125 캔 충전페이지
- 인 앱 결제 페이지 추가
2024-03-21 04:14:52 +09:00
klaus 79cb4b995a 캔 충전페이지
- 인 앱 결제 추가를 위해 단일 액티비티에서 탭 구성으로 변경
2024-03-20 01:24:39 +09:00
klaus 84b9dd0841 versionCode 27 versionName "1.8.2" 2024-03-15 18:05:52 +09:00
klaus 4004dcd99e 라이브 메인 지금 라이브 중
- 이미지 크기 가로 116 -> 128, 세로 164 -> 179
2024-03-15 00:49:11 +09:00
klaus ebbe7e8917 라이브 메인 지금 라이브 중, 라이브 전체 보기
- 이미지 크기 수정, 잠금 아이콘 사이즈 40 -> 60
2024-03-14 23:51:37 +09:00
klaus 196d5c6cfd 라이브 메인 - 지금 라이브 중
- 입장 가능한 잔여 인원 표시 제거
2024-03-14 23:20:00 +09:00
klaus f4626a7cd5 라이브 - 호스트는 리스너로 변경 버튼이 보이지 않도록 수정 2024-03-14 22:57:04 +09:00
klaus 4ed97d4600 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 19:13:21 +09:00
klaus c2cf05ef64 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:46:22 +09:00
klaus 0e131f6661 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:27:25 +09:00
klaus d091c47a0c 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:01:45 +09:00
klaus 6bd5d26882 지금 라이브 중 전체보기 - UI 표시 방식 수정
- 1줄에 1개 보이던 리스트 방식에서 1줄에 3개씩 표시하는 그리드 방식으로 변경
2024-03-14 17:37:05 +09:00
klaus e02ea116ff 지금 라이브 중 - 참여 가능 인원 표시 2024-03-14 17:22:37 +09:00
klaus 012d1d94d5 지금 라이브 중 - 라이브 아이템 유/무료 표시 방식 수정
- 무료 배경색 : #111111
- 유료 배경색 : #DD4500
- 유료 표시 - white 캔과 함께 100 과 같이 가격으로 변경
2024-03-14 16:50:57 +09:00
klaus 39e49b08d9 본인이 스피커일 때 리스너로 변경하는 버튼 추가 2024-03-14 14:48:43 +09:00
klaus 2a84d2dc41 시그니처 재생 길이 수정
- 3초에서 7초로 변경
2024-03-14 14:41:21 +09:00
klaus 66c9b38e04 versionCode 26 versionName "1.8.1" 2024-03-11 12:24:44 +09:00
klaus 8f35f7a573 라이브 정보 수정
- 메뉴 설정을 하지 않고 다른 라이브 정보만 수정 했을 경우 앱이 종료 되는 버그 수정
(selectedMenu가 초기화 되지 않아도 라이브 정보를 수정할 수 있도록 수정)
2024-03-11 12:16:49 +09:00
klaus c653563512 라이브 만들기
- 메뉴 등록 edittext 스크롤 적용
2024-03-08 23:04:54 +09:00
klaus a75821ed06 스플래시 화면 변경 2024-03-08 21:04:51 +09:00
klaus ca6416c697 라이브 정보 변경 다이얼로그
- 공지, 메뉴 입력 창 스크롤 적용
2024-03-08 04:55:49 +09:00
klaus f7b3caf320 스플래시 화면 변경 2024-03-08 04:49:34 +09:00
klaus 62d839b69b 시그니처 후원 이미지 표시
- gif만 표시되던 것 모든 이미지로 변경
2024-03-08 04:47:47 +09:00
klaus a9c3ea953d 시그니처 후원 이미지 표시
- 이미지 URL을 배열에 저장 후 순서대로 표시하도록 수정
2024-03-08 02:46:25 +09:00
klaus 5be9720fcc 시그니처 후원 이미지 표시
- 하단 마진 20 -> 65로 변경
2024-03-08 01:37:55 +09:00
klaus ec096b5831 후원
- 시그니처 후원 적용
2024-03-08 00:51:43 +09:00
klaus 51c5e5f32c 에러가 아닌데 에러로 표시하는 부분 warning으로 변경 2024-03-07 06:33:10 +09:00
klaus b6d7e0b0e9 불필요한 로그 제거 2024-03-07 06:32:22 +09:00
klaus 49209c4c4a 라이브 정보 수정
- 메뉴판 수정 기능 추가
2024-03-07 06:31:25 +09:00
klaus 6b466ac7d7 라이브 메인 지금 라이브 중
- 가격 유/무료 뱃지 배경색 3bb9f1(버튼색)으로 변경
2024-03-07 02:36:05 +09:00
klaus d6182f9e03 라이브 메인 추천 채널
- 라이브 중 표시 컬러 9970ff -> 3bb9f1로 변경
2024-03-07 02:33:28 +09:00
klaus 2e24a298ff 라이브 만들기
- 메뉴판 설정 추가
2024-03-07 02:29:38 +09:00
klaus d7d43bc7be 라이브
- 메뉴판 UI 추가
2024-03-06 17:02:37 +09:00
klaus 2d0c4ea738 룰렛 설정 개수에 따라 룰렛 프리셋 버튼 활성화/비활성화 2024-02-28 03:56:51 +09:00
klaus af4e802259 룰렛 설정 문구 수정 2024-02-28 03:39:42 +09:00
klaus ad9e97161c 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 여러줄 인 경우 줄간격 수정
2024-02-28 02:25:44 +09:00
klaus 1712f509dc gaid 업데이트 추가 2024-02-26 21:56:10 +09:00
klaus 78dd3b2785 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 글자 크기, 색상 수정
2024-02-24 23:30:56 +09:00
klaus f7f4638526 룰렛 프리셋 설정
- 변동 사항이 있을 경우에만 업데이트 하도록 수정
2024-02-23 18:39:42 +09:00
klaus 66690a6f89 룰렛 프리셋 설정 성공 메시지
- 현재 선택된 룰렛 프리셋의 활성화/비활성화에 따라 성공 메시지가 수정되도록 수정
2024-02-23 17:57:49 +09:00
klaus 4e56eb9bde 룰렛 프리셋 적용 2024-02-23 14:07:52 +09:00
klaus e3349e41a1 versionCode 23 versionName "1.6.1" 2024-02-16 14:10:34 +09:00
klaus 239ccb9018 튜토리얼 이미지 변경 2024-02-16 14:09:49 +09:00
klaus 6ff17e1ba6 콘텐츠 그리드 리스트
- 아이템 상하 간격 16으로 변경
2024-02-15 00:58:52 +09:00
klaus 1c88a90024 튜토리얼 이미지 순서 변경 2024-02-15 00:55:41 +09:00
klaus dd54e5b97a 튜토리얼 수정 2024-02-14 23:59:28 +09:00
klaus f4180eec14 콘텐츠 메인 숏플, 라이브 다시 듣기 버튼 수정
- 부가 설명 제거
- 버튼 배경 이미지로 변경
- 글자 크기 14.7 -> 16.7로 변경
- 아이콘 큰 사이즈로 변경
2024-02-14 23:39:53 +09:00
klaus b81576bdaf 스플래시 화면 이미지 변경 2024-02-14 23:30:40 +09:00
klaus bf43926d7d 라이브 방
- 후원 시 '20캔을' 색을 바뀌던 것을 '20캔'까지만 색이 바뀌도록 수정
2024-02-14 23:12:15 +09:00
klaus 1ec0d8540a 콘텐츠 리스트 아이템 - 사이즈, 간격 수정 2024-02-14 18:19:47 +09:00
klaus 91a2da9032 versionCode 22 versionName "1.6.0" 2024-02-14 16:49:36 +09:00
klaus a2cbeb87d5 콘텐츠 메인
- 숏플, 라이브 다시 듣기 추가
2024-02-14 02:10:30 +09:00
klaus a88f8c3316 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 14:17:57 +09:00
klaus e51af38b75 큐레이션
- 아이템 2개에서 3개로 변경
2024-02-13 02:21:28 +09:00
klaus 6a92f955ca versionCode 22 versionName "1.5.2" 2024-02-13 00:35:44 +09:00
klaus a7ef5a8147 라이브 상세
- 크리에이터는 예약자(참여자) 표시
2024-02-13 00:34:34 +09:00
klaus a14263bf27 versionCode 21 versionName "1.5.1" 2024-02-07 20:38:31 +09:00
klaus 489b09a54e 콘텐츠 전체보기 카테고리
- 아이템 간격 13.3 -> 8 로 수정
2024-02-07 18:39:58 +09:00
klaus 51b57a0b1d 콘텐츠 전체보기
- 카테고리 추가
2024-02-07 07:28:14 +09:00
klaus eafb922ae9 설정페이지
- 회사정보 추가
2024-02-05 22:06:44 +09:00
klaus 6cfbdd7acd 콘텐츠 상세
- 미리듣기가 없을 떄 문구 추가
2024-01-29 16:23:38 +09:00
klaus f8fd06706b 크리에이터 콘텐츠 리스트
- 고정 콘텐츠 핀 추가
2024-01-29 00:48:45 +09:00
klaus c23ef771be 콘텐츠 상세
- 콘텐츠 고정/해제 기능 추가
2024-01-29 00:33:50 +09:00
klaus 0bbb1e070c 콘텐츠 상세
- 미리듣기 없는 콘텐츠는 재생 버튼이 보이지 않도록 수정
2024-01-26 13:31:51 +09:00
klaus fba11ae4b9 콘텐츠 업로드
- 미리듣기 여부 선택 버튼 추가
2024-01-26 02:43:55 +09:00
klaus 6fbb98ca7b 유료방 입장 팝업
- UI 수정
- 알림 문구 수정
2024-01-22 16:09:53 +09:00
klaus b33c8ffd6d 유료방 입장 팝업
- 라이브 시작 시각 -> 시작 시각 으로 변경
2024-01-22 03:19:12 +09:00
klaus 14b3bfbae7 유료방 입장 팝업
- 1시간 이상 지난 후 팝업 내용 변경
- 라이브 시작 시각, 현재 시각 표시
2024-01-21 17:34:07 +09:00
klaus 7386c93d73 커뮤니티 글 - 줄 간격 0 -> 8 로 변경 2024-01-19 16:16:47 +09:00
klaus 4af8d7dce1 라이브 - 배경이미지 가운데 정렬 2024-01-18 18:46:59 +09:00
klaus 08d2ccdab4 라이브 - 배경이미지 상단정렬 2024-01-18 18:27:06 +09:00
klaus 0a8abeefbe 앱이 켜져 있을 때 딥링크가 동작하지 않던 버그 수정 2024-01-17 03:53:41 +09:00
klaus f90e089f8a 프로필 이미지 변경
- 이미지 변경시 로컬에 저장된 프로필 이미지 URL도 변경 하도록 수정
2024-01-17 03:14:04 +09:00
klaus 1eb905fe30 라이브
- 상단 총 받은 캔 사이즈 10->14로 변경
2024-01-16 22:44:14 +09:00
klaus fcc55288b1 라이브
- 오른쪽 하단 버튼 패딩 수정
2024-01-16 22:37:03 +09:00
klaus 97c5dc4363 라이브
- 9970ff 색상 3bb9f1로 변경
2024-01-16 22:18:05 +09:00
klaus 4223f0bc5d 라이브
- 후원, 입장, 룰렛 결과 채팅 배경색 변경
2024-01-16 21:34:48 +09:00
klaus 7ada4515aa 라이브 - 공지 활성화/비활성화 버튼 색 변경 2024-01-16 19:38:46 +09:00
klaus 8456f2b30b 라이브 - 19 배경 변경 2024-01-16 18:56:47 +09:00
klaus c20802f89c 라이브 - 공지 UI 수정 2024-01-16 18:20:03 +09:00
klaus 804f993b25 라이브 - 아고라 로컬 사용자의 음성 활동 감지를 비활성화 하여 말하는 유저 표시를 좀 더 자연스럽게 수정 2024-01-16 17:24:11 +09:00
klaus 869b83804d 크리에이터가 말할 때 크리에이터 배경 표시 2024-01-16 15:59:34 +09:00
klaus 9d97feb184 라이브 공지 버튼 수정 2024-01-16 15:53:51 +09:00
klaus 42a69609a1 라이브 배경 이미지 사이즈 비율
400:564로 변경
2024-01-16 15:10:15 +09:00
klaus 3a541f71a6 라이브 상단 UI 변경 2024-01-16 05:31:23 +09:00
klaus d75e4af348 댓글 쓰기 텍스트 필드 border 색상 변경 2024-01-15 23:38:30 +09:00
klaus 2dac54b3ec 크리에이터 커뮤니티
- 링크 적용
2024-01-15 23:36:02 +09:00
klaus 82cf1658cb 라이브 방
- 말하는 사람 배경 변경
- 배경 On/Off 색상 변경
- 참여자 숫자 색상 변경, 폰트 Bold
2024-01-15 23:11:08 +09:00
klaus 127e1f6673 본인이 스피커 인 경우 말하는 사람 배경이 잘 보이지 않는 버그 수정 2024-01-15 21:05:30 +09:00
klaus 22f1e4024e versionName 1.4.0, versionCode 18 2024-01-11 16:04:51 +09:00
klaus b0bfe2ac06 콘텐츠 상세
- 오픈 예정 날짜 데이터가 있더라도 콘텐츠를 올린 크리에이터의 경우 재생버튼이 동작하도록 수정
2024-01-11 00:27:45 +09:00
klaus d67bb8be50 크리에이터 채널, 콘텐츠 상세
- 오픈예정 추가
2024-01-10 01:57:04 +09:00
klaus eeac702cd7 다이얼로그
- 취소 버튼 글자색 변경
2024-01-09 19:46:37 +09:00
klaus c2216ab054 라이브 예약완료
- 홈으로 이동, 예약이 완료되었습니다 string 글자색 변경
2024-01-08 20:23:36 +09:00
klaus 45e5494653 콘텐츠 업로드
- 예약 업로드를 위해 날짜/시간 선택 추가
2024-01-08 15:10:30 +09:00
klaus 5f7ed22711 라이브 만들기 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 22:15:07 +09:00
klaus 49a823895d 콘텐츠 업로드 액티비티
- 요즘라이브 컬러에서 소다라이브 컬러로 변경
2024-01-05 17:38:01 +09:00
klaus 92324e3409 커뮤니티
- 이미지가 잘려서 보이는 현상 수정
2024-01-04 15:41:05 +09:00
klaus 13057af98a 쿠폰번호 입력 필터 수정
AS-IS : 영대문자와 숫자만 입력되도록 필터적용

TO-BE : 영문이 입력되면 대문자로 변경되지만 나머지 문자는 입력될 수 있도록 수정

- 기존의 InputFilter는 입력하다보면 앞에 문자가 반복해서 입력되는 기기도 있는 것 확인
2024-01-04 12:18:33 +09:00
klaus 144ff4af05 마이페이지 padding 추정 2024-01-03 15:00:22 +09:00
klaus 315e0627d1 쿠폰등록 페이지 추가 2024-01-03 05:37:21 +09:00
klaus 4bd6a766f5 메인 하단 탭, 마이페이지
- 글자 색 , 배경색 변경
2024-01-02 17:26:26 +09:00
klaus 32f99cc678 versionCode 17, versionName 1.3.3 2024-01-01 21:24:45 +09:00
klaus bd1800c2b5 라이브 - 커버이미지 수정방식 변경
AS-IS : 방 정보를 가져올 때 마다 변경
TO-BE : 이전 이미지 url과 다른 경우 에만 커버이미지 변경
2024-01-01 21:08:39 +09:00
klaus bf3e6230a0 라이브 채팅 폰트 사이즈
AS-IS : 14sp
TO-BE : 15sp
2023-12-27 00:23:01 +09:00
klaus 2400123a92 라이브 채팅 폰트 사이즈
AS-IS : 13sp
TO-BE : 14sp
2023-12-27 00:06:50 +09:00
klaus 525e399423 . 2023-12-26 22:25:05 +09:00
klaus 5da644a607 라이브 룰렛 색상 변경
AS-IS : 10가지 색상 순서 대로 표시
TO-BE : 10가지 색상 중 랜덤 으로 표시
2023-12-26 21:57:31 +09:00
klaus 3d231ea8be 라이브 채팅 폰트 변경
AS-IS : Gmarket Sans
TO-BE : System Font
2023-12-26 21:48:06 +09:00
klaus d9ad230804 라이브 룰렛 색상 수정
AS-IS : -
TO-BE : -
2023-12-26 19:18:44 +09:00
klaus 41a92a871d 스플래시 페이지 수정
AS-IS : 크리스마스 이미지
TO-BE : 신년 이미지
2023-12-26 17:41:28 +09:00
klaus 5634e0787f 룰렛 최대 개수 수정
AS-IS : 최대 6개
TO-BE : 최대 10개

룰렛 확률 수정
AS-IS : 소수점 없음
TO-BE : 소수점 2자리
2023-12-26 15:53:14 +09:00
klaus 8a7406aa22 versionCode 16, versionName 1.3.2 2023-12-26 00:31:22 +09:00
klaus ab24042863 커뮤니티 게시물 - 콘텐츠 글 더보기 기능 추가 2023-12-25 23:51:02 +09:00
klaus 5d3891c1db 게시물이 없을 때 Write 버튼이 눌리지 않는 버그 수정
versionCode 15, versionName 1.3.1
2023-12-25 20:06:51 +09:00
klaus 7d9fc13192 versionCode 14, versionName 1.3.0 2023-12-25 18:03:11 +09:00
klaus 12f944c79d 라이브 중 전체보기 - 불필요한 아바타 아이콘 제거 2023-12-25 17:03:36 +09:00
klaus e0da65e64f 커뮤니티 등록/수정 - 이미지 등록 알림 메시지 2줄로 조정 2023-12-25 16:49:13 +09:00
klaus 857b4de792 커뮤니티 수정 추가 2023-12-25 09:29:36 +09:00
klaus c896be5ece 라이브 메인 - 커뮤니티 클릭 이벤트 추가 2023-12-25 07:44:17 +09:00
klaus 6c96c4afe5 커뮤니티 신고하기 추가 2023-12-25 05:42:27 +09:00
klaus 6b2e59c09d 커뮤니티 게시물 삭제 추가 2023-12-25 05:15:50 +09:00
klaus 481cad1a46 커뮤니티 게시물 업로드 페이지 추가 2023-12-25 04:09:25 +09:00
klaus 62ecf3dd51 커뮤니티 전체보기 페이지 추가 2023-12-25 02:13:57 +09:00
klaus 116dde1b7e 메인 라이브 탭 - 커뮤니티 포스트 영역 추가 2023-12-23 00:03:03 +09:00
klaus 5db097d67c 크리에이터 채널 - 커뮤니티 영역 추가 2023-12-22 19:48:05 +09:00
klaus fbcdbf3b48 크리에이터 채널 - 공지사항 영역 제거 2023-12-22 16:24:10 +09:00
klaus 479f956db3 크리에이터 채널 - 함께 들으면 좋은 채널 제거 2023-12-22 16:18:17 +09:00
klaus 72d16c4d18 지금 라이브 중 - 라이브 없을 떄 문구 변경, 참여자 숫자 제거 2023-12-22 16:09:35 +09:00
klaus 2984aac0a5 라이브 탭 - 라이브 중 아이템 UI 변경 2023-12-14 18:27:58 +09:00
klaus 8ddf85c1be 콘텐츠 메인 API 분리 및 속도 개선 2023-12-13 15:58:31 +09:00
klaus 8f3a2f16ad versionCode 13, versionName 1.2.1 2023-12-08 23:54:13 +09:00
klaus 6a72bc63c0 라이브 배경이미지 캐시 적용 2023-12-08 23:45:47 +09:00
klaus f74d8bfc16 스플래시 - 크리스마스 로고 변경 2023-12-07 12:56:49 +09:00
klaus 29e293e6b5 룰렛 설정 - 설정된 적이 없을 때 기본 캔 설정 5 -> 0으로 변경 2023-12-07 11:06:21 +09:00
klaus 2883c63e07 versionCode 12, versionName 1.2.0 2023-12-07 09:59:47 +09:00
klaus dfaf45a83d 스플래시 - 크리스마스 버전으로 변경 2023-12-07 09:59:24 +09:00
klaus c2b99552da 라이브 방 룰렛 - 룰렛 결과 전송 데이터 수정, 룰렛 설정 완료 메시지 수정 2023-12-07 04:18:42 +09:00
klaus e4a92e0f2b 라이브 방 - 커버이미지 캐시 적용 2023-12-06 16:55:18 +09:00
klaus dabf8563b8 텍스트 메시지 쓰기 - 메시지 영역 debounce timeout 500 -> 100ms로 조정 2023-12-05 14:19:16 +09:00
klaus fb9c138eb4 라이브 방 룰렛 - 컬러 변경 2023-12-04 22:25:16 +09:00
klaus 7da0e2509c 라이브 방 룰렛 설정 - 당첨 내역 제거 2023-12-04 20:57:52 +09:00
klaus 63c2d607cc 라이브 방 룰렛 - 룰렛판 추가 2023-12-04 15:17:05 +09:00
klaus 9f66cb91fc 라이브 방 룰렛 - 룰렛 돌리기 기능 추가 2023-12-02 05:11:55 +09:00
klaus 91db6caec9 라이브 방 룰렛
- 리스너용 룰렛 버튼 추가
- 룰렛 설정 시 룰렛 버튼 토글 메시지를 발송하여 리스너 화면에서 룰렛이 (비)활성화 되도록 수정
2023-12-02 00:23:36 +09:00
klaus 4a4940f04d 라이브 방 룰렛 설정 - 미리보기 다이얼로그 추가 2023-12-01 16:49:07 +09:00
klaus 952df91619 라이브 방 - 룰렛 설정 추가 2023-12-01 03:37:23 +09:00
klaus b359ca58ba 라이브 방 - 배경 이미지 로딩 시 placeholder 제거 2023-11-24 21:49:01 +09:00
klaus fe121ee89b 라이브 프로필 다이얼로그
- 크리에이터가 아닌 사람 태그 영역 숨김
2023-11-23 23:44:51 +09:00
klaus 9e3859e2c2 콘텐츠 전체 보기
- 콘텐츠 상세로 이동 후 복귀 해도 콘텐츠 삭제/수정을 하지 않았을 때 스크롤이 상단으로 이동하지 않도록 수정
2023-11-23 00:19:50 +09:00
klaus f4ca63af9d 라이브 유저 프로필 다이얼로그
- 크리에이터가 아닌 일반 유저는 태그 영역과 소개 영역이 보이지 않도록 제거
2023-11-22 23:51:26 +09:00
klaus 61ee1fc5be version
- code : 11
- name : 1.1.1
2023-11-22 01:38:27 +09:00
klaus ead7aa3011 라이브 - 방장이 아닌 사용자가 유저 차단 시 라이브에서 강퇴되는 버그 수정 2023-11-21 14:19:40 +09:00
klaus 720df22df5 무료 충전 제거 2023-11-21 00:23:40 +09:00
klaus 74c56c2061 foreground service type 추가 - mediaPlayback 2023-11-20 22:44:18 +09:00
klaus e391fcbb6e 프로필 수정 페이지 - sns, 소개, 태그 수정 UI 크리에이터만 보이도록 수정 2023-11-20 17:56:52 +09:00
klaus 87e1156b02 콘텐츠 후원 배경색 - 기존 캔 수 / 10 으로 조정 2023-11-20 15:41:16 +09:00
klaus a77708dfbf 라이브 후원 배경색 - 기존 캔 수 / 10 으로 조정 2023-11-20 15:21:39 +09:00
klaus 664f34ed5b 라이브 프로필 다이얼로그
- 메시지 보내기 버튼 제거
2023-11-20 11:40:57 +09:00
klaus 48d1db3b79 라이브 상세 - 날짜 포맷 yyyy년 MM월 dd일 (E) a hh시 mm분 로 수정 2023-11-14 12:37:28 +09:00
klaus 7e32727773 version 10(1.1.0) 2023-11-05 00:47:11 +09:00
klaus 1a3396b293 콘텐츠 구매 - 캔이 부족하면 캔 충전 페이지로 이동하도록 수정 2023-11-03 20:37:34 +09:00
klaus d883a81602 콘텐츠 메인 - 인기 콘텐츠 조회 page 0->1 변경 2023-11-03 18:23:47 +09:00
klaus 81bdd52edd 인기 콘텐츠 전체 보기 - 정렬 추가 2023-11-02 21:05:39 +09:00
klaus 63483a2099 콘텐츠 메인 - 인기 콘텐츠 정렬 추가 2023-11-02 17:39:50 +09:00
klaus b68b4eb8da version 9 (1.0.8) 2023-10-31 22:13:02 +09:00
klaus d8cc218139 구매 목록 - 구매한 콘텐츠 총 개수 표시 2023-10-31 18:35:07 +09:00
klaus 0226a696d4 라이브 - 방장을 제외한 모든 유저에게 참여자 목록 버튼이 보이지 않도록 수정 2023-10-31 13:56:50 +09:00
klaus c9eb0f0e01 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-31 12:23:43 +09:00
klaus 5c293e79cf 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-31 12:23:12 +09:00
klaus 5cdb7426c6 구매 목록 - 페이징을 추가해 이전 구매목록이 추가로 로딩되도록 수정 2023-10-30 22:33:36 +09:00
klaus cc73f471d2 카울리 - 무료 충전 버튼 크기 수정, 캔 내역에 무료 충전 버튼 추가 2023-10-27 22:22:58 +09:00
klaus dc3240f224 카울리 무료 충전 버튼 보이게 수정 2023-10-27 02:06:21 +09:00
klaus 04eda1ffbd 결제수단 - 휴대폰 결제 추가 2023-10-27 02:02:24 +09:00
klaus 39d790c1c3 사용하지 않는 isAdult 제거 2023-10-24 23:22:21 +09:00
klaus 3e3d0de7ca 카울리 PointClick SDK verion 1.0.17로 변경 2023-10-23 16:58:25 +09:00
klaus 165b75487b 콘텐츠 등록 - 유료 콘텐츠의 경우에만 미리듣기 시간설정을 할 수 있도록 수정 2023-10-22 17:59:50 +09:00
klaus 19b351ef2a 콘텐츠 등록 - 대여만 가능한 콘텐츠 등록 기능 추가 2023-10-21 00:56:15 +09:00
klaus 83575aa1eb 콘텐츠 상세 - 대여만 가능한 콘텐츠의 경우 소장 버튼이 보이지 않고 가격의 100%가 보이도록 수정 2023-10-20 23:13:16 +09:00
klaus 26c9a236ec 결제수단 - 휴대폰 결제 숨김 2023-10-20 17:20:03 +09:00
klaus da7f72544f 결제수단 - 휴대폰 결제 추가 2023-10-20 16:24:49 +09:00
klaus 444f031f57 콘텐츠 메인 인기 콘텐츠 - 아이템 width 고정 2023-10-15 07:17:09 +09:00
klaus 3e7d06a2aa 탐색 - 인기 크리에이터 설명 글 UI 수정 2023-10-15 06:53:39 +09:00
klaus e6b8e55966 인기 콘텐츠 전체 보기 페이지 추가 2023-10-15 04:38:07 +09:00
klaus fe1a1cc3cb 콘텐츠 메인 - 인기 콘텐츠 영역 추가 2023-10-15 03:09:47 +09:00
klaus 2f17e04e1e 탐색 - 크리에이터 랭킹 UI 추가 2023-10-14 18:48:02 +09:00
klaus 41d175a19f admob 제거 2023-10-14 17:08:47 +09:00
klaus 8266167c02 탐색 - 섹션 제목 아래에 description 추가 2023-10-14 01:38:00 +09:00
klaus 2cfc4b97f4 채금 다이얼로그 - 취소 버튼 동작 추가 2023-10-11 16:55:19 +09:00
klaus fbad5f9d98 채금 기능 추가 2023-10-11 02:49:52 +09:00
klaus ac6b0c52d0 라이브 방 - 팔로잉 버튼 위치 수정 2023-10-06 20:05:26 +09:00
klaus 413c526a6a 라이브 지금 예약 중 아이템 - 이미지 RoundedCorners 추가 2023-10-06 19:19:14 +09:00
klaus 622021913d 메시지 발송 버튼 색상 변경 2023-10-06 17:45:47 +09:00
klaus 5ed5a86e0d 라이브 예약중 전체보기 - 캘린더 선택된 날짜 배경색 변경 2023-10-06 17:34:47 +09:00
klaus 3bf4f273d2 라이브 지금 예약중 - 라이브 커버 이미지 사이즈가 작게 보이던 버그 수정 2023-10-06 17:31:31 +09:00
klaus 0e6c78a6c0 유료 콘텐츠 미리 듣기 재생 버튼 추가 2023-10-05 23:24:10 +09:00
klaus 71cd52d30a 후원랭킹 전체보기 후원랭킹 활성화 스위치 - 클릭 리스너 추가 2023-10-05 22:31:15 +09:00
klaus d35b920470 후원랭킹 전체보기 - 후원랭킹 활성화 스위치 추가 2023-10-05 19:13:12 +09:00
klaus 5a4355044f 지금 라이브 중 전체 보기 아이템 - 배경 색상 제거 2023-09-27 16:27:42 +09:00
klaus b74d4b18e7 콘텐츠 큐레이션 전체보기 - UI 형태 그리드로 변경 2023-09-27 15:58:30 +09:00
klaus a53b76415b 콘텐츠 큐레이션 전체보기 - UI 형태 그리드로 변경 2023-09-27 15:49:00 +09:00
klaus a286ee760d 콘텐츠 업로드 - 미리듣기 시간 설정 안내 문구 글자 간격 수정 2023-09-27 15:48:02 +09:00
klaus 92b72db25c 콘텐츠 업로드 - 미리듣기 시간 설정 안내 문구 추가 2023-09-27 15:42:59 +09:00
klaus eed7bfa158 예약 라이브 전체 보기 - 라이브 만들기 페이지로 이동하는 기능 제거 2023-09-27 14:59:38 +09:00
klaus 549644a224 콘텐츠 큐레이션 - 너비가 가득 차도록 수정 2023-09-27 14:51:04 +09:00
klaus ecec8be386 새로운 콘텐츠 전체보기 페이지 추가 2023-09-27 14:19:54 +09:00
klaus 46b423e3e6 큐레이션 콘텐츠 전체보기 페이지 추가 2023-09-26 22:04:41 +09:00
klaus 1206977907 콘텐츠 사이 배너광고 제거 2023-09-26 16:07:03 +09:00
klaus b38fd26b77 무료 충전 아이콘 숨김 2023-09-22 22:32:30 +09:00
klaus 302e7d9a45 콘텐츠 업로드 - 미리 듣기 시간 설정 기능 추가 2023-09-22 18:08:31 +09:00
klaus b7a986c33c checkReleaseBuilds 추가 2023-09-21 22:44:21 +09:00
klaus 6fc474cff4 탐색 탭 - 배너 광고 unit id 변경 2023-09-20 18:52:28 +09:00
klaus 4bcc1b2680 point click sdk 추가 2023-09-19 22:42:11 +09:00
klaus 318bae54a1 versionCode 5, versionName 1.0.4 2023-09-19 15:38:36 +09:00
klaus 959d20fe6f 콘텐츠 상세 - 배너 광고 간격 수정 2023-09-16 01:40:36 +09:00
klaus 00277117f1 콘텐츠 상세 - 배너 광고 위치 수정 2023-09-16 01:31:08 +09:00
klaus 42613dfc76 탐색 - 광고 배너 추가 2023-09-16 01:12:31 +09:00
klaus 90df714a44 라이브 메인 - 광고 위치 추천 채널 밑으로 이동 2023-09-15 23:21:36 +09:00
klaus 62fc0e1d59 콘텐츠 메인 - 광고 위치 수정 2023-09-15 23:17:28 +09:00
klaus e3679fd1dc 라이브 방 - 배너 광고 제거 2023-09-15 23:06:35 +09:00
klaus 9626823f0c 라이브 방 - 배너 광고 추가 2023-09-15 21:35:25 +09:00
klaus 9fc795afac binding 버그 수정 2023-09-15 16:33:38 +09:00
klaus 6610f13619 라이브 상세 - 배너 광고 추가 2023-09-15 02:29:53 +09:00
klaus f9401d91c4 메시지 - 배너 광고 추가 2023-09-15 02:22:15 +09:00
klaus 52e6965472 크리에이터 채널 - 배너 광고 추가 2023-09-15 02:06:27 +09:00
klaus 0343c91f1c 라이브 메인, 팔로잉 채널 전체보기, 지금 라이브 중 전체보기 - 배너 광고 추가 2023-09-15 01:54:02 +09:00
klaus cce1b4f446 콘텐츠 메인 - 배너 광고 간격 수정 2023-09-15 01:35:00 +09:00
klaus db1981b5fe 콘텐츠 구매목록 - 배너 광고 추가 2023-09-15 01:32:21 +09:00
klaus cae15b7f39 콘텐츠 메인 - 배너 광고 추가 2023-09-15 01:22:51 +09:00
klaus 26e43bd548 콘텐츠 상세 - 배너 광고 추가 2023-09-14 03:14:48 +09:00
klaus f6cbaffd3b 휴대폰 결제 임시로 숨김 2023-09-13 14:43:05 +09:00
klaus 4d4ddb50ac 메시지 추가 로딩 되지 않는 버그 수정 2023-09-13 14:31:06 +09:00
klaus 9ed175191b 재생수 업데이트 로직 - 10초 이상 연속재생 한 경우 업데이트 하도록 수정 2023-09-13 12:25:41 +09:00
klaus 4d5c3acff5 휴대폰 결제 추가 2023-09-12 01:31:02 +09:00
2385 changed files with 209971 additions and 10489 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ indent_size = 4
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
max_line_length = 130
tab_width = 4
[*.{kt,kts}]
+18 -3
View File
@@ -44,6 +44,8 @@ captures/
# IntelliJ
*.iml
.idea/deviceManager.xml
.idea/androidTestResultsUserPreferences.xml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
@@ -57,10 +59,16 @@ captures/
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
.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.
#*.jks
#*.keystore
*.jks
*.keystore
*.p12
*.pem
*.key
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
@@ -140,6 +148,7 @@ output.json
hs_err_pid*
### Kotlin ###
.kotlin/
# Compiled class file
# Log file
@@ -306,4 +315,10 @@ fabric.properties
app/debug/
app/release/
.junie/
.kiro/
.omo/
.antigravitycli/
.worktrees/
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java
+158
View File
@@ -0,0 +1,158 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JunieProject"><![CDATA[{
"guidelinesPath": "AGENTS.md"
}]]></component>
</project>
+8
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>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
+144
View File
@@ -0,0 +1,144 @@
# AGENTS.md
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
## 커뮤니케이션 규칙
- **"질문에 대한 답변과 설명과 Todo는 한국어로 한다."**
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
## CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)
These principles override plugin behavior, skill behavior, workflow behavior, and default model behavior unless the user's direct instruction explicitly says otherwise.
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
## 실행 원칙 및 계층 사용 정책
- 불확실하거나 모호한 경우 추측하지 말고 확인하거나, 가능한 최소 범위의 안전한 조치를 취한다.
### 기본 모드: 보수적 실행
- 최소 변경
- 단순한 구현
- 검증 가능한 결과
### 확장 모드
- 사용자가 명시적으로 요청한 경우에만 사용한다.
- 대규모 리팩토링, 브레인스토밍, 다중 에이전트 실행, 병렬 workflow를 허용한다.
### 에이전트 동작 원칙
- 추측하지 말고 근거 파일을 읽고 결정한다.
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
## 저장소 범위
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
- 모든 명령은 저장소 루트에서 실행한다.
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
- 기존 로직 수정이 아닌 신규 `Activity`, `Fragment`, `ViewModel` 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
## 레거시 코드 사용 원칙
- 레거시 코드는 직접 수정하지 않는다.
- 레거시 기능이 필요하면 기존 코드를 호출해서 사용한다.
- 레거시 코드를 약간 변경해야 사용할 수 있는 경우에도 레거시 파일을 고치지 말고, 신규 파일에 wrapper/adapter/확장 코드를 추가해 사용한다.
- 레거시 코드 변경이 불가피해 보이면 구현 전에 사용자에게 확인한다.
## 작업 절차 핵심 규칙
- 모든 구현 작업은 PRD 문서와 구현 계획/TASK 문서가 모두 준비된 뒤에 시작한다.
- 사용자의 프롬프트를 받으면 먼저 PRD 문서를 작성한다.
- PRD 작성 중 애매하거나 더 필요한 내용, 결정해야 하는 사항이 있으면 애매한 사항이 없어질 때까지 사용자와 인터뷰한다.
- 인터뷰 내용을 PRD에 반영한 뒤, PRD를 기준으로 계획/TASK 문서를 작성하고 그 문서에 따라 필요한 내용만 최소 구현한다.
- 문서는 `docs/[날짜]_구현할내용한글/` 아래에 `prd.md`, `plan-task.md`로 만든다.
- `docs/[날짜]_구현할내용한글/prd.md`
- `docs/[날짜]_구현할내용한글/plan-task.md`
- 문서 폴더명에서 원래 띄어쓰기가 들어갈 위치는 공백 대신 `_`를 사용한다.
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
- 문서 샘플의 기준 위치는 `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`를 참고한다.
- 코드 스타일/구조는 `docs/agent-guides/code-style.md`를 참고한다.
- 작업 절차/docs/계획 문서 규칙은 `docs/agent-guides/work-plan-docs.md`를 참고한다.
- 커밋 메시지 규칙은 `docs/agent-guides/commit-message-rules.md`를 참고한다.
- 저장소 세부 규칙/보안/Git 안전 수칙은 `docs/agent-guides/safety-repo-rules.md`를 참고한다.
## 핵심 금지사항
- `local.properties`, 키스토어, Google Services 파일, 비밀값은 커밋하거나 외부에 노출하지 않는다.
- `BuildConfig` 값(키/토큰/URL)을 로그, Toast, 크래시 메시지에 직접 노출하지 않는다.
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
+279 -42
View File
@@ -1,31 +1,51 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Locale
import java.util.Properties
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
id 'com.google.android.gms.oss-licenses-plugin'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox'
id 'com.google.firebase.crashlytics'
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { stream ->
localProperties.load(stream)
}
}
def releaseSigningPropertyKeys = [
'RELEASE_STORE_FILE',
'RELEASE_STORE_PASSWORD',
'RELEASE_KEY_ALIAS',
'RELEASE_KEY_PASSWORD'
]
def releaseSigningProperty = { key ->
localProperties.getProperty(key)?.trim()
}
def missingReleaseSigningProperties = {
releaseSigningPropertyKeys.findAll { key -> !releaseSigningProperty(key) }
}
android {
namespace 'kr.co.vividnext.sodalive'
compileSdk 33
compileSdk = 35
viewBinding {
enabled true
}
buildFeatures {
dataBinding true
}
lintOptions {
checkDependencies true
buildConfig true
}
dependenciesInfo {
@@ -35,56 +55,201 @@ android {
includeInBundle = false
}
packaging {
// JNI(.so) 관련
jniLibs {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += ["**/libaosl.so"]
}
// 일반 리소스(META-INF 등) 관련
resources {
// pickFirsts: 충돌 시 첫 파일만 채택
pickFirsts += [
"META-INF/LICENSE.txt",
"META-INF/NOTICE*"
]
// 자주 쓰는 제외/병합 예시
excludes += [
"META-INF/DEPENDENCIES",
"META-INF/AL2.0",
"META-INF/LGPL2.1"
]
merges += [
"META-INF/services/**"
]
}
}
defaultConfig {
applicationId "kr.co.vividnext.sodalive"
minSdk 23
targetSdk 33
versionCode 3
versionName "1.0.2"
targetSdk 35
versionCode 250
versionName "1.56.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
def releaseStoreFile = releaseSigningProperty('RELEASE_STORE_FILE')
def releaseStorePassword = releaseSigningProperty('RELEASE_STORE_PASSWORD')
def releaseKeyAlias = releaseSigningProperty('RELEASE_KEY_ALIAS')
def releaseKeyPassword = releaseSigningProperty('RELEASE_KEY_PASSWORD')
if (missingReleaseSigningProperties().isEmpty()) {
storeFile rootProject.file(releaseStoreFile)
storePassword releaseStorePassword
keyAlias releaseKeyAlias
keyPassword releaseKeyPassword
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// 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_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"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140295-10"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140295-11"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140295-12"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140295-13"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140295-14"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140295-15"'
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140295-16"'
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"664c1707b18b225deca4b429"'
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"15cadeea4ba94ff7b091c9a10f4bf4a6"'
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"765102ec85855aa680da35f1b0f55712"'
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
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",
FACEBOOK_APP_ID : "612448298237287",
FACEBOOK_CLIENT_TOKEN: "32af760f4a7b7cb7e3b1e7ffd0b0da70",
KAKAO_APP_KEY : "231cf78acfa8252fca38b9eedf87c5cb"
]
}
debug {
minifyEnabled false
debuggable true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix '.debug'
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_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"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140297-10"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140297-11"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140297-12"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140297-13"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140297-14"'
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140297-15"'
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140297-16"'
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"667fca5d3bab7404f831c3e4"'
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"'
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"5f7ebe90d1ce5f0392164b8a53a662bc"'
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
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",
FACEBOOK_APP_ID : "608674328645232",
FACEBOOK_CLIENT_TOKEN: "3775e6ea83236a685d264b6c5a1bbb4d",
KAKAO_APP_KEY : "20cf19413d63bfdfd30e8e6dff933d33"
]
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
lint {
checkDependencies true
checkReleaseBuilds false
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
gradle.taskGraph.whenReady { taskGraph ->
def requiresReleaseSigning = taskGraph.allTasks.any { task ->
def taskName = task.name.toLowerCase(Locale.US)
taskName.contains('release') && (
taskName.startsWith('assemble') ||
taskName.startsWith('bundle') ||
taskName.startsWith('install') ||
taskName.startsWith('package')
)
}
if (!requiresReleaseSigning) {
return
}
def missingProperties = missingReleaseSigningProperties()
if (!missingProperties.isEmpty()) {
throw new GradleException(
"Release signing properties are missing in local.properties: ${missingProperties.join(', ')}"
)
}
def releaseStoreFile = rootProject.file(releaseSigningProperty('RELEASE_STORE_FILE'))
if (!releaseStoreFile.exists()) {
throw new GradleException(
"Release signing store file does not exist: ${releaseStoreFile}"
)
}
}
dependencies {
implementation "androidx.media:media:1.6.0"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.media:media:1.7.1"
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.webkit:webkit:1.7.0'
implementation 'androidx.webkit:webkit:1.14.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4'
// Logger
implementation("com.orhanobut:logger:2.2.0") {
@@ -92,7 +257,7 @@ dependencies {
}
// image library
implementation "io.coil-kt:coil:1.4.0"
implementation "io.coil-kt:coil:2.7.0"
// Koin DI
implementation "io.insert-koin:koin-android:3.1.3"
@@ -102,50 +267,122 @@ dependencies {
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
}
implementation "androidx.datastore:datastore-preferences:1.2.1"
// Gson
implementation "com.google.code.gson:gson:2.10.1"
implementation "com.google.code.gson:gson:2.13.2"
// Network
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
implementation "com.squareup.retrofit2:retrofit:3.0.0"
implementation "com.squareup.retrofit2:converter-gson:3.0.0"
implementation "com.squareup.retrofit2:adapter-rxjava3:3.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:5.2.1"
// RxJava3
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// permission
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
implementation "io.github.ParkSangGwon:tedpermission-normal:3.4.2"
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.github.yalantis:ucrop:2.2.11'
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.2.2')
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
implementation platform('com.google.firebase:firebase-bom:33.16.0')
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'androidx.credentials:credentials:1.3.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
// bootpay
implementation "io.github.bootpay:android:4.3.4"
implementation "io.github.bootpay:android:4.4.3"
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
implementation "io.agora.rtc:voice-sdk:4.5.2"
implementation 'io.agora:agora-rtm:2.2.6'
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.github.bumptech.glide:glide:5.0.5'
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
implementation "com.michalsvec:single-row-calednar:1.0.0"
// google in-app-purchase
implementation "com.android.billingclient:billing-ktx:8.0.0"
// ROOM
ksp "androidx.room:room-compiler:2.8.3"
implementation "androidx.room:room-ktx:2.8.3"
implementation "androidx.room:room-runtime:2.8.3"
implementation "androidx.room:room-rxjava3:2.8.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "androidx.media3:media3-session:1.8.0"
implementation "androidx.media3:media3-exoplayer:1.8.0"
// Facebook
implementation "com.facebook.android:facebook-core:18.0.0"
// Appsflyer
implementation 'com.appsflyer:af-android-sdk:6.17.4'
// Yandex Mobile Ads
implementation 'com.yandex.android:mobileads:7.18.5'
// 노티플라이
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
// Kakao
implementation "com.kakao.sdk:v2-common:2.21.0"
implementation "com.kakao.sdk:v2-auth:2.21.0"
implementation "com.kakao.sdk:v2-user:2.21.0"
implementation 'io.github.glailton.expandabletextview:expandabletextview:1.0.4'
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'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
testImplementation 'io.mockk:mockk:1.14.6'
testImplementation 'androidx.test:core-ktx:1.6.1'
testImplementation 'org.robolectric:robolectric:4.15.1'
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
}
// KSP args for Room schema export
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}
// Kotlin compiler and toolchain configuration (migrated from deprecated kotlinOptions.jvmTarget)
kotlin {
// Ensures Kotlin compiles with Java 17 toolchain
jvmToolchain(17)
// New DSL replacing kotlinOptions.jvmTarget
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
-67
View File
@@ -1,67 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2209417227252155460",
"lastPropertyId": "8:7803281435927194929",
"name": "PlaybackTracking",
"properties": [
{
"id": "1:3889922602505997244",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:874896374244616380",
"name": "contentId",
"type": 6
},
{
"id": "3:305496269372931228",
"name": "totalDuration",
"type": 5
},
{
"id": "4:1202262957765031780",
"name": "startPosition",
"type": 5
},
{
"id": "5:1595250877919247629",
"name": "isFree",
"type": 1
},
{
"id": "6:4066577743967565922",
"name": "isPreview",
"type": 1
},
{
"id": "7:7482414752180672089",
"name": "endPosition",
"type": 5
},
{
"id": "8:7803281435927194929",
"name": "playDateTime",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "1:2209417227252155460",
"lastIndexId": "0:0",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
+24
View File
@@ -221,3 +221,27 @@
-keep class androidx.viewpager2.widget.**{*;}
-keep class kr.co.bootpay.core.** { *; }
-keep class retrofit2.** { *; }
-keep class com.google.gson.** { *; }
-keep class sun.misc.** { *; }
# @Keep 애노테이션이 붙은 클래스, 메서드, 필드를 보호
-keep @androidx.annotation.Keep class * { *; }
-keep class com.kakao.sdk.**.model.* { <fields>; }
-keep class * extends com.google.gson.TypeAdapter
# https://github.com/square/okhttp/pull/6792
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.conscrypt.*
-dontwarn org.openjsse.**
-keep interface kr.co.vividnext.sodalive.tracking.UserEventApi
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }
-dontwarn com.linecorp.linesdk.BR
+1
View File
@@ -0,0 +1 @@
# Keep schemas directory under version control
@@ -0,0 +1,76 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "b9a331035b36b70f8ca7a14962b13fdf",
"entities": [
{
"tableName": "playback_tracking",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contentId` INTEGER NOT NULL, `totalDuration` INTEGER NOT NULL, `startPosition` INTEGER NOT NULL, `isFree` INTEGER NOT NULL, `isPreview` INTEGER NOT NULL, `endPosition` INTEGER, `playDateTime` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalDuration",
"columnName": "totalDuration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startPosition",
"columnName": "startPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isFree",
"columnName": "isFree",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPreview",
"columnName": "isPreview",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endPosition",
"columnName": "endPosition",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "playDateTime",
"columnName": "playDateTime",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a331035b36b70f8ca7a14962b13fdf')"
]
}
}
@@ -0,0 +1,82 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "7429c2998f64cb70e5e8b1d2525a4708",
"entities": [
{
"tableName": "alarms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL, `days` TEXT NOT NULL, `contentId` INTEGER NOT NULL, `contentTitle` TEXT NOT NULL, `contentCreatorNickname` TEXT NOT NULL, `volume` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "days",
"columnName": "days",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentTitle",
"columnName": "contentTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentCreatorNickname",
"columnName": "contentCreatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "volume",
"columnName": "volume",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isEnabled",
"columnName": "isEnabled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7429c2998f64cb70e5e8b1d2525a4708')"
]
}
}
@@ -0,0 +1,58 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e46a8b457c3ea6ceefd0db76bb763056",
"entities": [
{
"tableName": "recent_contents",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contentId` INTEGER NOT NULL, `coverImageUrl` TEXT NOT NULL, `title` TEXT NOT NULL, `creatorNickname` TEXT NOT NULL, `listenedAt` INTEGER NOT NULL, PRIMARY KEY(`contentId`))",
"fields": [
{
"fieldPath": "contentId",
"columnName": "contentId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "coverImageUrl",
"columnName": "coverImageUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "creatorNickname",
"columnName": "creatorNickname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "listenedAt",
"columnName": "listenedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"contentId"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e46a8b457c3ea6ceefd0db76bb763056')"
]
}
}
@@ -0,0 +1,88 @@
package kr.co.vividnext.sodalive.runtime
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
import kr.co.vividnext.sodalive.common.AppPreferencesDataStoreProvider
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlinx.coroutines.runBlocking
@RunWith(AndroidJUnit4::class)
class DataStoreRuntimeRegressionTest {
private val context: Context
get() = ApplicationProvider.getApplicationContext()
@Before
fun setUp() {
SharedPreferenceManager.init(context)
ChatRoomPreferenceManager.init(context)
}
@After
fun tearDown() {
SharedPreferenceManager.resetForTest()
ChatRoomPreferenceManager.resetForTest()
}
@Test
fun sharedPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
runBlocking {
AppPreferencesDataStoreProvider.get(context).edit { preferences ->
preferences[stringPreferencesKey(PREF_APP_LANGUAGE_CODE)] = "en"
preferences[booleanPreferencesKey(Constants.PREF_IS_PLAYER_SERVICE_RUNNING)] = true
}
}
SharedPreferenceManager.resetForTest()
SharedPreferenceManager.init(context)
assertEquals("en", SharedPreferenceManager.appLanguageCode)
assertTrue(SharedPreferenceManager.isPlayerServiceRunning)
}
@Test
fun chatRoomPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
val roomId = 13579L
val visibleKey = "chat_bg_visible_room_$roomId"
val imageIdKey = "chat_bg_image_id_room_$roomId"
runBlocking {
chatRoomDataStore().edit { preferences ->
preferences[booleanPreferencesKey(visibleKey)] = false
preferences[longPreferencesKey(imageIdKey)] = 777L
}
}
ChatRoomPreferenceManager.resetForTest()
ChatRoomPreferenceManager.init(context)
assertEquals(false, ChatRoomPreferenceManager.getBoolean(visibleKey, true))
assertEquals(777L, ChatRoomPreferenceManager.getLong(imageIdKey, -1L))
}
@Suppress("UNCHECKED_CAST")
private fun chatRoomDataStore(): DataStore<Preferences> {
val field = ChatRoomPreferenceManager::class.java.getDeclaredField("dataStore")
field.isAccessible = true
return field.get(ChatRoomPreferenceManager) as DataStore<Preferences>
}
companion object {
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
}
}
@@ -0,0 +1,69 @@
package kr.co.vividnext.sodalive.runtime
import androidx.media3.session.MediaController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
import kr.co.vividnext.sodalive.main.MainActivity
import org.junit.Assert.assertSame
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MiniPlayerConnectionGuardTest {
@Test
fun mainActivity_skipsConnectingWhenFutureAlreadyExists() {
val activity = createOnMainThread { MainActivity() }
val sentinel = SettableFuture.create<MediaController>()
setPrivateField(activity, "mediaControllerFuture", sentinel)
invokePrivateNoArg(activity, "connectPlayerService")
@Suppress("UNCHECKED_CAST")
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
assertSame(sentinel, after)
}
@Test
fun playlistDetailActivity_skipsConnectingWhenFutureAlreadyExists() {
val activity = createOnMainThread { AudioContentPlaylistDetailActivity() }
val sentinel = SettableFuture.create<MediaController>()
setPrivateField(activity, "mediaControllerFuture", sentinel)
invokePrivateNoArg(activity, "connectPlayerService")
@Suppress("UNCHECKED_CAST")
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
assertSame(sentinel, after)
}
private fun setPrivateField(target: Any, fieldName: String, value: Any?) {
val field = target.javaClass.getDeclaredField(fieldName)
field.isAccessible = true
field.set(target, value)
}
private fun getPrivateField(target: Any, fieldName: String): Any? {
val field = target.javaClass.getDeclaredField(fieldName)
field.isAccessible = true
return field.get(target)
}
private fun invokePrivateNoArg(target: Any, methodName: String) {
val method = target.javaClass.getDeclaredMethod(methodName)
method.isAccessible = true
method.invoke(target)
}
private fun <T> createOnMainThread(factory: () -> T): T {
var instance: Any? = null
InstrumentationRegistry.getInstrumentation().runOnMainSync {
instance = factory()
}
@Suppress("UNCHECKED_CAST")
return instance as T
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">SodaLive-Test</string>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ソーダライブ-テスト</string>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">소다라이브-테스트</string>
</resources>
+226 -22
View File
@@ -2,17 +2,28 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="com.facebook.katana" />
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Android 15+ 녹화 상태 콜백(addScreenRecordingCallback)에 필요한 권한 -->
<uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
@@ -28,11 +39,25 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:name=".app.SodaLiveApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/appsflyer_data_extraction_rules"
android:fullBackupContent="@xml/appsflyer_backup_rules"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -41,7 +66,41 @@
android:supportsRtl="true"
android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:targetApi="31">
<activity
android:name=".main.DeepLinkActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${APPLINK_HOST}"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${URISCHEME}" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 -->
<data
android:host="payverse"
android:path="/result"
android:scheme="${URISCHEME}" />
</intent-filter>
</activity>
<activity
android:name=".splash.SplashActivity"
android:exported="true">
@@ -50,26 +109,45 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="sodalive.page.link" />
<data android:host="sodalive.net" />
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
<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"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".user.signup.SignUpActivity" />
<activity android:name=".audio_content.all.AudioContentAllActivity" />
<activity android:name=".settings.language.LanguageSettingsActivity" />
<activity
android:name=".user.signup.SignUpActivity"
android:windowSoftInputMode="stateVisible" />
<activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".mypage.point.PointStatusActivity" />
<activity
android:name=".mypage.can.charge.CanChargeActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity
android:name=".mypage.can.payment.CanPaymentActivity"
android:launchMode="singleTop" />
<activity android:name=".mypage.can.payment.CanPaymentTempActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
@@ -78,9 +156,12 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />
<activity android:name=".explorer.profile.creator_community.write.CreatorCommunityWriteActivity" />
<activity android:name=".explorer.profile.creator_community.modify.CreatorCommunityModifyActivity" />
<activity android:name=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
@@ -91,6 +172,8 @@
<activity android:name=".settings.event.EventActivity" />
<activity android:name=".settings.event.EventDetailActivity" />
<activity android:name=".settings.notification.NotificationSettingsActivity" />
<activity android:name=".settings.notification.NotificationReceiveSettingsActivity" />
<activity android:name=".settings.ContentSettingsActivity" />
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
<activity android:name=".audio_content.AudioContentActivity" />
@@ -102,10 +185,46 @@
<activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity android:name=".onboarding.OnBoardingActivity" />
<activity android:name=".message.MessageActivity" />
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
<activity android:name=".home.pushnotification.PushNotificationListActivity" />
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
<activity android:name=".live.room.menu.MenuConfigActivity" />
<activity android:name=".audio_content.series.SeriesListAllActivity" />
<activity android:name=".audio_content.series.detail.SeriesDetailActivity" />
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
<activity android:name=".audio_content.playlist.detail.AudioContentPlaylistDetailActivity" />
<activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" />
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
<activity android:name=".audio_content.box.AudioContentBoxActivity" />
<activity android:name=".audition.detail.AuditionDetailActivity" />
<activity android:name=".audition.role.AuditionRoleDetailActivity" />
<activity android:name=".search.SearchActivity" />
<activity android:name=".audition.AuditionActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" />
<activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" />
<activity android:name=".mypage.block.BlockMemberActivity" />
<activity
android:name=".mypage.alarm.AlarmActivity"
android:exported="true"
android:showWhenLocked="true"
android:turnScreenOn="true">
<intent-filter>
<action android:name="com.example.alarmapp.ALARM_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".chat.character.newcharacters.NewCharactersAllActivity" />
<activity android:name=".chat.original.detail.OriginalWorkDetailActivity" />
<activity android:name=".audio_content.series.main.SeriesMainActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@@ -114,11 +233,40 @@
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.AppCompat.DayNight" />
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" -->
<data
android:host="oauth"
android:scheme="kakao${KAKAO_APP_KEY}" />
</intent-filter>
</activity>
<service
android:name=".common.SodaLiveService"
android:foregroundServiceType="microphone|mediaPlayback"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<service
android:name=".audio_content.AudioContentPlayService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false" />
<service
android:name=".audio_content.player.AudioContentPlayerService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<!-- [START firebase_service] -->
<service
@@ -130,10 +278,66 @@
</service>
<!-- [END firebase_service] -->
<!-- 부팅 시 알람 재설정을 위한 리시버 -->
<receiver
android:name=".mypage.alarm.receiver.AlarmBootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".mypage.alarm.receiver.AlarmReceiver"
android:enabled="true"
android:exported="false" />
<!-- [START fcm_default_channel] -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<!-- [END fcm_default_channel] -->
<!-- [START facebook] -->
<meta-data
android:name="com.facebook.sdk.ApplicationId"
android:value="${FACEBOOK_APP_ID}" />
<meta-data
android:name="com.facebook.sdk.ClientToken"
android:value="${FACEBOOK_CLIENT_TOKEN}" />
<meta-data
android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled"
android:value="true" />
<activity
android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
<!-- [END facebook] -->
<!-- Character Detail -->
<activity android:name=".chat.character.detail.CharacterDetailActivity" />
<activity android:name=".chat.talk.room.ChatRoomActivity" />
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<!-- ★ 이 meta-data가 꼭 필요 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
+25
View File
@@ -0,0 +1,25 @@
<!-- app/src/main/assets/payverse_starter_debug.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<!-- PayVerse SDK -->
<script src="https://ui.payverseglobal.com/js/payments.js"></script>
</head>
<body>
<script>
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
function startPay(payloadJson) {
try {
const p = JSON.parse(payloadJson);
// 즉시 실행: 페이지가 열리자마자 결제창 시작
window.payVerse.requestUI(p);
} catch (e) {
console.error('startPay error', e);
alert('결제 초기화에 실패했습니다.');
}
}
</script>
</body>
</html>
@@ -0,0 +1,25 @@
<!-- app/src/main/assets/payverse_starter_debug.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<!-- PayVerse SDK -->
<script src="https://ui-snd.payverseglobal.com/js/payments.js"></script>
</head>
<body>
<script>
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
function startPay(payloadJson) {
try {
const p = JSON.parse(payloadJson);
// 즉시 실행: 페이지가 열리자마자 결제창 시작
window.payVerse.requestUI(p);
} catch (e) {
console.error('startPay error', e);
alert('결제 초기화에 실패했습니다.');
}
}
</script>
</body>
</html>
@@ -6,28 +6,29 @@ import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
import io.agora.rtm.PublishOptions
import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmChannel
import io.agora.rtm.RtmChannelListener
import io.agora.rtm.RtmClient
import io.agora.rtm.RtmClientListener
import io.agora.rtm.SendMessageOptions
import io.agora.rtm.RtmConfig
import io.agora.rtm.RtmConstants
import io.agora.rtm.RtmEventListener
import io.agora.rtm.SubscribeOptions
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread
class Agora(
private val uid: Long,
private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener
private val rtmEventListener: RtmEventListener
) {
// RTM client instance
private var rtmClient: RtmClient? = null
// 상태 플래그: RTM 로그인 완료 여부
private var rtmLoggedIn: Boolean = false
// RTM channel instance
private var rtmChannel: RtmChannel? = null
private var rtcEngine: RtcEngine? = null
// 상태 플래그: RTM 로그인 진행 중 여부
private var rtmLoginInProgress: Boolean = false
init {
initAgoraEngine()
@@ -35,65 +36,51 @@ class Agora(
private fun initAgoraEngine() {
try {
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
rtmClient = RtmClient.createInstance(
context,
BuildConfig.AGORA_APP_ID,
rtmClientListener
)
initRtcEngine()
initRtmClient()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun deInitAgoraEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
rtmChannel?.leave(null)
rtmChannel?.release()
rtmClient?.logout(null)
fun deInitAgoraEngine(rtmEventListener: RtmEventListener) {
deInitRtcEngine()
deInitRtmClient(rtmEventListener)
}
fun inputChat(message: String) {
val rtmMessage = rtmClient!!.createMessage()
rtmMessage.text = message
// region RtcEngine
private var rtcEngine: RtcEngine? = null
rtmChannel!!.sendMessage(
rtmMessage,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
}
}
@Throws(Exception::class)
private fun initRtcEngine() {
Logger.e("initRtcEngine")
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
Logger.e("initRtcEngine - rtcEngine: ${rtcEngine != null}")
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.setParameters("{\"che.audio.aiaec.working_mode\":0}")
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
}
fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
val state = rtcEngine?.connectionState
val isDisconnected = state == null || state == Constants.CONNECTION_STATE_DISCONNECTED
if (!isDisconnected) {
Logger.e("joinRtcChannel - skip (state=$state)")
return
}
Logger.e("joinRtcChannel - proceed (state=$state) uid=$uid channel=$channelName")
rtcEngine!!.joinChannel(
rtcToken,
channelName,
@@ -102,62 +89,6 @@ class Agora(
)
}
fun createRtmChannelAndLogin(
uid: String,
rtmToken: String,
channelName: String,
rtmChannelListener: RtmChannelListener,
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener)
rtmClient!!.login(
rtmToken,
uid,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
rtmChannel!!.join(object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmChannel join - onSuccess")
rtmChannelJoinSuccess()
}
override fun onFailure(p0: ErrorInfo?) {
rtmChannelJoinFail()
}
})
}
override fun onFailure(p0: ErrorInfo?) {
}
}
)
}
fun sendRawMessageToGroup(
rawMessage: ByteArray,
onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
val message = rtmClient!!.createMessage()
message.rawMessage = rawMessage
rtmChannel!!.sendMessage(
message,
object : ResultCallback<Void?> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorDescription}")
onFailure?.invoke()
}
}
)
}
fun setClientRole(role: Int) {
rtcEngine!!.setClientRole(role)
}
@@ -170,36 +101,307 @@ class Agora(
rtcEngine?.muteAllRemoteAudioStreams(mute)
}
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType,
onSuccess: () -> Unit
) {
val option = SendMessageOptions()
val message = rtmClient!!.createMessage()
message.rawMessage = requestType.toString().toByteArray()
rtmClient!!.sendMessageToPeer(
receiverUid,
message,
option,
object : ResultCallback<Void?> {
override fun onSuccess(aVoid: Void?) {
onSuccess()
}
override fun onFailure(errorInfo: ErrorInfo) {
}
}
)
}
fun rtmChannelIsNull(): Boolean {
return rtmChannel == null
}
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
fun isRtmLoggedIn(): Boolean {
return rtmLoggedIn
}
fun deInitRtcEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
}
// endregion
// region RtmClient
private var rtmClient: RtmClient? = null
private var roomChannelName: String? = null
@Throws(Exception::class)
private fun initRtmClient() {
val rtmConfig = RtmConfig.Builder(BuildConfig.AGORA_APP_ID, uid.toString())
.eventListener(rtmEventListener)
.build()
rtmClient = RtmClient.create(rtmConfig)
}
fun rtmLogin(
rtmToken: String,
channelName: String,
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
// 이미 RTM 로그인 및 구독이 완료된 경우 재호출 방지
if (rtmLoggedIn && roomChannelName == channelName) {
Logger.e("rtmLogin - already logged in and subscribed. skip")
return
}
// 로그인 시도 중이면 재호출 방지
if (rtmLoginInProgress) {
Logger.e("rtmLogin - already in progress. skip")
return
}
roomChannelName = channelName
fun attemptLogin(attempt: Int) {
rtmClient!!.login(
rtmToken,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("rtmClient login - success (attempt=$attempt)")
// 로그인 성공 후 두 채널 구독 시도
subscribeChannel(rtmChannelJoinSuccess, rtmChannelJoinFail)
}
override fun onFailure(p0: ErrorInfo?) {
Logger.e("rtmClient login - fail (attempt=$attempt), ${p0?.errorReason}")
if (attempt < 4) {
attemptLogin(attempt + 1)
} else {
rtmLoginInProgress = false
rtmChannelJoinFail()
}
}
}
)
}
rtmLoginInProgress = true
attemptLogin(1)
}
private fun subscribeChannel(
rtmChannelJoinSuccess: () -> Unit,
rtmChannelJoinFail: () -> Unit
) {
val targetRoom = roomChannelName
if (targetRoom == null) {
Logger.e("subscribeChannel - roomChannelName is null")
rtmChannelJoinFail()
return
}
var completed = false
var roomSubscribed = false
var inboxSubscribed = false
fun completeSuccessIfReady() {
if (!completed && roomSubscribed && inboxSubscribed) {
completed = true
rtmLoggedIn = true
rtmLoginInProgress = false
Logger.e("RTM subscribe - both channels subscribed")
rtmChannelJoinSuccess()
}
}
fun failOnce(reason: String?) {
if (!completed) {
completed = true
Logger.e("RTM subscribe failed: $reason")
rtmChannelJoinFail()
}
}
fun subscribeRoom(attempt: Int) {
val channelOptions = SubscribeOptions()
channelOptions.withMessage = true
channelOptions.withPresence = true
Logger.e("RTM subscribe(room: $targetRoom) attempt=$attempt")
rtmClient!!.subscribe(
targetRoom,
channelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(room) success at attempt=$attempt")
roomSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(room) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeRoom(attempt + 1)
} else {
failOnce("room subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
fun subscribeInbox(attempt: Int) {
val inboxChannel = "inbox_$uid"
val inboxChannelOptions = SubscribeOptions()
inboxChannelOptions.withMessage = true
Logger.e("RTM subscribe(inbox: $inboxChannel) attempt=$attempt")
rtmClient!!.subscribe(
inboxChannel,
inboxChannelOptions,
object : ResultCallback<Void> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM subscribe(inbox) success at attempt=$attempt")
inboxSubscribed = true
completeSuccessIfReady()
}
override fun onFailure(errorInfo: ErrorInfo?) {
Logger.e("RTM subscribe(inbox) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
if (attempt < 4) {
subscribeInbox(attempt + 1)
} else {
failOnce("inbox subscribe failed after 3 retries (4 attempts)")
}
}
}
)
}
// 두 채널 구독을 병렬로 시도
subscribeRoom(1)
subscribeInbox(1)
}
fun inputChat(message: String, onFailure: () -> Unit) {
if (roomChannelName != null) {
val options = PublishOptions()
options.setChannelType(RtmConstants.RtmChannelType.MESSAGE)
rtmClient!!.publish(
roomChannelName!!,
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
onFailure()
}
}
fun sendRawMessageToGroup(
rawMessage: ByteArray,
onSuccess: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
if (roomChannelName != null) {
val options = PublishOptions()
options.customType = "ByteArray"
rtmClient!!.publish(
roomChannelName!!,
rawMessage,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess?.invoke()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
onFailure?.invoke()
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
onFailure?.invoke()
}
}
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType? = null,
rawMessage: ByteArray? = null,
onSuccess: () -> Unit
) {
if (roomChannelName != null) {
val message = rawMessage ?: requestType.toString().toByteArray()
val options = PublishOptions()
options.customType = "ByteArray"
rtmClient!!.publish(
"inbox_$receiverUid",
message,
options,
object : ResultCallback<Void> {
override fun onSuccess(p0: Void?) {
Logger.e("sendMessage - onSuccess")
onSuccess()
}
override fun onFailure(p0: ErrorInfo) {
Logger.e("sendMessage fail - ${p0.errorCode}")
Logger.e("sendMessage fail - ${p0.errorReason}")
}
}
)
} else {
Logger.e("inputChat - roomChannelName is null")
}
}
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
}
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> {
override fun onSuccess(responseInfo: Void?) {
Logger.e("RTM unsubscribe - inbox_${SharedPreferenceManager.userId}")
}
override fun onFailure(errorInfo: ErrorInfo) {
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")
rtmClient = null
}
override fun onFailure(errorInfo: ErrorInfo) {
Logger.e("RTM logout fail - ${errorInfo.errorCode}")
Logger.e("RTM logout fail - ${errorInfo.errorReason}")
}
})
// 상태 리셋
rtmLoggedIn = false
rtmLoginInProgress = false
}
// endregion
}
@@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.agora.v2v
import io.reactivex.rxjava3.core.Single
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
interface V2vApi {
@POST("projects/{appId}/join")
fun join(
@Path("appId") appId: String,
@Header("Authorization") authorization: String,
@Header("X-Request-Id") requestId: String,
@Body request: V2vJoinRequest
): Single<V2vJoinResponse>
@POST("projects/{appId}/agents/{agentId}/leave")
fun leave(
@Path("appId") appId: String,
@Path("agentId") agentId: String,
@Header("Authorization") authorization: String,
@Header("X-Request-Id") requestId: String
): Single<V2vLeaveResponse>
}
@@ -0,0 +1,63 @@
package kr.co.vividnext.sodalive.agora.v2v
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class V2vJoinRequest(
@SerializedName("name") val name: String,
@SerializedName("preset") val preset: String,
@SerializedName("properties") val properties: V2vJoinProperties
)
@Keep
data class V2vJoinProperties(
@SerializedName("channel") val channel: String,
@SerializedName("token") val token: String,
@SerializedName("agent_rtc_uid") val agentRtcUid: String,
@SerializedName("remote_rtc_uids") val remoteRtcUids: List<String>,
@SerializedName("idle_timeout") val idleTimeout: Int,
@SerializedName("advanced_features") val advancedFeatures: V2vAdvancedFeatures,
@SerializedName("parameters") val parameters: V2vParameters,
@SerializedName("asr") val asr: V2vAsr,
@SerializedName("translation") val translation: V2vTranslation,
@SerializedName("tts") val tts: V2vTts
)
@Keep
data class V2vAdvancedFeatures(
@SerializedName("enable_rtm") val enableRtm: Boolean
)
@Keep
data class V2vParameters(
@SerializedName("data_channel") val dataChannel: String
)
@Keep
data class V2vAsr(
@SerializedName("language") val language: String
)
@Keep
data class V2vTranslation(
@SerializedName("language") val language: String
)
@Keep
data class V2vTts(
@SerializedName("enable") val enable: Boolean
)
@Keep
data class V2vJoinResponse(
@SerializedName("agent_id") val agentId: String,
@SerializedName("create_ts") val createTs: Long,
@SerializedName("status") val status: String
)
@Keep
data class V2vLeaveResponse(
@SerializedName("agent_id") val agentId: String,
@SerializedName("status") val status: String
)
@@ -0,0 +1,36 @@
package kr.co.vividnext.sodalive.agora.v2v
import android.util.Base64
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.BuildConfig
import java.util.UUID
class V2vRepository(private val api: V2vApi) {
fun join(request: V2vJoinRequest): Single<V2vJoinResponse> {
return api.join(
appId = BuildConfig.AGORA_APP_ID,
authorization = buildAuthorizationHeader(),
requestId = generateRequestId(),
request = request
)
}
fun leave(agentId: String): Single<V2vLeaveResponse> {
return api.leave(
appId = BuildConfig.AGORA_APP_ID,
agentId = agentId,
authorization = buildAuthorizationHeader(),
requestId = generateRequestId()
)
}
private fun buildAuthorizationHeader(): String {
val credentials = "${BuildConfig.AGORA_CUSTOMER_ID}:${BuildConfig.AGORA_CUSTOMER_SECRET}"
val encoded = Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP)
return "Basic $encoded"
}
private fun generateRequestId(): String {
return UUID.randomUUID().toString().replace("-", "")
}
}
@@ -5,15 +5,30 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.appsflyer.AppsFlyerLib
import com.appsflyer.deeplink.DeepLinkResult
import com.facebook.FacebookSdk
import com.kakao.sdk.common.KakaoSdk
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.yandex.mobile.ads.common.MobileAds
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
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
class SodaLiveApp : Application() {
class SodaLiveApp : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super.onCreate()
super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
Logger.addLogAdapter(object : AndroidLogAdapter() {
override fun isLoggable(priority: Int, tag: String?): Boolean {
@@ -25,7 +40,21 @@ class SodaLiveApp : Application() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SodaLiveApplicationHolder.init(this)
SharedPreferenceManager.init(applicationContext)
ChatRoomPreferenceManager.init(applicationContext)
ImageLoaderProvider.init(applicationContext)
FacebookSdk.fullyInitialize()
KakaoSdk.init(applicationContext, BuildConfig.KAKAO_APP_KEY)
setupAppsFlyer()
setupNotifly()
setupYandexAd()
}
private fun isDebuggable(): Boolean {
@@ -41,10 +70,90 @@ class SodaLiveApp : Application() {
packageManager.getApplicationInfo(packageName, 0)
}
debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
} catch (e: PackageManager.NameNotFoundException) {
/* debuggable variable will remain false */
} catch (_: PackageManager.NameNotFoundException) {
}
return debuggable
}
private fun setupAppsFlyer() {
// Appsflyer SDK 초기화
AppsFlyerLib.getInstance().init("tWF2wbJ5nSkya5Ru9mGcPU", null, this)
AppsFlyerLib.getInstance().start(this)
// 딥링크 및 디퍼드 딥링크 처리
AppsFlyerLib.getInstance().subscribeForDeepLink { deepLinkResult ->
when (deepLinkResult.status) {
DeepLinkResult.Status.FOUND -> {
SharedPreferenceManager.alreadyTrackingAppLaunch = false
val deepLink = deepLinkResult.deepLink
SharedPreferenceManager.marketingLinkValue = deepLink?.getStringValue(
"deep_link_value"
) ?: ""
val marketingPid = deepLink?.getStringValue(
"deep_link_sub1"
)
if (marketingPid != null) {
SharedPreferenceManager.marketingPid = marketingPid
}
SharedPreferenceManager.marketingUtmSource = deepLink?.getStringValue(
"deep_link_sub2"
) ?: ""
SharedPreferenceManager.marketingUtmMedium = deepLink?.getStringValue(
"deep_link_sub3"
) ?: ""
SharedPreferenceManager.marketingUtmCampaign = deepLink?.getStringValue(
"deep_link_sub4"
) ?: ""
SharedPreferenceManager.marketingLinkValueId = deepLink?.getStringValue(
"deep_link_sub5"
)?.toLongOrNull() ?: 0L
logUtmInFirebase()
}
DeepLinkResult.Status.NOT_FOUND -> Logger.d(
getString(R.string.deeplink_not_found)
)
DeepLinkResult.Status.ERROR -> Logger.d(
getString(R.string.deeplink_error, deepLinkResult.error)
)
}
}
}
private fun logUtmInFirebase() {
FirebaseTracking.logUtm()
}
private fun setupNotifly() {
Notifly.initialize(
applicationContext,
BuildConfig.NOTIFLY_PROJECT_ID,
BuildConfig.NOTIFLY_USERNAME,
BuildConfig.NOTIFLY_PASSWORD
)
}
private fun setupYandexAd() {
MobileAds.enableDebugErrorIndicator(BuildConfig.DEBUG && isDebuggable())
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
isAppInForeground = true
}
override fun onStop(owner: LifecycleOwner) {
isAppInForeground = false
}
companion object {
var isAppInForeground = false
}
}
@@ -1,15 +1,18 @@
package kr.co.vividnext.sodalive.audio_content
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import java.util.TimeZone
@Keep
data class AddAllPlaybackTrackingRequest(
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
@SerializedName("trackingDataList") val trackingDataList: List<PlaybackTrackingData>
)
@Keep
data class PlaybackTrackingData(
@SerializedName("contentId") val contentId: Long,
@SerializedName("playDateTime") val playDateTime: String,
@SerializedName("isPreview") val isPreview: Boolean,
@SerializedName("isPreview") val isPreview: Boolean
)
@@ -14,6 +14,8 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.category.AudioContentCategoryAdapter
import kr.co.vividnext.sodalive.audio_content.category.GetCategoryListResponse
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity
@@ -32,9 +34,11 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
private lateinit var loadingDialog: LoadingDialog
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var categoryAdapter: AudioContentCategoryAdapter
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)
@@ -43,23 +47,29 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.page = 1
viewModel.isLast = false
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
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()
}
bindData()
viewModel.getCategoryList(userId = userId)
viewModel.getAudioContentList(userId = userId) { finish() }
}
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 {
@@ -70,6 +80,69 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
activityResultLauncher.launch(intent)
}
categoryAdapter = AudioContentCategoryAdapter(
allCategoryLabel = allCategoryLabel
) {
viewModel.selectCategory(it, userId = userId)
}
binding.rvCategory.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvCategory.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
categoryAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvCategory.adapter = categoryAdapter
binding.rvAudioContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
audioContentAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
binding.rvAudioContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
@@ -188,6 +261,17 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
)
viewModel.getAudioContentList(userId = userId) { finish() }
}
viewModel.categoryListLiveData.observe(this) {
if (it.isNotEmpty()) {
binding.rvCategory.visibility = View.VISIBLE
val items = it as MutableList<GetCategoryListResponse>
items.add(0, GetCategoryListResponse(0, allCategoryLabel))
categoryAdapter.addItems(items = items)
} else {
binding.rvCategory.visibility = View.GONE
}
}
}
private fun deselectSort() {
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
@@ -21,6 +22,18 @@ class AudioContentAdapter(
private val binding: ItemAudioContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentListItem) {
binding.ivPin.visibility = if (item.isPin) {
View.VISIBLE
} else {
View.GONE
}
binding.tvPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
@@ -33,17 +46,38 @@ class AudioContentAdapter(
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)
binding.tvPrice.visibility = View.GONE
binding.tvOwned.visibility = View.GONE
binding.tvRented.visibility = View.GONE
binding.tvSoldOut.visibility = View.GONE
binding.tvScheduledToOpen.visibility = if (item.isScheduledToOpen) {
View.VISIBLE
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
View.GONE
}
if (item.isOwned) {
binding.tvOwned.visibility = View.VISIBLE
} else if (item.isRented) {
binding.tvRented.visibility = View.VISIBLE
} else if (item.isSoldOut) {
binding.tvSoldOut.visibility = View.VISIBLE
} else {
binding.tvPrice.visibility = View.VISIBLE
if (item.price < 1) {
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()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
}
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
@@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
import kr.co.vividnext.sodalive.audio_content.all.by_theme.GetContentByThemeResponse
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
@@ -8,13 +11,17 @@ import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailRespon
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
import kr.co.vividnext.sodalive.audio_content.player.GenerateUrlResponse
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
import kr.co.vividnext.sodalive.home.AudioContentMainItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
@@ -29,20 +36,53 @@ import retrofit2.http.Path
import retrofit2.http.Query
interface AudioContentApi {
@GET("/audio-content/all")
fun getAllAudioContents(
@Query("page") page: Int,
@Query("size") size: Int,
@Query("isFree") isFree: Boolean?,
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
@Query("sort-type") sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
@Query("theme") theme: String? = null,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<AudioContentMainItem>>>
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@Query("category-id") categoryId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentListResponse>>
@GET("/audio-content/replay-live")
fun getAudioContentReplayLiveList(
@Header("Authorization") authHeader: String
): Flowable<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/theme")
fun getAudioContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
@GET("/audio-content/theme/active")
fun getAudioContentActiveThemeList(
@Query("isFree") isFree: Boolean?,
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/theme/{id}/content")
fun getAudioContentByTheme(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetContentByThemeResponse>>
@POST("/audio-content")
@Multipart
fun uploadAudioContent(
@@ -122,17 +162,21 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/main")
fun getMain(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentMainResponse>>
@GET("/audio-content/main/new")
fun getNewContentOfTheme(
@Query("theme") theme: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/main/new/all")
fun getNewContentAllOfTheme(
@Query("isFree") isFree: Boolean,
@Query("theme") theme: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetNewContentAllResponse>>
@POST("/audio-content/donation")
fun donation(
@Body request: AudioContentDonationRequest,
@@ -144,4 +188,57 @@ interface AudioContentApi {
@Body request: ModifyCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/main/theme")
fun getNewContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking-sort-type")
fun getContentRankingSortType(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<String>>>
@GET("/audio-content/ranking")
fun getContentRanking(
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sortType: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentRanking>>
@GET("/audio-content/main/curation-list")
fun getCurationList(
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentCurationResponse>>>
@GET("/audio-content/main/banner-list")
fun getMainBannerList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentBannerResponse>>>
@GET("/audio-content/main/order-list")
fun getMainOrderList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@POST("/audio-content/pin-to-the-top/{id}")
fun pinContent(
@Path("id") audioContentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/audio-content/unpin-at-the-top/{id}")
fun unpinContent(
@Path("id") audioContentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}/generate-url")
fun generateUrl(
@Path("id") contentId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GenerateUrlResponse>>
}
@@ -1,3 +1,5 @@
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
package kr.co.vividnext.sodalive.audio_content
import android.app.NotificationChannel
@@ -6,6 +8,7 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.media.AudioAttributes
@@ -21,7 +24,7 @@ import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
class AudioContentPlayService :
@@ -51,6 +54,7 @@ class AudioContentPlayService :
putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
intent.setPackage(packageName)
sendBroadcast(intent)
handler.postDelayed(this, 1000)
}
@@ -64,6 +68,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
@@ -101,6 +106,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
@@ -136,6 +142,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
false
@@ -198,6 +205,14 @@ class AudioContentPlayService :
}
}
MusicAction.SEEK_BACKWARD.name -> {
seekBackward10Seconds()
}
MusicAction.SEEK_FORWARD.name -> {
seekForward10Seconds()
}
else -> {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (contentId != null && this.contentId == contentId) {
@@ -205,6 +220,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PAUSE
@@ -220,6 +236,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@@ -254,6 +271,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
true
@@ -306,6 +324,23 @@ class AudioContentPlayService :
}
}
private fun seekForward10Seconds() {
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
val currentPosition = mediaPlayer.currentPosition
val duration = mediaPlayer.duration
val newPosition = (currentPosition + 10_000).coerceAtMost(duration)
mediaPlayer.seekTo(newPosition)
}
}
private fun seekBackward10Seconds() {
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
val currentPosition = mediaPlayer.currentPosition
val newPosition = (currentPosition - 10_000).coerceAtLeast(0)
mediaPlayer.seekTo(newPosition)
}
}
private fun toggleIsPlaying(isPlaying: Boolean? = null) {
this.isPlaying = isPlaying ?: !this.isPlaying
if (this.isPlaying) {
@@ -317,6 +352,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
@@ -341,6 +377,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
@@ -367,6 +404,7 @@ class AudioContentPlayService :
mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
@@ -378,6 +416,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
@@ -403,6 +442,7 @@ class AudioContentPlayService :
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
setPackage(packageName)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
@@ -433,9 +473,17 @@ class AudioContentPlayService :
}
private fun updateNotification() {
val intent = Intent(this, MainActivity::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,
@@ -468,7 +516,7 @@ class AudioContentPlayService :
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "")
@@ -498,7 +546,16 @@ class AudioContentPlayService :
.setShowActionsInCompactView(0, 1)
)
startForeground(1, notificationBuilder.build())
val notification = notificationBuilder.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
1,
notification,
FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
startForeground(1, notification)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
@@ -556,6 +613,6 @@ class AudioContentPlayService :
}
enum class MusicAction {
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP, SEEK_FORWARD, SEEK_BACKWARD
}
}
@@ -1,33 +1,40 @@
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.user.CreatorFollowRequestRequest
import kr.co.vividnext.sodalive.user.UserApi
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
private val categoryApi: CategoryApi
) {
fun getAudioContentList(
id: Long,
categoryId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort,
token: String
) = api.getAudioContentList(
id = id,
categoryId = categoryId,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
authHeader = token
)
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
fun uploadAudioContent(
@@ -60,28 +67,15 @@ 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
)
fun registerNotification(
creatorId: Long,
token: String
) = userApi.creatorFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun unRegisterNotification(
creatorId: Long,
token: String
) = userApi.creatorUnFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun orderContent(
contentId: Long,
orderType: OrderType,
@@ -115,10 +109,21 @@ class AudioContentRepository(
token: String
) = api.likeContent(request, authHeader = token)
fun getMain(token: String) = api.getMain(authHeader = token)
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
fun getNewContentAllOfTheme(
isFree: Boolean,
theme: String,
page: Int,
size: Int,
token: String
) = api.getNewContentAllOfTheme(
isFree = isFree,
theme = theme,
page = page - 1,
size = size,
authHeader = token
)
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(
authHeader = token
)
@@ -135,4 +140,76 @@ class AudioContentRepository(
),
authHeader = token
)
fun getContentRankingSortType(token: String) = api.getContentRankingSortType(authHeader = token)
fun getContentRanking(
page: Int,
size: Int,
sortType: String = SodaLiveApplicationHolder.get()
.getString(R.string.screen_home_sort_revenue),
token: String
) = api.getContentRanking(
page = page - 1,
size = size,
sortType = sortType,
authHeader = token
)
fun pinContent(
audioContentId: Long,
token: String
) = api.pinContent(audioContentId, authHeader = token)
fun unpinContent(
audioContentId: Long,
token: String
) = api.unpinContent(audioContentId, authHeader = token)
fun getCategoryList(
creatorId: Long,
token: String
) = categoryApi.getCategoryList(creatorId, authHeader = token)
fun getAudioContentByTheme(
themeId: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
token: String
) = api.getAudioContentByTheme(
id = themeId,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAllAudioContents(
page: Int,
size: Int,
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null,
sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
theme: String? = null,
token: String
) = api.getAllAudioContents(
page = page - 1,
size = size,
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
sortType = sortType,
theme = theme,
authHeader = token
)
fun getAudioContentActiveThemeList(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null,
token: String
) = api.getAudioContentActiveThemeList(
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
authHeader = token
)
}
@@ -6,8 +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() {
@@ -24,6 +27,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
val audioContentListLiveData: LiveData<GetAudioContentListResponse>
get() = _audioContentListLiveData
private var _categoryListLiveData = MutableLiveData<List<GetCategoryListResponse>>()
val categoryListLiveData: LiveData<List<GetCategoryListResponse>>
get() = _categoryListLiveData
private val _sort = MutableLiveData(Sort.NEWEST)
val sort: LiveData<Sort>
get() = _sort
@@ -36,12 +43,16 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
PRICE_HIGH,
@SerializedName("PRICE_LOW")
PRICE_LOW
PRICE_LOW,
@SerializedName("POPULARITY")
POPULARITY
}
private var isLast = false
var isLast = false
var page = 1
private val size = 10
private var selectedCategoryId = 0L
fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
@@ -49,6 +60,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
compositeDisposable.add(
repository.getAudioContentList(
id = userId,
categoryId = selectedCategoryId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}",
@@ -59,18 +71,19 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
if (it.data.items.isEmpty()) {
isLast = true
}
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
@@ -84,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()
}
@@ -99,4 +115,48 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
isLast = false
_sort.postValue(sort)
}
fun selectCategory(categoryId: Long, userId: Long) {
isLast = false
page = 1
selectedCategoryId = categoryId
getAudioContentList(userId = userId)
}
fun getCategoryList(userId: Long) {
compositeDisposable.add(
repository.getCategoryList(
creatorId = userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_categoryListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
}
@@ -1,14 +1,16 @@
package kr.co.vividnext.sodalive.audio_content
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import androidx.annotation.Keep
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Entity
@Entity(tableName = "playback_tracking")
@Keep
data class PlaybackTracking(
@Id
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var contentId: Long,
var totalDuration: Int,
@@ -1,29 +1,21 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.common.ObjectBox
import kr.co.vividnext.sodalive.audio_content.db.PlaybackTrackingDao
class PlaybackTrackingRepository(private val objectBox: ObjectBox) {
class PlaybackTrackingRepository(private val dao: PlaybackTrackingDao) {
fun savePlaybackTracking(data: PlaybackTracking): Long {
return objectBox.playbackTrackingBox.put(data)
return dao.insert(data)
}
fun getPlaybackTracking(id: Long): PlaybackTracking? {
val query = objectBox.playbackTrackingBox
.query(PlaybackTracking_.id.equal(id))
.build()
val playbackTracking = query.findFirst()
query.close()
return playbackTracking
return dao.getById(id)
}
fun getAllPlaybackTracking(): List<PlaybackTracking> {
return objectBox
.playbackTrackingBox
.all
return dao.getAll()
}
fun removeAllPlaybackTracking() {
objectBox.playbackTrackingBox.removeAll()
dao.deleteAll()
}
}
@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.audio_content
import com.google.gson.annotations.SerializedName
enum class PurchaseOption {
@SerializedName("BOTH")
BOTH,
@SerializedName("BUY_ONLY")
BUY_ONLY,
@SerializedName("RENT_ONLY")
RENT_ONLY
}
@@ -0,0 +1,230 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
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.AudioContentViewModel
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeContentAdapter
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
import org.koin.android.ext.android.inject
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
ActivityAudioContentAllBinding::inflate
) {
private val viewModel: AudioContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: HomeContentAdapter
private lateinit var themeAdapter: HomeContentThemeAdapter
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)
isPointOnly = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, false)
super.onCreate(savedInstanceState)
bindData()
viewModel.reset()
viewModel.getThemeList(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = when {
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() }
binding.tvSortNewest.setOnClickListener {
viewModel.selectSort(AudioContentViewModel.Sort.NEWEST)
}
binding.tvSortPopularity.setOnClickListener {
viewModel.selectSort(AudioContentViewModel.Sort.POPULARITY)
}
setupTheme()
setupRecycler()
}
private fun setupTheme() {
themeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
adapter.addItems(emptyList())
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
viewModel.selectTheme(theme, isFree = isFree, isPointOnly = isPointOnly)
}
binding.rvTheme.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
themeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvTheme.adapter = themeAdapter
}
private fun setupRecycler() {
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
adapter = HomeContentAdapter(
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
itemSquareSizePx = itemSize
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, true)
)
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
}
})
binding.rvContent.adapter = adapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) loadingDialog.show(screenWidth) else loadingDialog.dismiss()
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.themeListLiveData.observe(this) {
val themes = mutableListOf(allThemeLabel)
themes.addAll(it)
themeAdapter.addItems(themes)
}
viewModel.itemsLiveData.observe(this) { list ->
if (adapter.itemCount > 0 || list.isNotEmpty()) {
binding.rvContent.visibility = View.VISIBLE
binding.llEmpty.visibility = View.GONE
} else {
binding.rvContent.visibility = View.GONE
binding.llEmpty.visibility = View.VISIBLE
}
adapter.appendItems(list)
}
viewModel.sortLiveData.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.POPULARITY -> {
binding.tvSortPopularity
}
else -> {
binding.tvSortNewest
}
}
)
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortPopularity.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
adapter.addItems(emptyList())
viewModel.loadAll(
isFree = if (isFree) true else null,
isPointAvailableOnly = if (isPointOnly) true else null
)
}
}
@@ -0,0 +1,132 @@
package kr.co.vividnext.sodalive.audio_content.all
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.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(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?> get() = _toastLiveData
private val _itemsLiveData = MutableLiveData<List<AudioContentMainItem>>()
val itemsLiveData: LiveData<List<AudioContentMainItem>> get() = _itemsLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _sortLiveData = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
val sortLiveData: LiveData<AudioContentViewModel.Sort>
get() = _sortLiveData
private var page = 1
private val size = 20
private var isLast = false
private var selectedTheme = ""
fun reset() {
page = 1
isLast = false
}
fun getThemeList(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null
) {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getAudioContentActiveThemeList(
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_themeListLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
}
)
)
}
fun loadAll(
isFree: Boolean? = null,
isPointAvailableOnly: Boolean? = null
) {
if (_isLoading.value == true || isLast) return
_isLoading.value = true
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getAllAudioContents(
page = page,
size = size,
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
sortType = _sortLiveData.value!!,
theme = selectedTheme.ifBlank { null },
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
val list = response.data ?: emptyList()
if (list.isNotEmpty()) {
page += 1
}
if (list.size < size) {
isLast = true
}
_itemsLiveData.postValue(list)
_isLoading.value = false
}, { t ->
_isLoading.value = false
_toastLiveData.postValue(t.message ?: unknownError)
})
)
}
fun selectTheme(theme: String, isFree: Boolean, isPointOnly: Boolean) {
reset()
selectedTheme = theme
loadAll(isFree, isPointOnly)
}
fun selectSort(sortType: AudioContentViewModel.Sort) {
if (_sortLiveData.value != sortType) {
reset()
_sortLiveData.value = sortType
}
}
}
@@ -0,0 +1,195 @@
package kr.co.vividnext.sodalive.audio_content.all
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.annotation.OptIn
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
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import org.koin.android.ext.android.inject
@OptIn(UnstableApi::class)
class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBinding>(
ActivityAudioContentNewAllBinding::inflate
) {
private val viewModel: AudioContentNewAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
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
override fun onCreate(savedInstanceState: Bundle?) {
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
super.onCreate(savedInstanceState)
bindData()
viewModel.getThemeList()
viewModel.getNewContentList(isFree = isFree)
}
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) {
getString(R.string.screen_audio_content_new_all_notice_free)
} else {
getString(R.string.screen_audio_content_new_all_notice)
}
setupNewContentTheme()
setupNewContent()
}
@SuppressLint("NotifyDataSetChanged")
private fun setupNewContentTheme() {
newContentThemeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
newContentAdapter.clear()
newContentAdapter.notifyDataSetChanged()
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
viewModel.selectTheme(theme, isFree = isFree)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
newContentThemeAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = itemSize,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
CreatorChannelActivity.newIntent(this, it)
)
}
)
val spanCount = 2
val spacingPx = 16f.dpToPx().toInt()
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, true)
)
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getNewContentList(isFree)
}
}
})
binding.rvContent.adapter = newContentAdapter
}
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.themeListLiveData.observe(this) {
val themes = mutableListOf(allThemeLabel)
themes.addAll(it)
newContentThemeAdapter.addItems(themes)
}
viewModel.newContentListLiveData.observe(this) {
if (newContentAdapter.itemCount > 0 || it.isNotEmpty()) {
binding.rvContent.visibility = View.VISIBLE
binding.llNoItems.visibility = View.GONE
} else {
binding.rvContent.visibility = View.GONE
binding.llNoItems.visibility = View.VISIBLE
}
newContentAdapter.addItems(it)
}
viewModel.newContentTotalCountLiveData.observe(this) {
binding.tvTotalCount.text = "$it"
}
}
}
@@ -0,0 +1,112 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentNewAllAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentNewAllBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
CenterCrop(),
RoundedCorners(8)
)
)
.into(binding.ivAudioContentCoverImage)
val layoutParams =
binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
binding.ivPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvAudioContentTitle.text = item.title
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
if (item.price > 0) {
binding.ivCan.visibility = View.VISIBLE
binding.tvCan.text = item.price.moneyFormat()
} else {
binding.ivCan.visibility = View.GONE
binding.tvCan.text = context.getString(R.string.audio_content_price_free)
}
binding.tvTime.text = item.duration
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
private val items = mutableListOf<GetAudioContentMainItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
parent.context,
ItemAudioContentNewAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
onClickItem = onClickItem,
onClickCreator = onClickCreator
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentMainItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}
@@ -0,0 +1,124 @@
package kr.co.vividnext.sodalive.audio_content.all
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.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
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _newContentTotalCountLiveData = MutableLiveData<Int>()
val newContentTotalCountLiveData: LiveData<Int>
get() = _newContentTotalCountLiveData
private var isLast = false
private var page = 1
private val size = 10
private var selectedTheme = ""
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 = selectedTheme,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
} else {
isLast = true
}
_newContentListLiveData.postValue(it.data.items)
_newContentTotalCountLiveData.postValue(it.data.totalCount)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
}
)
)
}
}
fun getThemeList() {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_themeListLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
}
)
)
}
fun selectTheme(theme: String, isFree: Boolean) {
isLast = false
page = 1
selectedTheme = theme
getNewContentList(isFree)
}
}
@@ -0,0 +1,186 @@
package kr.co.vividnext.sodalive.audio_content.all
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.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
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.ActivityAudioContentRankingAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentRankingAllActivity : BaseActivity<ActivityAudioContentRankingAllBinding>(
ActivityAudioContentRankingAllBinding::inflate
) {
private val viewModel: AudioContentRankingAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentRankingAllAdapter
private lateinit var sortAdapter: AudioContentMainNewContentThemeAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getAudioContentRankingSortType()
viewModel.getAudioContentRanking()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_ranking_title)
adapter = AudioContentRankingAllAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
startActivity(intent)
}
binding.rvContentRanking.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvContentRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 10f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 10f.dpToPx().toInt()
}
}
}
})
binding.rvContentRanking.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getAudioContentRanking()
}
}
})
binding.rvContentRanking.adapter = adapter
setupContentRankingSortType()
}
@SuppressLint("NotifyDataSetChanged")
private fun setupContentRankingSortType() {
sortAdapter = AudioContentMainNewContentThemeAdapter {
adapter.items.clear()
adapter.notifyDataSetChanged()
viewModel.selectSort(sort = it)
}
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvContentRankingSort.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
sortAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvContentRankingSort.adapter = sortAdapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.dateStringLiveData.observe(this) {
binding.tvDate.text = it
}
viewModel.contentRankingItemsLiveData.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.contentRankingSortListLiveData.observe(this) {
sortAdapter.addItems(it)
}
}
}
@@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentRankingAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentRankingAllAdapter(
private val onItemClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentRankingAllAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentRankingAllBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentRankingItem, index: Int) {
binding.root.setOnClickListener { onItemClick(item.contentId) }
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.tvTitle.text = item.title
binding.tvRank.text = index.plus(1).toString()
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvNickname.text = item.creatorNickname
if (item.price < 1) {
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(
5.3f.dpToPx().toInt(),
2.7f.dpToPx().toInt(),
5.3f.dpToPx().toInt(),
2.7f.dpToPx().toInt()
)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_2_6_cf5c37)
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.color_909090))
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can,
0,
0,
0
)
binding.tvPrice.setPadding(0, 0, 0, 0)
binding.tvPrice.setBackgroundResource(0)
}
}
}
val items = mutableListOf<GetAudioContentRankingItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
parent.context,
ItemAudioContentRankingAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
}
@@ -0,0 +1,122 @@
package kr.co.vividnext.sodalive.audio_content.all
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.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
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _dateStringLiveData = MutableLiveData<String>()
val dateStringLiveData: LiveData<String>
get() = _dateStringLiveData
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
val contentRankingSortListLiveData: LiveData<List<String>>
get() = _contentRankingSortListLiveData
private var _contentRankingItemsLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
val contentRankingItemsLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _contentRankingItemsLiveData
var page = 1
private var pageSize = 10
private var isLast = false
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,
size = pageSize,
sortType = selectedSort,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_isLoading.value = false
_dateStringLiveData.value =
"${it.data.startDate}~${it.data.endDate}"
if (it.data.items.isNotEmpty()) {
page += 1
_contentRankingItemsLiveData.value = it.data.items
} else {
isLast = true
}
} else {
_isLoading.value = false
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
}
)
)
}
}
fun getAudioContentRankingSortType() {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
compositeDisposable.add(
repository.getContentRankingSortType(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_contentRankingSortListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
}
)
)
}
fun selectSort(sort: String) {
page = 1
isLast = false
selectedSort = sort
getAudioContentRanking()
}
}
@@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.all
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetNewContentAllResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)
@@ -0,0 +1,177 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
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.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import org.koin.android.ext.android.inject
class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>(
ActivityAudioContentAllByThemeBinding::inflate
) {
private val viewModel: AudioContentAllByThemeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentNewAllAdapter
private var themeId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
themeId = intent.getLongExtra(Constants.EXTRA_THEME_ID, 0)
if (themeId <= 0) {
Toast.makeText(
applicationContext,
getString(R.string.screen_audio_content_error_invalid_request_retry),
Toast.LENGTH_LONG
).show()
finish()
}
super.onCreate(savedInstanceState)
bindData()
viewModel.getContentList(themeId = themeId)
}
@OptIn(UnstableApi::class)
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
CreatorChannelActivity.newIntent(this, it)
)
}
)
binding.rvContentAll.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContentAll.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContentAll.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getContentList(themeId = themeId)
}
}
})
binding.rvContentAll.adapter = adapter
binding.tvSortNewest.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.NEWEST)
}
binding.tvSortPriceLow.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW)
}
binding.tvSortPriceHigh.setOnClickListener {
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH)
}
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.contentListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
adapter.clear()
binding.rvContentAll.scrollToPosition(0)
}
binding.tvTotalCount.text = "${it.totalCount}"
binding.toolbar.tvBack.text = it.theme
adapter.addItems(it.items)
}
viewModel.sort.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.PRICE_HIGH -> {
binding.tvSortPriceHigh
}
AudioContentViewModel.Sort.PRICE_LOW -> {
binding.tvSortPriceLow
}
else -> {
binding.tvSortNewest
}
}
)
viewModel.getContentList(themeId = themeId)
}
}
private fun deselectSort() {
val color = ContextCompat.getColor(
applicationContext,
R.color.color_88e2e2e2
)
binding.tvSortNewest.setTextColor(color)
binding.tvSortPriceLow.setTextColor(color)
binding.tvSortPriceHigh.setTextColor(color)
}
private fun selectSort(view: TextView) {
view.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_e2e2e2
)
)
}
}
@@ -0,0 +1,92 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme
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.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
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _contentListLiveData = MutableLiveData<GetContentByThemeResponse>()
val contentListLiveData: LiveData<GetContentByThemeResponse>
get() = _contentListLiveData
private val _sort = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
val sort: LiveData<AudioContentViewModel.Sort>
get() = _sort
private var isLast = false
var page = 1
private val size = 15
fun getContentList(themeId: Long) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentByTheme(
themeId = themeId,
page = page,
size = size,
sort = _sort.value!!,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_contentListLiveData.postValue(it.data!!)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
}
fun changeSort(sort: AudioContentViewModel.Sort) {
page = 1
isLast = false
_sort.postValue(sort)
}
}
@@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.audio_content.all.by_theme
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
@Keep
data class GetContentByThemeResponse(
@SerializedName("theme") val theme: String,
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentMainItem>
)
@@ -0,0 +1,80 @@
package kr.co.vividnext.sodalive.audio_content.box
import android.widget.LinearLayout
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListFragment
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBoxBinding
class AudioContentBoxActivity : BaseActivity<ActivityAudioContentBoxBinding>(
ActivityAudioContentBoxBinding::inflate
) {
private var startTabPosition = 0
override fun setupView() {
startTabPosition = intent.getIntExtra(Constants.EXTRA_START_TAB_POSITION, 0)
setupToolbar()
setupTabs()
binding.tabs.getTabAt(startTabPosition)?.select()
supportFragmentManager.beginTransaction()
.replace(
R.id.fl_container,
if (startTabPosition == 1) {
AudioContentPlaylistListFragment()
} else {
AudioContentOrderListFragment()
}
)
.commit()
}
private fun setupToolbar() {
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(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) {
tab.view.isSelected = true
when (tab.position) {
1 -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, AudioContentPlaylistListFragment())
.commit()
else -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, AudioContentOrderListFragment())
.commit()
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tab.view.isSelected = false
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
val tabStrip = tabs.getChildAt(0) as LinearLayout
for (i in 0 until tabStrip.childCount) {
val tab = tabStrip.getChildAt(i)
val params = tab.layoutParams as LinearLayout.LayoutParams
params.setMargins(12, 0, 12, 0)
params.height = LinearLayout.LayoutParams.WRAP_CONTENT
tab.layoutParams = params
tab.minimumHeight = 0
}
}
}
@@ -0,0 +1,80 @@
package kr.co.vividnext.sodalive.audio_content.category
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
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.databinding.ItemContentCategoryBinding
class AudioContentCategoryAdapter(
private val allCategoryLabel: String,
private val onClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCategoryAdapter.ViewHolder>() {
private val items = mutableListOf<GetCategoryListResponse>()
private var selectedCategory = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemContentCategoryBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(item: GetCategoryListResponse) {
if (
item.category == selectedCategory ||
(selectedCategory.isEmpty() && item.category == allCategoryLabel)
) {
binding.tvCategory.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvCategory.setTextColor(
ContextCompat.getColor(context, R.color.color_3bb9f1)
)
} else {
binding.tvCategory.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
)
binding.tvCategory.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
}
binding.tvCategory.text = item.category
binding.root.setOnClickListener {
onClick(item.categoryId)
selectedCategory = item.category
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetCategoryListResponse>) {
this.selectedCategory = ""
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemContentCategoryBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
fun clear() {
this.items.clear()
}
}
@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.audio_content.category
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface CategoryApi {
@GET("/category")
fun getCategoryList(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetCategoryListResponse>>>
}
@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.category
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetCategoryListResponse(
@SerializedName("categoryId") val categoryId: Long,
@SerializedName("category") val category: String
)
@@ -19,7 +19,8 @@ class AudioContentCommentAdapter(
private val creatorId: Long,
private val modifyComment: (Long, String) -> Unit,
private val onClickDelete: (Long) -> Unit,
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
private val onItemClick: (GetAudioContentCommentListItem) -> Unit,
private val onClickProfile: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
@@ -30,12 +31,26 @@ class AudioContentCommentAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCommentListItem) {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
binding.tvSecret.visibility = if (item.isSecret) {
View.VISIBLE
} else {
View.GONE
}
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.ivCommentProfile.setOnClickListener {
if (SharedPreferenceManager.userId != item.writerId) {
onClickProfile(item.writerId)
}
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val can = item.donationCan
if (can > 0) {
@@ -44,27 +59,27 @@ class AudioContentCommentAdapter(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
@@ -84,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 (
@@ -99,12 +117,16 @@ class AudioContentCommentAdapter(
showOptionMenu(
context,
binding.ivMenu,
commentId = item.id,
writerId = item.writerId,
creatorId = creatorId,
onClickModify = {
binding.rlCommentModify.visibility = View.VISIBLE
binding.tvComment.visibility = View.GONE
},
onClickDelete = {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
onClickDelete(item.id)
}
)
}
@@ -141,10 +163,10 @@ class AudioContentCommentAdapter(
private fun showOptionMenu(
context: Context,
v: View,
commentId: Long,
writerId: Long,
creatorId: Long,
onClickModify: () -> Unit
onClickModify: () -> Unit,
onClickDelete: () -> Unit
) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
@@ -162,7 +184,7 @@ class AudioContentCommentAdapter(
}
R.id.menu_review_delete -> {
onClickDelete(commentId)
onClickDelete()
}
}
@@ -14,7 +14,8 @@ import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding
class AudioContentCommentFragment(
private val creatorId: Long,
private val audioContentId: Long
private val audioContentId: Long,
private val isShowSecret: Boolean
) : BottomSheetDialogFragment() {
private lateinit var binding: DialogAudioContentCommentBinding
@@ -50,7 +51,8 @@ class AudioContentCommentFragment(
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = AudioContentCommentListFragment.newInstance(
creatorId = creatorId,
audioContentId = audioContentId
audioContentId = audioContentId,
isShowSecret = isShowSecret
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)
@@ -20,6 +20,7 @@ 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.databinding.FragmentAudioContentCommentListBinding
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
@@ -35,6 +36,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
private var creatorId: Long = 0
private var audioContentId: Long = 0
private var isShowSecret: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
@@ -43,6 +45,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
): View? {
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
isShowSecret = arguments?.getBoolean(Constants.EXTRA_IS_SHOW_SECRET) ?: false
return super.onCreateView(inflater, container, savedInstanceState)
}
@@ -73,8 +76,20 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
val isSecret = binding.tvSecret.isSelected
viewModel.registerComment(audioContentId, comment, isSecret)
binding.etComment.setText("")
viewModel.registerComment(audioContentId, comment)
binding.tvSecret.isSelected = false
}
if (isShowSecret) {
binding.tvSecret.visibility = View.VISIBLE
binding.tvSecret.setOnClickListener {
binding.tvSecret.isSelected = !binding.tvSecret.isSelected
}
} else {
binding.tvSecret.visibility = View.GONE
}
adapter = AudioContentCommentAdapter(
@@ -91,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,
@@ -101,12 +116,20 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
isActive = false
)
},
cancelButtonTitle = "취소",
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
},
onItemClick = {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
},
onClickProfile = {
MemberProfileDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
memberId = it,
screenWidth = screenWidth
).show()
}
)
@@ -202,10 +225,15 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
}
companion object {
fun newInstance(creatorId: Long, audioContentId: Long): AudioContentCommentListFragment {
fun newInstance(
creatorId: Long,
audioContentId: Long,
isShowSecret: Boolean
): AudioContentCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
args.putBoolean(Constants.EXTRA_IS_SHOW_SECRET, isShowSecret)
val fragment = AudioContentCommentListFragment()
fragment.arguments = args
@@ -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()
}
@@ -84,7 +95,7 @@ class AudioContentCommentListViewModel(
}
}
fun registerComment(contentId: Long, comment: String) {
fun registerComment(contentId: Long, comment: String, isSecret: Boolean) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
@@ -93,6 +104,7 @@ class AudioContentCommentListViewModel(
repository.registerComment(
contentId = contentId,
comment = comment,
isSecret = isSecret,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -109,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)
}
)
)
@@ -131,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
}
@@ -168,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)
}
)
)
@@ -35,7 +35,7 @@ class AudioContentCommentReplyAdapter(
LayoutInflater.from(parent.context),
parent,
false
),
)
)
} else {
AudioContentCommentReplyItemViewHolder(
@@ -124,27 +124,27 @@ class AudioContentCommentReplyHeaderViewHolder(
binding.tvDonationCan.text = can.moneyFormat()
binding.llDonationCan.setBackgroundResource(
when {
can >= 100000 -> {
can >= 10000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
can >= 50000 -> {
can >= 5000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
can >= 10000 -> {
can >= 1000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
can >= 5000 -> {
can >= 500 -> {
R.drawable.bg_round_corner_10_7_59548f
}
can >= 1000 -> {
can >= 100 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
can >= 500 -> {
can >= 50 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
@@ -172,13 +172,13 @@ 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) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.rlCommentModify.visibility = View.GONE
binding.tvComment.visibility = View.VISIBLE
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
@@ -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)
}
@@ -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)
}
)
)
@@ -8,12 +8,14 @@ class AudioContentCommentRepository(private val api: AudioContentApi) {
contentId: Long,
comment: String,
parentId: Long? = null,
isSecret: Boolean = false,
token: String
) = api.registerComment(
request = RegisterAudioContentCommentRequest(
comment = comment,
contentId = contentId,
parentId = parentId
parentId = parentId,
isSecret = isSecret
),
authHeader = token
)
@@ -1,21 +1,25 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Keep
data class GetAudioContentCommentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentCommentListItem>
)
@Parcelize
@Keep
data class GetAudioContentCommentListItem(
@SerializedName("id") val id: Long,
@SerializedName("writerId") val writerId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("comment") val comment: String,
@SerializedName("isSecret") val isSecret: Boolean,
@SerializedName("donationCan") val donationCan: Int,
@SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int
@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.comment
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class ModifyCommentRequest(
@SerializedName("commentId") val commentId: Long,
@SerializedName("comment") var comment: String? = null,
@@ -1,9 +1,12 @@
package kr.co.vividnext.sodalive.audio_content.comment
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class RegisterAudioContentCommentRequest(
@SerializedName("comment") val comment: String,
@SerializedName("contentId") val contentId: Long,
@SerializedName("parentId") val parentId: Long?
@SerializedName("parentId") val parentId: Long?,
@SerializedName("isSecret") val isSecret: Boolean
)
@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.audio_content.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
@Dao
interface PlaybackTrackingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entity: PlaybackTracking): Long
@Query("SELECT * FROM playback_tracking WHERE id = :id LIMIT 1")
fun getById(id: Long): PlaybackTracking?
@Query("SELECT * FROM playback_tracking")
fun getAll(): List<PlaybackTracking>
@Query("DELETE FROM playback_tracking")
fun deleteAll()
}
@@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.audio_content.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
import kr.co.vividnext.sodalive.common.Converter
@Database(entities = [PlaybackTracking::class], version = 1, exportSchema = true)
@TypeConverters(Converter::class)
abstract class PlaybackTrackingDatabase : RoomDatabase() {
abstract fun playbackTrackingDao(): PlaybackTrackingDao
companion object {
@Volatile
private var INSTANCE: PlaybackTrackingDatabase? = null
fun getDatabase(context: Context): PlaybackTrackingDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PlaybackTrackingDatabase::class.java,
"playback_tracking_database"
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
INSTANCE = instance
instance
}
}
}
}
@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDetailUserBinding
class AudioContentBuyerAdapter : RecyclerView.Adapter<AudioContentBuyerAdapter.ViewHolder>() {
private val items = mutableListOf<ContentBuyer>()
inner class ViewHolder(
private val binding: ItemLiveRoomDetailUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ContentBuyer) {
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
return ViewHolder(
ItemLiveRoomDetailUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(buyerList: List<ContentBuyer>) {
items.addAll(buyerList)
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun clear() {
items.clear()
notifyDataSetChanged()
}
}
@@ -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()
}
@@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.audio_content.detail
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.databinding.DialogAudioContentDetailMenuBinding
class AudioContentDetailMenuBottomSheetDialog(
private val isPin: Boolean,
private val isCreator: Boolean,
private val onClickPin: () -> Unit,
private val onClickModify: () -> Unit,
private val onClickDelete: () -> Unit,
private val onClickReport: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var dialog: DialogAudioContentDetailMenuBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
dialog = DialogAudioContentDetailMenuBinding.inflate(inflater, container, false)
return dialog.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (isCreator) {
dialog.tvReport.visibility = View.GONE
dialog.llMenuCreator.visibility = View.VISIBLE
if (isPin) {
dialog.ivPin.setImageResource(R.drawable.ic_pin_cancel)
dialog.tvPin.text =
getString(R.string.screen_audio_content_detail_pin_cancel)
} else {
dialog.ivPin.setImageResource(R.drawable.ic_pin)
dialog.tvPin.text = getString(R.string.screen_audio_content_detail_pin)
}
dialog.llPin.setOnClickListener {
dismiss()
onClickPin()
}
dialog.llModify.setOnClickListener {
dismiss()
onClickModify()
}
dialog.llDelete.setOnClickListener {
dismiss()
onClickDelete()
}
} else {
dialog.llMenuCreator.visibility = View.GONE
dialog.tvReport.visibility = View.VISIBLE
dialog.tvReport.setOnClickListener {
dismiss()
onClickReport()
}
}
}
}
@@ -1,33 +1,30 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.net.Uri
import androidx.core.net.toUri
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.firebase.dynamiclinks.ShortDynamicLink
import com.google.firebase.dynamiclinks.ktx.androidParameters
import com.google.firebase.dynamiclinks.ktx.dynamicLinks
import com.google.firebase.dynamiclinks.ktx.iosParameters
import com.google.firebase.dynamiclinks.ktx.shortLinkAsync
import com.google.firebase.dynamiclinks.ktx.socialMetaTagParameters
import com.google.firebase.ktx.Firebase
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
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.user.UserRepository
class AudioContentDetailViewModel(
private val repository: AudioContentRepository,
private val userRepository: UserRepository,
private val authRepository: AuthRepository,
private val reportRepository: ReportRepository,
private val commentRepository: AudioContentCommentRepository
@@ -58,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
}
@@ -79,7 +82,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -93,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()
}
@@ -102,12 +105,14 @@ class AudioContentDetailViewModel(
)
}
fun registerNotification(contentId: Long, creatorId: Long) {
fun follow(contentId: Long, creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
isLoading.value = true
compositeDisposable.add(
repository.registerNotification(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
userRepository.creatorFollow(
creatorId = creatorId,
follow,
notify,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@@ -120,7 +125,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -129,40 +134,7 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun unRegisterNotification(contentId: Long, creatorId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.unRegisterNotification(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getAudioContentDetail(contentId)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
isLoading.value = false
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -176,7 +148,7 @@ class AudioContentDetailViewModel(
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
}
fun order(contentId: Long, orderType: OrderType) {
fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.orderContent(
@@ -190,13 +162,26 @@ class AudioContentDetailViewModel(
{
if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue("구매가 완료되었습니다.")
_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 {
if (it.message != null) {
_toastLiveData.postValue(it.message)
if (it.message.contains("캔이 부족합니다")) {
gotoShop()
}
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -205,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))
}
)
)
@@ -229,7 +214,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -239,13 +224,13 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
}
fun registerComment(audioContentId: Long, comment: String) {
fun registerComment(audioContentId: Long, comment: String, isSecret: Boolean) {
if (!isLoading.value!!) {
isLoading.value = true
}
@@ -254,6 +239,7 @@ class AudioContentDetailViewModel(
commentRepository.registerComment(
contentId = audioContentId,
comment = comment,
isSecret = isSecret,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -267,7 +253,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
@@ -277,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))
}
)
)
@@ -310,7 +296,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -318,44 +304,24 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
}
fun shareAudioContent(
fun shareContent(
audioContentId: Long,
contentImage: String,
contentTitle: String,
onSuccess: (String) -> Unit
) {
isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://sodalive.net/?audio_content_id=$audioContentId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
socialMetaTagParameters {
title = contentTitle
description = "지금 소다라이브에서 이 콘텐츠 감상하기"
imageUrl = contentImage.toUri()
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
val message = uri.toString()
onSuccess(message)
} else {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}.addOnCompleteListener {
isLoading.value = false
}
val params = mapOf(
"af_dp" to "voiceon://",
"deep_link_value" to "content",
"deep_link_sub5" to "$audioContentId"
)
val shareUrl = Utils.createOneLinkUrl(params = params)
onSuccess(shareUrl)
}
fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) {
@@ -374,7 +340,7 @@ class AudioContentDetailViewModel(
if (it.success) {
_toastLiveData.postValue(
"삭제되었습니다."
getString(R.string.screen_audio_content_detail_delete_success)
)
onSuccess()
} else {
@@ -382,7 +348,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -390,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))
}
)
)
@@ -412,7 +378,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"신고가 접수되었습니다."
getString(R.string.screen_audio_content_detail_report_submitted)
)
}
@@ -421,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)
)
}
)
)
@@ -451,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 {
@@ -459,7 +430,7 @@ class AudioContentDetailViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
getString(R.string.common_error_unknown)
)
}
}
@@ -467,7 +438,81 @@ class AudioContentDetailViewModel(
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
}
fun pinContent(audioContentId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.pinContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_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)
)
}
}
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
}
fun unPinContent(audioContentId: Long) {
isLoading.value = true
compositeDisposable.add(
repository.unpinContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_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)
)
}
}
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(getString(R.string.common_error_unknown))
}
)
)
@@ -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()
}
}
@@ -1,9 +1,12 @@
package kr.co.vividnext.sodalive.audio_content.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem
import kr.co.vividnext.sodalive.audio_content.order.OrderType
@Keep
data class GetAudioContentDetailResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@@ -14,9 +17,15 @@ data class GetAudioContentDetailResponse(
@SerializedName("tag") val tag: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("releaseDate") val releaseDate: String?,
@SerializedName("totalContentCount") val totalContentCount: Int?,
@SerializedName("remainingContentCount") val remainingContentCount: Int?,
@SerializedName("orderSequence") val orderSequence: Int?,
@SerializedName("isActivePreview") val isActivePreview: Boolean,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("purchaseOption") val purchaseOption: PurchaseOption,
@SerializedName("orderType") val orderType: OrderType?,
@SerializedName("remainingTime") val remainingTime: String?,
@SerializedName("creatorOtherContentList")
@@ -28,18 +37,42 @@ data class GetAudioContentDetailResponse(
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("creator") val creator: AudioContentCreator
@SerializedName("isPin") val isPin: Boolean,
@SerializedName("isAvailablePin") val isAvailablePin: Boolean,
@SerializedName("creator") val creator: AudioContentCreator,
@SerializedName("previousContent") val previousContent: OtherContentResponse?,
@SerializedName("nextContent") val nextContent: OtherContentResponse?,
@SerializedName("buyerList") val buyerList: List<ContentBuyer>,
@SerializedName("isAvailableUsePoint") val isAvailableUsePoint: Boolean,
@SerializedName("translated") val translated: TranslatedContent?
)
@Keep
data class OtherContentResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverUrl") val coverUrl: String,
@SerializedName("coverUrl") val coverUrl: String
)
@Keep
data class AudioContentCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String,
@SerializedName("isFollowing") val isFollowing: Boolean
@SerializedName("isFollowing") val isFollowing: Boolean,
@SerializedName("isFollow") var isFollow: Boolean,
@SerializedName("isNotify") var isNotify: Boolean
)
@Keep
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?
)
@@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.audio_content.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class PutAudioContentLikeRequest(
@SerializedName("contentId") val contentId: Long
)
@Keep
data class PutAudioContentLikeResponse(
@SerializedName("like") val like: Boolean
)
@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.donation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class AudioContentDonationRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("donationCan") val donationCan: Int,
@@ -1,41 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding
class AudioContentMainContentAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
) : RecyclerView.Adapter<AudioContentMainItemViewHolder>() {
private val items = mutableListOf<GetAudioContentMainItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = AudioContentMainItemViewHolder(
ItemAudioContentMainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
onClickItem = onClickItem,
onClickCreator = onClickCreator
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: AudioContentMainItemViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentMainItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}
@@ -1,94 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentCurationResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainCurationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCurationResponse) {
binding.tvTitle.text = item.title
binding.tvDesc.text = item.description
setAudioContentList(item.audioContents)
}
private fun setAudioContentList(audioContents: List<GetAudioContentMainItem>) {
val adapter = AudioContentMainContentAdapter(onClickItem, onClickCreator)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
if (binding.rvCuration.itemDecorationCount == 0) {
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
}
binding.rvCuration.adapter = adapter
adapter.addItems(audioContents)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainCurationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}
@@ -1,474 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.app.Service
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
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.databinding.FragmentAudioContentMainBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
}
private fun setupView() {
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
setupNewContentCreator()
setupBanner()
setupOrderList()
setupNewContentTheme()
setupNewContent()
setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
}
}
private fun setupNewContentCreator() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 10.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
}
private fun setupBanner() {
val layoutParams = binding
.rvBanner
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
val pagerHeight = (pagerWidth * 0.53).roundToInt()
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(
requireContext(),
pagerWidth.roundToInt(),
pagerHeight
) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
}
)
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.rvBanner
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
}
private fun setupOrderList() {
orderListAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvMyStash.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvMyStash.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvMyStash.adapter = orderListAdapter
binding.tvMyStashViewAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvNewContent.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContent.adapter = newContentAdapter
}
private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 40f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 40f.dpToPx().toInt()
}
else -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
}
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (
curationAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
}
}
@@ -1,35 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainItemViewHolder(
private val binding: ItemAudioContentMainBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvAudioContentTitle.text = item.title
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
@@ -1,52 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding
class AudioContentMainNewContentCreatorAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewContentCreatorAdapter.ViewHolder>() {
private val items = mutableListOf<GetNewContentUploadCreator>()
inner class ViewHolder(
private val binding: ItemAudioContentMainNewContentCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetNewContentUploadCreator) {
binding.tvNewContentCreator.text = item.creatorNickname
binding.ivNewContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.root.setOnClickListener { onClickItem(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentMainNewContentCreatorBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetNewContentUploadCreator>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}
@@ -1,121 +0,0 @@
package kr.co.vividnext.sodalive.audio_content.main
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.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _bannerLiveData
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _orderListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
fun getMain() {
_isLoading.value = true
compositeDisposable.add(
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_newContentUploadCreatorListLiveData.value =
data.newContentUploadCreatorList
_newContentListLiveData.value = data.newContentList
_orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getNewContentOfTheme(theme: String) {
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
@@ -1,45 +1,58 @@
package kr.co.vividnext.sodalive.audio_content.main
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.settings.event.EventItem
data class GetAudioContentMainResponse(
@SerializedName("newContentUploadCreatorList")
val newContentUploadCreatorList: List<GetNewContentUploadCreator>,
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
@SerializedName("orderList") val orderList: List<GetAudioContentMainItem>,
@SerializedName("themeList") val themeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>
)
data class GetNewContentUploadCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean
)
@Keep
data class GetAudioContentRanking(
@SerializedName("startDate") val startDate: String,
@SerializedName("endDate") val endDate: String,
@SerializedName("items") val items: List<GetAudioContentRankingItem>
)
@Keep
data class GetAudioContentRankingItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class GetAudioContentCurationResponse(
@SerializedName("curationId") val curationId: Long,
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
)
@Keep
data class GetAudioContentBannerResponse(
@SerializedName("type") val type: AudioContentBannerType,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
@SerializedName("eventItem") val eventItem: EventItem?,
@SerializedName("creatorId") val creatorId: Long?,
@SerializedName("seriesId") val seriesId: Long?,
@SerializedName("link") val link: String?
)
@@ -51,5 +64,8 @@ enum class AudioContentBannerType {
CREATOR,
@SerializedName("LINK")
LINK
LINK,
@SerializedName("SERIES")
SERIES
}
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.banner
import android.content.Context
import android.graphics.Bitmap
@@ -11,6 +11,7 @@ import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
class AudioContentMainBannerAdapter(
private val context: Context,
@@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.audio_content.main
package kr.co.vividnext.sodalive.audio_content.main.new_content
import android.annotation.SuppressLint
import android.content.Context
@@ -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(
@@ -22,11 +23,22 @@ class AudioContentMainNewContentThemeAdapter(
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) {
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
)
)
) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
@@ -45,7 +57,6 @@ class AudioContentMainNewContentThemeAdapter(
@SuppressLint("NotifyDataSetChanged")
fun addItems(themeList: List<String>) {
this.selectedTheme = ""
this.themeList.clear()
this.themeList.addAll(themeList)
notifyDataSetChanged()
@@ -2,16 +2,18 @@ package kr.co.vividnext.sodalive.audio_content.modify
import android.Manifest
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import coil.load
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
@@ -20,8 +22,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
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
@@ -33,42 +37,18 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
private val viewModel: AudioContentModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
viewModel.coverImageUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
private lateinit var cropper: ImagePickerCropper
override fun onCreate(savedInstanceState: Bundle?) {
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()
}
@@ -82,24 +62,54 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onDestroy() {
cropper.cleanup()
super.onDestroy()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
cropper = ImagePickerCropper(
caller = this,
context = this,
excludeGif = true,
isEnabledFreeStyleCrop = true,
config = ImagePickerCropper.Config(
aspectX = 1f,
aspectY = 1f,
compressFormat = Bitmap.CompressFormat.JPEG,
compressQuality = 90
),
onSuccess = { file, uri ->
binding.ivCover.setPadding(0)
binding.ivCover.background = null
Glide.with(this)
.load(uri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
13.3f.dpToPx().toInt()
)
)
)
.into(binding.ivCover)
viewModel.coverImageFile = file
},
onError = { e ->
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
}
)
binding.toolbar.tvBack.text = "콘텐츠 수정"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
binding.llAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(true) }
binding.llNotAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(false) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
@@ -112,7 +122,7 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
private fun checkPermissions() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
listOf(Manifest.permission.READ_MEDIA_AUDIO)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
@@ -152,6 +162,15 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
}
)
compositeDisposable.add(
binding.etTag.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.tags = it.toString()
}
)
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
@@ -164,6 +183,14 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
}
}
viewModel.isAvailablePointLiveData.observe(this) {
if (it) {
checkAvailablePoint()
} else {
checkNotAvailablePoint()
}
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
@@ -173,17 +200,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
R.drawable.bg_round_corner_6_7_13181b_3bb9f1
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
@@ -193,17 +220,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b_3bb9f1)
}
}
@@ -219,21 +246,21 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
viewModel.isAdultLiveData.observe(this) { isAdult ->
if (isAdult) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734
R.drawable.bg_round_corner_6_7_13181b
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
@@ -242,17 +269,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
R.color.color_3bb9f1
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff
R.drawable.bg_round_corner_6_7_3bb9f1
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
@@ -284,5 +311,53 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
viewModel.detailLiveData.observe(this) {
binding.etDetail.setText(it)
}
viewModel.tagsLiveData.observe(this) {
binding.etTag.setText(it)
}
}
private fun checkAvailablePoint() {
binding.ivAvailablePoint.visibility = View.VISIBLE
binding.tvAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivNotAvailablePoint.visibility = View.GONE
binding.tvNotAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llNotAvailablePoint.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
private fun checkNotAvailablePoint() {
binding.ivNotAvailablePoint.visibility = View.VISIBLE
binding.tvNotAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llNotAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivAvailablePoint.visibility = View.GONE
binding.tvAvailablePoint.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.llAvailablePoint.setBackgroundResource(
R.drawable.bg_round_corner_6_7_13181b
)
}
}
@@ -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
@@ -46,6 +48,10 @@ class AudioContentModifyViewModel(
val detailLiveData: LiveData<String>
get() = _detailLiveData
private val _tagsLiveData = MutableLiveData("")
val tagsLiveData: LiveData<String>
get() = _tagsLiveData
private val _coverImageLiveData = MutableLiveData("")
val coverImageLiveData: LiveData<String>
get() = _coverImageLiveData
@@ -54,12 +60,18 @@ class AudioContentModifyViewModel(
val isAdultShowUiLiveData: LiveData<Boolean>
get() = _isAdultShowUiLiveData
private val _isAvailablePointLiveData = MutableLiveData(false)
val isAvailablePointLiveData: LiveData<Boolean>
get() = _isAvailablePointLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var contentId: Long = 0
var title: String? = null
var detail: String? = null
var coverImageUri: Uri? = null
var tags: String? = null
var coverImageFile: File? = null
var isPointAvailable: Boolean? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
@@ -69,6 +81,11 @@ class AudioContentModifyViewModel(
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun setAvailablePoint(isAvailablePoint: Boolean) {
isPointAvailable = isAvailablePoint
_isAvailablePointLiveData.value = isAvailablePoint
}
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
this.contentId = audioContentId
_isLoading.value = true
@@ -85,16 +102,19 @@ class AudioContentModifyViewModel(
if (it.success && it.data != null) {
_titleLiveData.value = it.data.title
_detailLiveData.value = it.data.detail
_tagsLiveData.value = it.data.tag
_coverImageLiveData.value = it.data.coverImageUrl
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
_isAdultLiveData.value = it.data.isAdult
_isAdultShowUiLiveData.value = !it.data.isAdult
_isAvailablePointLiveData.value = it.data.isAvailableUsePoint
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
@@ -108,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()
}
@@ -125,14 +148,20 @@ class AudioContentModifyViewModel(
contentId = contentId,
title = title,
detail = detail,
tags = if (tags != _tagsLiveData.value!!) {
tags
} else {
null
},
isAdult = _isAdultLiveData.value!!,
isPointAvailable = isPointAvailable,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
val coverImage = if (coverImageFile != null) {
val file = coverImageFile!!
MultipartBody.Part.createFormData(
"coverImage",
file.name,
@@ -178,7 +207,8 @@ class AudioContentModifyViewModel(
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
@@ -188,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)
)
}
)
@@ -198,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
}
@@ -1,11 +1,15 @@
package kr.co.vividnext.sodalive.audio_content.modify
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class ModifyAudioContentRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String?,
@SerializedName("detail") val detail: String?,
@SerializedName("tags") val tags: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean?,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
)
@@ -4,15 +4,17 @@ import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kotlin.math.ceil
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentOrderConfirmDialog(
activity: Activity,
@@ -25,7 +27,8 @@ class AudioContentOrderConfirmDialog(
duration: String,
orderType: OrderType,
price: Int,
confirmButtonClick: () -> Unit,
isAvailableUsePoint: Boolean,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog
@@ -33,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)
@@ -57,18 +61,72 @@ class AudioContentOrderConfirmDialog(
}
dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL) {
"${ceil(price * 0.6).toInt()}"
val maxUsablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
price * 10
} else {
"$price"
0
}
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
val totalAvailablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
SharedPreferenceManager.point
} else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
0
}
val usablePoint = (minOf(totalAvailablePoint, maxUsablePoint) / 10) * 10
if (SharedPreferenceManager.userId == 17958L) {
dialogView.ivPoint.visibility = View.GONE
dialogView.tvPoint.visibility = View.GONE
dialogView.tvPlus.visibility = View.GONE
dialogView.ivCan.visibility = View.GONE
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
dialogView.tvPoint.visibility = View.VISIBLE
dialogView.tvPoint.text = usablePoint.moneyFormat()
} else {
dialogView.ivPoint.visibility = View.GONE
dialogView.tvPoint.visibility = View.GONE
}
val remainingCan = ((price * 10) - usablePoint) / 10
dialogView.tvPlus.visibility = if (usablePoint > 0 && remainingCan > 0) {
View.VISIBLE
} else {
View.GONE
}
if (remainingCan > 0) {
dialogView.ivCan.visibility = View.VISIBLE
dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCan.text = remainingCan.moneyFormat()
} else {
dialogView.ivCan.visibility = View.GONE
dialogView.tvCan.visibility = View.GONE
}
}
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()
}
@@ -5,7 +5,10 @@ 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
import kotlin.math.ceil
class AudioContentOrderFragment(
@@ -28,9 +31,34 @@ class AudioContentOrderFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
val context = requireContext()
if (SharedPreferenceManager.userId == 17958L) {
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 =
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 = 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()
}
binding.rlKeep.visibility = View.VISIBLE
binding.llKeep.setOnClickListener {
onClickKeep()
dismiss()
@@ -1,110 +1,28 @@
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.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.R
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
import org.koin.android.ext.android.inject
class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderListBinding>(
ActivityAudioContentOrderListBinding::inflate
) {
private val viewModel: AudioContentOrderListViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentOrderListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
viewModel.getAudioContentOrderList { finish() }
supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, AudioContentOrderListFragment())
.commit()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "구매목록"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = AudioContentOrderListAdapter {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
)
}
binding.rvOrderList.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvOrderList.adapter = adapter
setupToolbar()
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.orderList.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
private fun setupToolbar() {
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_order_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
}
}
@@ -0,0 +1,128 @@
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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentOrderListFragment : BaseFragment<FragmentAudioContentOrderListBinding>(
FragmentAudioContentOrderListBinding::inflate
) {
private val viewModel: AudioContentOrderListViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentOrderListAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.getAudioContentOrderList { requireActivity().finish() }
}
fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
adapter = AudioContentOrderListAdapter {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java)
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
)
}
binding.rvOrderList.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
// 스크롤이 끝에 도달했는지 확인
if (!recyclerView.canScrollVertically(1) &&
lastVisibleItemPosition == itemTotalCount
) {
viewModel.getAudioContentOrderList {}
}
}
})
binding.rvOrderList.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { showToast(it) }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.orderList.observe(viewLifecycleOwner) {
if (viewModel.page == 2) {
adapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.totalCount.observe(viewLifecycleOwner) {
binding.tvTotalCount.text = it.toString()
}
}
}
@@ -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(
@@ -25,53 +27,60 @@ class AudioContentOrderListViewModel(
val orderList: LiveData<List<GetAudioContentOrderListItem>>
get() = _orderList
private var _totalCount = MutableLiveData<Int>()
val totalCount: LiveData<Int>
get() = _totalCount
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
if (_isLoading.value == false) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_totalCount.value = it.data.totalCount
page += 1
_orderList.postValue(it.data.items)
if (it.data.items.isNotEmpty()) {
_orderList.postValue(it.data.items)
} else {
_orderList.postValue(listOf())
isLast = true
}
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(unknownError)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(unknownError)
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
)
)
}
}
}
@@ -1,12 +1,15 @@
package kr.co.vividnext.sodalive.audio_content.order
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetAudioContentOrderListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentOrderListItem>
)
@Keep
data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@@ -17,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
)
@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.order
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class OrderRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("orderType") val orderType: OrderType,
@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.player
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
class AudioContentGenerateUrlRepository(private val api: AudioContentApi) {
fun generateUrl(contentId: Long, token: String) = api.generateUrl(
contentId = contentId,
authHeader = token
)
}
@@ -0,0 +1,497 @@
package kr.co.vividnext.sodalive.audio_content.player
import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
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.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailAdapter
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.Utils
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlayerBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@UnstableApi
class AudioContentPlayerFragment(
private val screenWidth: Int,
private val playlist: ArrayList<AudioContentPlaylistContent>
) : BottomSheetDialogFragment() {
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentPlaylistDetailAdapter
private lateinit var binding: FragmentAudioContentPlayerBinding
private val viewModel: AudioContentPlayerViewModel by viewModel()
private val recentContentViewModel: RecentContentViewModel by inject()
private var mediaController: MediaController? = null
private val handler = Handler(Looper.getMainLooper())
private var isUserSeeking = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setOnShowListener {
val bottomSheet = dialog.findViewById<View>(
com.google.android.material.R.id.design_bottom_sheet
)
bottomSheet?.let {
val layoutParams = it.layoutParams
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
it.layoutParams = layoutParams
// BottomSheet를 전체 화면으로 설정
val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
behavior.isDraggable = false
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAudioContentPlayerBinding.inflate(
inflater,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
connectPlayerService()
}
override fun onDestroyView() {
handler.removeCallbacksAndMessages(null)
mediaController?.release()
mediaController = null
super.onDestroyView()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
binding.ivClose.setOnClickListener { dismiss() }
binding.ivPlaylist.setOnClickListener {
viewModel.toggleShowPlayList()
}
adapter = AudioContentPlaylistDetailAdapter { contentId ->
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val extras = Bundle().apply {
putLong(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
val sessionCommand = SessionCommand("PLAY_SELECTED_CONTENT", Bundle.EMPTY)
mediaController!!.sendCustomCommand(sessionCommand, extras)
}
val recyclerView = binding.rvPlaylistContent
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 0
outRect.right = 0
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
binding.ivLoopSegment.setOnClickListener {
val sessionCommand = SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY)
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
resultFuture.addListener(
{
val result = resultFuture.get()
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
val imageRes = result.extras.getInt(
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
R.drawable.ic_loop_segment_idle
)
binding.ivLoopSegment.setImageResource(imageRes)
}
},
ContextCompat.getMainExecutor(requireContext())
)
}
}
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show()
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(
screenWidth,
""
)
} else {
loadingDialog.dismiss()
}
}
viewModel.isShowPlaylistLiveData.observe(viewLifecycleOwner) {
if (it) {
binding.ivCover.visibility = View.GONE
binding.tvTitle.visibility = View.GONE
binding.ivCreatorProfile.visibility = View.GONE
binding.tvCreatorNickname.visibility = View.GONE
binding.rvPlaylistContent.visibility = View.VISIBLE
binding.ivPlaylist.setBackgroundResource(
R.drawable.bg_round_corner_6_7_cc333333
)
} else {
binding.ivCover.visibility = View.VISIBLE
binding.tvTitle.visibility = View.VISIBLE
binding.ivCreatorProfile.visibility = View.VISIBLE
binding.tvCreatorNickname.visibility = View.VISIBLE
binding.rvPlaylistContent.visibility = View.GONE
binding.ivPlaylist.setBackgroundResource(0)
}
}
}
private fun connectPlayerService() {
context?.let {
if (!SharedPreferenceManager.isPlayerServiceRunning) {
startPlayerService(context = it)
}
view?.postDelayed({
connectToMediaSession(it)
}, 500)
}
}
private fun startPlayerService(context: Context) {
val serviceIntent = Intent(context, AudioContentPlayerService::class.java)
context.startService(serviceIntent)
}
private fun connectToMediaSession(context: Context) {
val componentName = ComponentName(context, AudioContentPlayerService::class.java)
val sessionToken = SessionToken(context, componentName)
val mediaControllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
mediaControllerFuture.addListener(
{
mediaController = mediaControllerFuture.get()
setupMediaController()
updatePlayerUI()
startUpdatingUI()
},
ContextCompat.getMainExecutor(context)
)
}
private fun setupMediaController() {
if (mediaController == null) {
Toast.makeText(
requireContext(),
getString(R.string.audio_content_player_error_launch_failed),
Toast.LENGTH_LONG
).show()
dismiss()
return
}
mediaController!!.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
mediaController?.let {
when (playbackState) {
Player.STATE_ENDED -> {
it.seekTo(0)
it.pause()
}
else -> {}
}
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
updateMediaMetadata(mediaItem?.mediaMetadata)
updateTimeUI()
}
override fun onIsLoadingChanged(isLoading: Boolean) {
viewModel.setLoading(isLoading)
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
binding.ivPlayOrPause.setImageResource(
if (playWhenReady) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
}
})
if (playlist.isNotEmpty()) {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val extras = Bundle().apply {
putParcelableArrayList(
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
playlist
)
}
val sessionCommand = SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY)
mediaController!!.sendCustomCommand(sessionCommand, extras)
adapter.updateItems(playlist)
} else {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
context?.let {
val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY)
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
resultFuture.addListener(
{
val result = resultFuture.get()
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
val data = BundleCompat.getParcelableArrayList(
result.extras,
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
AudioContentPlaylistContent::class.java
)
playlist.clear()
playlist.addAll(data ?: listOf())
adapter.updateItems(data ?: listOf())
}
},
ContextCompat.getMainExecutor(it)
)
}
}
}
private fun updatePlayerUI() {
binding.ivPlayOrPause.setImageResource(
if (mediaController!!.isPlaying) {
R.drawable.ic_player_pause
} else {
R.drawable.ic_player_play
}
)
binding.ivPlayOrPause.setOnClickListener {
if (!SharedPreferenceManager.isPlayerServiceRunning) {
mediaController = null
connectPlayerService()
} else {
mediaController?.let {
if (it.playWhenReady) {
it.pause()
} else {
it.play()
}
}
}
}
binding.ivSkipForward.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"PLAY_NEXT_CONTENT",
Bundle.EMPTY
)
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
}
}
binding.ivSkipBack.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"PLAY_PREVIOUS_CONTENT",
Bundle.EMPTY
)
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
}
}
binding.ivSeekForward10.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"SEEK_FORWARD",
Bundle.EMPTY
)
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
}
}
binding.ivSeekBackward10.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"SEEK_BACKWARD",
Bundle.EMPTY
)
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
}
}
binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar?,
progress: Int,
fromUser: Boolean
) {
if (fromUser) {
isUserSeeking = true
}
}
override fun onStartTrackingTouch(p0: SeekBar?) {
isUserSeeking = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
isUserSeeking = false
seekBar?.progress?.let { progress ->
mediaController?.seekTo(progress.toLong())
}
}
})
updateMediaMetadata(mediaController?.currentMediaItem?.mediaMetadata)
updateTimeUI()
}
private fun updateMediaMetadata(metadata: MediaMetadata?) {
metadata?.let {
binding.tvTitle.text = it.title
binding.tvCreatorNickname.text = it.artist
binding.ivCreatorProfile.load(
it.extras?.getString(Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE)
) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.ivCover.load(it.artworkUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
val contentId = it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
adapter.updateCurrentPlayingId(contentId)
// Save to recent content
contentId?.let { id ->
val recentContent = RecentContent(
contentId = id,
coverImageUrl = it.artworkUri.toString(),
title = it.title.toString(),
creatorNickname = it.artist.toString()
)
recentContentViewModel.insertRecentContent(recentContent)
}
}
}
private fun updateTimeUI() {
mediaController?.let {
val duration = it.duration
val currentPosition = it.currentPosition
binding.sbProgress.max = duration.toInt()
binding.sbProgress.progress = currentPosition.toInt()
binding.tvTotalTime.text = Utils.convertDurationToString(duration.toInt())
binding.tvProgressTime.text = Utils.convertDurationToString(currentPosition.toInt())
}
}
private fun startUpdatingUI() {
handler.post(object : Runnable {
override fun run() {
if (mediaController?.isPlaying == true && !isUserSeeking) {
updateTimeUI()
}
handler.postDelayed(this, 500)
}
})
}
}
@@ -0,0 +1,447 @@
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
package kr.co.vividnext.sodalive.audio_content.player
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.core.os.BundleCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.ts.AdtsExtractor
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
import org.koin.android.ext.android.inject
@UnstableApi
class AudioContentPlayerService : MediaSessionService() {
private val repository: AudioContentGenerateUrlRepository by inject()
private var playlistManager: AudioContentPlaylistManager? = null
private var mediaSession: MediaSession? = null
private var player: ExoPlayer? = null
val compositeDisposable = CompositeDisposable()
private val playlist = ArrayList<AudioContentPlaylistContent>()
private var loopStartMs: Long? = null
private var loopEndMs: Long? = null
private var isLooping = false
private val loopHandler = Handler(Looper.getMainLooper())
private val loopCheckInterval = 100L // 0.1초 간격
override fun onCreate() {
super.onCreate()
try {
initPlayer()
initMediaSession()
} catch (e: Exception) {
onStopService()
return
}
}
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
compositeDisposable.dispose()
SharedPreferenceManager.isPlayerServiceRunning = false
stopLoop()
super.onDestroy()
}
override fun onTaskRemoved(rootIntent: Intent?) {
onStopService()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == "STOP_SERVICE") {
onStopService()
}
return super.onStartCommand(intent, flags, startId)
}
private fun toggleSegmentLoop() {
when {
loopStartMs == null -> {
loopStartMs = player?.currentPosition
}
loopEndMs == null -> {
loopEndMs = player?.currentPosition
isLooping = true
startLoopMonitoring()
}
else -> {
stopLoop()
}
}
}
private fun startLoopMonitoring() {
loopHandler.post(object : Runnable {
override fun run() {
if (isLooping && loopStartMs != null && loopEndMs != null && player != null) {
if (player!!.currentPosition >= loopEndMs!!) {
player!!.seekTo(loopStartMs!!)
}
loopHandler.postDelayed(this, loopCheckInterval)
}
}
})
}
private fun stopLoop() {
isLooping = false
loopStartMs = null
loopEndMs = null
loopHandler.removeCallbacksAndMessages(null)
}
private fun initPlayer() {
player = ExoPlayer.Builder(this).build()
player!!.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
player?.play()
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_IDLE) {
onStopService()
} else if (playbackState == Player.STATE_ENDED) {
if (playlistManager!!.hasNextContent()) {
playNextContent()
}
}
}
})
}
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,
0,
contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mediaSession = MediaSession.Builder(this, player!!)
.setSessionActivity(pendingIntent)
.setCallback(object : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
SharedPreferenceManager.isPlayerServiceRunning = true
val allowedCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS
.buildUpon()
.add(SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY))
.add(SessionCommand("PLAY_NEXT_CONTENT", Bundle.EMPTY))
.add(SessionCommand("PLAY_PREVIOUS_CONTENT", Bundle.EMPTY))
.add(SessionCommand("PLAY_SELECTED_CONTENT", Bundle.EMPTY))
.add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY))
.add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY))
.add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY))
.add(SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY))
.build()
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(allowedCommands)
.build()
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
return when (customCommand.customAction) {
"UPDATE_PLAYLIST" -> {
stopLoop()
val playlist = BundleCompat.getParcelableArrayList(
args,
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
AudioContentPlaylistContent::class.java
)
if (playlist != null) {
this@AudioContentPlayerService.playlist.addAll(playlist)
playlistManager = AudioContentPlaylistManager(playlist)
playNextContent()
}
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"PLAY_NEXT_CONTENT" -> {
stopLoop()
playNextContent()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"PLAY_PREVIOUS_CONTENT" -> {
stopLoop()
playPreviousContent()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"PLAY_SELECTED_CONTENT" -> {
stopLoop()
val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
playSelectedContent(contentId = selectedContentId)
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"SEEK_FORWARD" -> {
stopLoop()
playSeekForward()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"SEEK_BACKWARD" -> {
stopLoop()
playSeekBackward()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"TOGGLE_SEGMENT_LOOP" -> {
val extras = Bundle().apply {
putInt(
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
when {
loopStartMs == null -> {
R.drawable.ic_loop_segment_start_set
}
loopEndMs == null -> {
R.drawable.ic_loop_segment_active
}
else -> {
R.drawable.ic_loop_segment_idle
}
}
)
}
toggleSegmentLoop()
Futures.immediateFuture(
SessionResult(
SessionResult.RESULT_SUCCESS,
extras
)
)
}
"GET_PLAYLIST" -> {
val extras = Bundle().apply {
putParcelableArrayList(
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
playlist
)
}
Futures.immediateFuture(
SessionResult(
SessionResult.RESULT_SUCCESS,
extras
)
)
}
else -> super.onCustomCommand(session, controller, customCommand, args)
}
}
})
.build()
}
private fun onStopService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
mediaSession?.run {
player.release()
release()
mediaSession = null
}
stopSelf()
}
private fun playNextContent() {
val content = playlistManager?.moveToNext()
if (content != null) {
generateUrl(
content.id,
onSuccess = { urlGenerateSuccess(content, it) },
onFailure = { playNextContent() }
)
}
}
private fun playSeekForward() {
val currentPosition = player!!.currentPosition
val duration = player!!.duration
var newPosition = currentPosition + SEEK_INTERVAL_MS
if (newPosition > duration) {
newPosition = duration
}
player!!.seekTo(newPosition)
}
private fun playSeekBackward() {
val currentPosition = player!!.currentPosition
var newPosition = currentPosition - SEEK_INTERVAL_MS
if (newPosition < 0) {
newPosition = 0
}
player!!.seekTo(newPosition)
}
private fun urlGenerateSuccess(
content: AudioContentPlaylistContent,
contentUrl: String
) {
val extras = Bundle().apply {
putString(
Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE,
content.creatorProfileUrl
)
putLong(
Constants.EXTRA_AUDIO_CONTENT_ID,
content.id
)
}
val mediaMetadata = MediaMetadata.Builder()
.setTitle(content.title)
.setArtist(content.creatorNickname)
.setArtworkUri(Uri.parse(content.coverUrl))
.setExtras(extras)
.setDurationMs(Utils.convertStringToDuration(content.duration))
.build()
val mediaItem = MediaItem.Builder()
.setUri(Uri.parse(contentUrl))
.setMediaMetadata(mediaMetadata)
.build()
val extractorFactory = DefaultExtractorsFactory().setAdtsExtractorFlags(
AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
)
val mediaSource = ProgressiveMediaSource.Factory(
DefaultDataSource.Factory(this),
extractorFactory
).createMediaSource(mediaItem)
player?.setMediaSource(mediaSource)
player?.prepare()
}
private fun playPreviousContent() {
val content = playlistManager?.moveToPrevious()
if (content != null) {
generateUrl(
content.id,
onSuccess = { urlGenerateSuccess(content, it) },
onFailure = { playPreviousContent() }
)
}
}
private fun playSelectedContent(contentId: Long) {
val content = playlistManager?.findByContentId(contentId = contentId)
if (content != null) {
generateUrl(
content.id,
onSuccess = { urlGenerateSuccess(content, it) },
onFailure = { playPreviousContent() }
)
}
}
private fun generateUrl(contentId: Long, onSuccess: (String) -> Unit, onFailure: () -> Unit) {
if (contentId <= 0) {
onFailure()
}
compositeDisposable.add(
repository.generateUrl(
contentId = contentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null && it.data.contentUrl.isNotBlank()) {
onSuccess(it.data.contentUrl)
} else {
onFailure()
}
},
{
it.message?.let { message -> Logger.e(message) }
onFailure()
}
)
)
}
companion object {
private const val SEEK_INTERVAL_MS = 10 * 1000
}
}
@@ -0,0 +1,27 @@
package kr.co.vividnext.sodalive.audio_content.player
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kr.co.vividnext.sodalive.base.BaseViewModel
class AudioContentPlayerViewModel : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _isShowPlaylistLiveData = MutableLiveData(false)
val isShowPlaylistLiveData: LiveData<Boolean>
get() = _isShowPlaylistLiveData
fun setLoading(loading: Boolean) {
_isLoading.value = loading
}
fun toggleShowPlayList() {
_isShowPlaylistLiveData.value = !_isShowPlaylistLiveData.value!!
}
}

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