Compare commits

142 Commits

Author SHA1 Message Date
Yu Sung
ed9c2d9d32 라인 로그인 - 사용하지 않는 profile 스코프 제거 2026-01-29 02:07:50 +09:00
Yu Sung
27720cfb65 라인 앱에서 로그인 후 앱으로 돌아오기 위한 line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER) URL 스키마를 추가 2026-01-29 01:53:30 +09:00
Yu Sung
11dede49e2 애플 로그인 Capabilities 추가 2026-01-29 01:27:48 +09:00
Yu Sung
5cd0ea9bc2 x 로그인 버튼 제거, 언어에 관계없이 Line 로그인 버튼 보이도록 수정 2026-01-28 23:18:59 +09:00
Yu Sung
42e375ec4b LINE 로그인 지원 추가
LINE 로그인 요청과 토큰 처리 흐름을 추가함
2026-01-28 19:05:42 +09:00
Yu Sung
5e85b1d679 로그인 화면 일본어 SNS 아이콘 분기 추가 2026-01-28 15:57:25 +09:00
Yu Sung
5e209662d8 카카오 로그인 기능 추가 2026-01-27 16:18:54 +09:00
Yu Sung
b522b50dee 구글 로그인 기능 추가 2026-01-27 13:56:13 +09:00
Yu Sung
95cae7d0bf 애플 로그인 기능 추가 2026-01-27 10:45:00 +09:00
Yu Sung
7e37fc6f57 카카오, 애플 로그인 버튼 추가 2026-01-26 21:00:17 +09:00
Yu Sung
d0a17739a1 일본어 폰트 NotoSansJP로 변경 2026-01-26 11:37:40 +09:00
Yu Sung
091f90dee6 gmarket_sans 폰트 제거 2026-01-26 10:20:18 +09:00
Yu Sung
e100bcb593 권한 문구 다국어 추가 2026-01-23 06:41:44 +09:00
Yu Sung
7825f54a01 언어설정 페이지 "적용" 버튼 다국어 적용 2026-01-23 06:26:39 +09:00
Yu Sung
aa29acad0e 라이브 전체보기 국제화 2026-01-23 06:22:23 +09:00
Yu Sung
fb6b623564 닉네임 변경 화면 국제화 2026-01-23 06:13:02 +09:00
Yu Sung
791ebb0242 비밀번호 변경 화면 국제화 2026-01-23 06:05:45 +09:00
Yu Sung
0a40bdbee8 보관함 문자열 국제화 적용 2026-01-23 06:01:13 +09:00
Yu Sung
7cdb46dd8a 캔 일본어 : 缶 -> can 로 수정 2026-01-23 05:32:20 +09:00
Yu Sung
02d767d5d8 검색 화면 하드 코딩된 텍스트 다국어 전환 2026-01-23 05:27:26 +09:00
Yu Sung
9533b06d1e 나머지 .font를 .appFont로 변경하여 한국어 텍스트와 다른 언어 텍스트에 다른 폰트 적용 2026-01-23 03:46:11 +09:00
Yu Sung
b3331d5512 커스텀 폰트 pretendard-regular, gmarket-regular를 사용하고 있던 것을 appFont 모디파이어를 사용하여 한국어는 pretendard, 그 외에는 시스템 폰트를 사용하도록 수정 2026-01-23 03:25:19 +09:00
Yu Sung
c9c1db39a6 커스텀 폰트 pretendard-light, gmarket-light를 사용하고 있던 것을 appFont 모디파이어를 사용하여 한국어는 pretendard, 그 외에는 시스템 폰트를 사용하도록 수정 2026-01-23 03:13:31 +09:00
Yu Sung
280e424385 커스텀 폰트 pretendard-medium, gmarket-medium를 사용하고 있던 것을 appFont 모디
파이어를 사용하여 한국어는 pretendard, 그 외에는 시스템 폰트를 사용하도록 수정
2026-01-23 03:09:20 +09:00
Yu Sung
d92dcbc696 커스텀 폰트 pretendard-bold, gmarket-bold를 사용하고 있던 것을 appFont 모디파이어를 사용하여 한국어는 pretendard, 그 외에는 시스템 폰트를 사용하도록 수정 2026-01-23 02:57:23 +09:00
Yu Sung
5a344956e3 KoreanFontModifier 적용 2026-01-23 02:45:36 +09:00
Yu Sung
87c66136ac 앱 언어 설정이 한국어("ko")이면 Pretendard 폰트 그 외에는 시스템 폰트하는 FontModifier 추가 2026-01-23 02:44:18 +09:00
Yu Sung
4b9cdeb824 인기 크리에이터 팔로우 해제 시 확인 다이얼로그 추가 2026-01-22 19:04:48 +09:00
Yu Sung
75452f0ffd 라이브 예약 완료 화면의 날짜 표시 로직 수정
MakeLiveReservationResponse의 필드 변경 사항을 반영하여 날짜 표시
로직을 수정함. UTC 시간을 디바이스 타임존으로 변환하고
yyyy.MM.dd E hh:mm a 포맷으로 표시함.
2026-01-21 18:56:32 +09:00
Yu Sung
9223e26a07 라이브 예약 표시를 UTC 기준으로 변경 2026-01-21 17:27:26 +09:00
Yu Sung
a743fecfdd 홈 인기 크리에이터 - 닉네임 UI 높이 조정 2026-01-20 01:50:17 +09:00
Yu Sung
d3037d1ba3 크리에이터 채널 - 팔로우/팔로잉 버튼 로컬 라이징 적용 2026-01-20 00:17:42 +09:00
Yu Sung
f1353fc2e6 콘텐츠 상세 미리듣기 버튼 변경 2026-01-19 21:18:14 +09:00
Yu Sung
68dab028cc 홈 - 크리에이터 랭킹 뱃지 변경 2026-01-19 20:07:23 +09:00
Yu Sung
4d893a2081 채널 상세 - 팔로워 수 천 단위 콤마 표시 2026-01-19 19:02:52 +09:00
Yu Sung
d9861b4612 스플래시 화면 변경 2026-01-19 13:56:33 +09:00
Yu Sung
a34c050d25 큰하트 애니메이션 대기열 처리 추가 2026-01-16 15:15:59 +09:00
Yu Sung
c84227008c 지금 라이브 중 전체보기 그리드뷰 UI 3단 구성에서 2단구성으로 변경 2026-01-14 11:55:27 +09:00
Yu Sung
4354530372 인증 완료 후 동작 실행 2026-01-14 11:31:08 +09:00
Yu Sung
1b583eabba 일본어 문구를 정리한다
일본어 로컬라이즈 문구를 최신화한다.

신고 화면에서 공용 신고 사유 문구를 제공한다.
2026-01-09 15:18:24 +09:00
Yu Sung
eb83d5e5e3 라이브룸 버튼 문구 다국어 키 추가
I18n.LiveRoom에 라이브 종료/나가기 등 버튼 문구 키를
추가한다.
일부 안내 및 단위 문구의 영문/일문 표현을 다듬는다.
2026-01-05 15:38:49 +09:00
Yu Sung
fa4483cd18 현재 로케일과 시간대 적용 2026-01-05 14:53:20 +09:00
Yu Sung
f855ca55ca 라이브 취소 안내 문구와 강조 색상 조정 2026-01-05 14:21:36 +09:00
Yu Sung
e7c8c2a12e 라이브 취소 사유 플레이스홀더 현지화 2026-01-05 11:51:46 +09:00
Yu Sung
2f8331f2ff 라이브 시작시간 UTC 적용
라이브 상세와 수정 화면에서 UTC 기준 시간을 표시한다.
날짜 표기는 OS 언어 설정의 기본 포맷을 사용한다.
2026-01-05 11:36:26 +09:00
Yu Sung
921c7e008c 영문 리소스와 스플래시 문구 갱신 2025-12-30 19:24:57 +09:00
Yu Sung
7775e4f0d9 일본어 방송 공지 문구 수정 2025-12-29 17:06:51 +09:00
Yu Sung
19380ccc70 다이얼로그 문구를 공통 번역으로 제공
콘텐츠, 라이브, 스플래시 화면의 안내 문구를
공통 번역 키로 제공해 지역화 품질을 개선한다
2025-12-29 16:44:55 +09:00
Yu Sung
64bb5668f4 일본어 로컬라이즈 문구 갱신 2025-12-26 19:29:36 +09:00
Yu Sung
c6da15f42c Merge branch 'main' into feature/i18n 2025-12-26 18:25:01 +09:00
Yu Sung
fba3940a94 스플래시 화면 변경 2025-12-26 15:22:28 +09:00
Yu Sung
57f24f2481 일본어 로컬라이즈 문구 갱신 2025-12-26 14:20:14 +09:00
Yu Sung
7c1d57676c 캔 단위 표기 현지화
캔 관련 텍스트가 앱 언어에 맞게 변환된다.
2025-12-20 00:15:57 +09:00
Yu Sung
6a7497af7d 인 앱 결제 라벨을 다국어 문자열로 교체 2025-12-20 00:02:29 +09:00
Yu Sung
dd51a3fc2e 완료 방송 상대 시간 표시
최근 종료 방송 카드에 UTC 기준 상대 시간 문자열을 표시한다.
2025-12-19 23:52:43 +09:00
Yu Sung
7307e5b255 메시지 현지화와 종료 라이브 날짜 추가
문자 메시지 화면에 새 현지화 문구를 추가한다.
2025-12-19 23:31:14 +09:00
Yu Sung
dbc4e40904 채팅 화면과 비밀번호 재설정 현지화 추가
채팅방 잠금 및 초기화 안내 문구와 설정, 비밀번호 재설정\n제목을 다국어 문자열로 제공한다.
2025-12-19 22:24:38 +09:00
Yu Sung
3220f300b5 설정 로그아웃 문구 다국어화 2025-12-19 21:53:01 +09:00
Yu Sung
8dae75afd7 회원탈퇴 문구 다국어화 2025-12-19 21:43:32 +09:00
Yu Sung
0ddb5810b1 프로필 수정 화면 문자열 다국어 적용
마이페이지 프로필 수정에서 소셜 링크와 비밀번호 안내 문구를\n다국어로 제공한다.
2025-12-19 19:59:27 +09:00
Yu Sung
f67fd5bb26 캔/포인트 내역 제목 로컬라이즈 추가
캔내역과 포인트 내역 제목을 로컬라이즈 키로 제공한다.
2025-12-19 19:48:27 +09:00
Yu Sung
a406935e4f 쿠폰 및 공지 화면 다국어 적용 2025-12-19 19:20:28 +09:00
Yu Sung
ee4b7c9a79 검색 입력 플레이스홀더 현지화 추가 2025-12-19 18:53:24 +09:00
Yu Sung
321d97bf4d 시리즈 전체보기 제목 현지화
크리에이터 기준 제목을 다국어로 제공한다.
요일 선택 영역을 가로 스크롤로 제공한다.
2025-12-19 18:48:31 +09:00
Yu Sung
fdac62fc7a 시리즈 메인 탭 라벨을 다국어로 제공한다 2025-12-19 18:38:23 +09:00
Yu Sung
27b024f187 시리즈 상세 화면 현지화 문자열 제공
시리즈 상세 화면의 탭과 섹션 제목을 다국어로 제공한다.
가격 표시와 요일 표기를 로케일에 맞게 보여준다.
2025-12-19 18:30:52 +09:00
Yu Sung
e035e57fc2 콘텐츠 상세의 섹션 제목 다국어 처리 2025-12-19 16:16:07 +09:00
Yu Sung
c80246adbf 콘텐츠 상세 구매 버튼 텍스트 다국어 처리 2025-12-19 16:11:43 +09:00
Yu Sung
bea50b0085 커뮤니티 게시글 상대 시간 표기 다국어 지원 2025-12-19 16:02:09 +09:00
Yu Sung
f51fe327e9 국제화 가능한 문자열을 I18n의 상수로 변경 2025-12-19 12:29:28 +09:00
Yu Sung
8bcbd3aca0 프로필과 시리즈 문구를 I18n으로 정리
프로필/시리즈 화면의 문자열을 I18n 및 Localizable로 통일한다.
채널 공유·예약·유료 라이브 관련 안내 문구를 다국어로 제공한다.
2025-12-18 17:52:02 +09:00
Yu Sung
982a17bb41 라이브 생성 문구를 I18n으로 이동한다
라이브 생성 화면 문구를 다국어 리소스로 통합한다.\n공지 입력 검증을 5자 이상으로 적용한다.
2025-12-18 15:25:13 +09:00
Yu Sung
8efa89d564 콘텐츠 작성 및 라이브 메뉴 다국어화
콘텐츠 등록 화면 텍스트와 버튼을 I18n 기반 번역 문자열로 교체

룰렛 설정과 미션 메뉴 버튼 라벨을 다국어 문자열로 통일

신규 텍스트를 String Catalog에 추가하여 네비게이션 타이틀 번역
2025-12-17 18:47:46 +09:00
Yu Sung
a0bb593431 로컬라이징 문자열 정비
탭, 상세, 설정 화면 텍스트를 I18n 키로 교체

신규 캐릭터 전체보기 등 문자열 번역을 추가
2025-12-17 15:55:11 +09:00
Yu Sung
fa163ec83d 다국어 문자열과 카운트 노출을 정비 2025-12-17 15:15:08 +09:00
Yu Sung
619870e1de 콘텐츠 전체 문자열 번역 추가 2025-12-17 14:27:25 +09:00
Yu Sung
ad9f26c8a0 String Catalog가 적용되지 않는 부분을 I18n 파일에 언어별로 하드코딩 하여 보완 2025-12-17 13:54:12 +09:00
Yu Sung
da2e8d0209 사용하지 않는 불필요한 string catalog 제거 2025-12-17 11:21:50 +09:00
Yu Sung
d2ff2782fe AI 채팅 원작 상세 - 번역 데이터가 있으면 번역 데이터를 표시하도록 수정 2025-12-17 00:42:34 +09:00
Yu Sung
30c70ee638 시리즈 상세 - 번역 데이터가 있으면 번역 데이터를 표시하도록 수정 2025-12-17 00:35:16 +09:00
Yu Sung
b61f432b72 캐릭터 상세 - 언어 설정에 따라 번역 데이터를 표시하도록 수정 2025-12-17 00:16:29 +09:00
Yu Sung
96df4c1f1b 콘텐츠 상세 - 언어 설정에 따라 번역 데이터를 표시하도록 수정 2025-12-16 23:42:06 +09:00
Yu Sung
0285f62ecb 언어 설정 화면 추가 및 언어 헤더 적용
설정에서 시스템/한국어/영어/일본어 선택을 지원한다.

선택 시 Accept-Language 헤더와 UI locale을 즉시 반영한다.

언어 변경 후 스플래시를 거쳐 메인으로 소프트 재시작한다.
2025-12-16 22:56:37 +09:00
Yu Sung
b2c94a44d9 Merge branch 'main' into feature/i18n 2025-12-09 20:06:15 +09:00
Yu Sung
bc4b30a965 마이페이지 탭에 보이스 크리에이터 지원하기 배너 추가 2025-12-09 18:32:41 +09:00
Yu Sung
3f24b1f3d4 스플래시 이미지 교체 2025-12-09 17:24:57 +09:00
Yu Sung
d5437c03b0 번역 누락 키 채우고 타이틀 로컬라이즈 2025-12-05 21:26:21 +09:00
Yu Sung
f88926f8a8 git 메시지 체크 스크립트 추가 2025-12-05 17:24:47 +09:00
Yu Sung
ea733b57e6 라이브 섹션 타이틀 문자열 정리
라이브 관련 섹션 헤더를 한 줄 문구로 통합해 표기 일관성을 높임
2025-12-05 17:20:22 +09:00
Yu Sung
dd7b42d58a 신규 캐릭터 문자열 번역 추가
영어와 일본어 로컬라이제이션에 신규 캐릭터 문자열을 채움
2025-12-05 17:18:16 +09:00
Yu Sung
972e889fab 누락 번역 추가 2025-12-05 16:30:37 +09:00
Yu Sung
3e60a8c340 탭 버튼 제목을 현지화한다 2025-12-05 15:59:12 +09:00
Yu Sung
dbf8bc2dd2 마이페이지 버튼 타이틀을 현지화한다 2025-12-05 15:43:45 +09:00
Yu Sung
3f857eb293 현지화 누락 번역을 추가한다 2025-12-05 15:41:33 +09:00
Yu Sung
0aa86c26ca 일본어 Localize 수정
- 콘텐츠 업로드 -> 일본어에서는 업로드로 표시하도록 수정
2025-12-05 15:24:20 +09:00
Yu Sung
a36a440340 앱 번들 표시 이름과 현지화를 정리한다 2025-12-05 15:16:31 +09:00
Yu Sung
55fb470b37 번역 데이터 전면 추가 2025-12-04 17:20:05 +09:00
Yu Sung
0375722d4c 라이브 만들기 버튼 스타일 개선
라이브 생성 아이콘을 ic_make_live로 교체하고 텍스트를 추가한다.

샘플 MessageInputView 파일을
  제거한다.
2025-12-04 15:42:07 +09:00
Yu Sung
4c8aabdd4c Add localization resources 2025-12-04 14:58:35 +09:00
Yu Sung
9e7c62b794 fix(character): 추천 캐릭터가 터치 액션이 실행 되지 않는 버그 수정 2025-11-24 14:49:00 +09:00
Yu Sung
d3ddcecc68 feat(content-list-all): 정렬의 vertical padding을 12 -> 16으로 변경 2025-11-21 00:53:34 +09:00
Yu Sung
6a4f4767e0 feat(series-all): 시리즈 아이템을 SeriesListItemView -> SeriesMainItemView로 변경하여 최신 시리즈 UI 적용 2025-11-21 00:53:01 +09:00
Yu Sung
90b6dfb498 fix(banner): 이미지가 2장씩 넘어가는 버그 수정 2025-11-20 18:08:54 +09:00
Yu Sung
001f161fc5 feat(series-list-all): 완결시리즈 전체보기 페이지 추가 2025-11-20 17:56:43 +09:00
Yu Sung
4d5ac61dbe feat(latest-audio-content-all): 테마 UI 변경, 아이템 2단으로 변경 2025-11-20 15:42:32 +09:00
Yu Sung
68fd9ee3ad feat(content-all): theme, 정렬(최신순/인기순) 추가 2025-11-20 14:52:28 +09:00
Yu Sung
af42fd074f refactor(live-room): BIG_HEART 메시지 수신 애니메이션이 여러번 실행되면 버벅거림과 발열이 생기던 문제 수정
- DispatchQueue로 Concurrent 처리
2025-11-17 18:47:50 +09:00
Yu Sung
31319e4292 feat(live-room): 왕하트 애니메이션 수정 2025-11-17 17:50:38 +09:00
Yu Sung
6cd0e86308 feat(series-all-by-genre): 시리즈 전체보기 장르별 탭 - 스크롤 로딩 추가 2025-11-15 06:21:19 +09:00
Yu Sung
54a5c99666 feat(series-all-by-genre): 시리즈 전체보기 장르별 탭 - 장르, 시리즈 UI 추가 2025-11-15 06:03:54 +09:00
Yu Sung
cb1b26c548 feat(series-all-by-genre): 시리즈 전체보기 장르별 탭 - API 추가 2025-11-15 05:41:14 +09:00
Yu Sung
5fcc6a9a60 feat(series-all-day-of-week): 시리즈 전체보기 요일별 탭 UI 추가 2025-11-15 05:08:24 +09:00
Yu Sung
a6ef1d89ce feat(series-all-day-of-week): 시리즈 전체보기 요일별 탭 API 추가 2025-11-15 03:28:03 +09:00
Yu Sung
9cea4c244a feat(series-all-home): 오류 토스트 UI 추가 2025-11-15 03:10:02 +09:00
Yu Sung
43629a27b8 feat(series-all-home): 시리즈 전체보기 홈 탭 - 추천 시리즈 UI 추가 2025-11-15 03:04:25 +09:00
Yu Sung
daca685ea2 feat(series-all-home): 시리즈 전체보기 홈 탭 완결 시리즈 UI 추가 2025-11-15 02:47:03 +09:00
Yu Sung
438f12024f feat(series-all-home): 시리즈 전체보기 홈 탭 배너 UI 추가 2025-11-15 02:32:53 +09:00
Yu Sung
72329d6f60 feat(series-all-home): 시리즈 전체보기 홈 탭 API 적용 2025-11-15 02:13:12 +09:00
Yu Sung
280ce4beda feat(series-all): 시리즈 전체보기 탭 구성 2025-11-15 01:50:58 +09:00
Yu Sung
320dbb6d57 feat(series-all): 시리즈 전체보기 페이지 파일 생성
- 홈 뷰의 요일별 시리즈에 전체보기 텍스트 추가
- 전체보기 터치시 시리즈 전체보기 페이지로 이동 액션 추가
2025-11-14 23:57:17 +09:00
Yu Sung
be40fbc226 feat(series-all): 오직 보이스온에서만(오리지널 시리즈) 전체보기 추가 2025-11-14 18:07:22 +09:00
Yu Sung
ed48efd58d feat(series-all): 시리즈 전체보기 UI 수정
- 기존 3단 구성에서 2단 구성으로 변경
- NavigationBar 제목에 OOO님의 시리즈 전체보기로 변경
2025-11-14 17:55:23 +09:00
Yu Sung
c4a7742514 feat(audio-content-all): 무료 콘텐츠, 포인트 대여 콘텐츠 전체보기 페이지 UI/API 구현 2025-11-14 04:21:39 +09:00
Yu Sung
e5810766b1 feat(home): 보온 주간 차트 콘텐츠 정렬 기준 추가
- 매출, 판매량, 댓글 수, 좋아요 수
2025-11-14 02:53:46 +09:00
Yu Sung
74212405a4 feat(chat-character): 추천 캐릭터 섹션 추가 및 새로고침 API 반영 2025-11-14 01:46:07 +09:00
Yu Sung
2c74bb743b feat(chat-character): 큐레이션 영역 제거 2025-11-14 01:31:52 +09:00
Yu Sung
0fd49a71f6 feat(home): 홈 추천 콘텐츠 섹션 추가 2025-11-14 01:24:20 +09:00
Yu Sung
0902b1fe30 feat(home): 사용하지 않는 큐레이션 영역 제거 2025-11-14 00:55:57 +09:00
Yu Sung
8bacf363ff feat(home): 포인트 대여 콘텐츠 섹션 추가 2025-11-14 00:52:35 +09:00
Yu Sung
1f88bcbddc feat(chat-character): 캐릭터 신규 이미지 표시 UI 추가 2025-11-14 00:47:20 +09:00
Yu Sung
7de2b1c4dd feat(chat-character): 작품별 탭 다시 추가 2025-11-14 00:32:02 +09:00
Yu Sung
6e3a1e1869 feat(live-room): BIG_HEART_DONATION 하트 메시지 3초 표시
HEART_DONATION은 기존 1.5초 유지. heartNicknameList를 (nickname,duration) 큐로 변경해 개별 표시시간을 저장. addHeartMessage(nickname:type:) 도입 및 호출부 수정. showNextHeartMessage가 현재 항목의 duration으로 전환 예약하도록 변경.
2025-11-06 17:35:31 +09:00
Yu Sung
0134a5286f fix(live-room): BIG_HEART 메시지 수신 후 Path로 그리는 하트 표시 시간 0.15초에서 0.3초로 수정 2025-11-06 17:12:23 +09:00
Yu Sung
2af2f2ffea fix(live): BIG_HEART 수신 연출 변경(가득 찬 하트→0.15초 후 폭발)
- 물 채우기 1초 연출 제거\n- 수신 시: 가득 찬 하트 즉시 표시 후 0.15초 뒤 폭발 파편 시작\n- 색상 유지(#ff959a)\n- ViewModel.addBigHeartAnimation()에서 progress=1.0 즉시 설정 후 0.15초 뒤 폭발 실행\n- suppressNextRemoteWaterFill 유지(로컬 발신 동작 기존과 동일)\n- UI 상태 변경은 메인 스레드에서 처리\n- 전체 빌드 성공
2025-11-06 16:51:02 +09:00
Yu Sung
3a7da9a876 fix(live-room-like-heart): 가운데 보이는 하트 크기 수정 2025-11-05 22:22:54 +09:00
Yu Sung
54d8845342 fix(live-room-like-heart): 하트 모양 수정 2025-11-05 22:00:54 +09:00
Yu Sung
a4c5a790fe feat(live-room-like-heart): 폭발 후 하트 비/우박 애니메이션 반영 2025-11-05 19:22:06 +09:00
Yu Sung
3cbac1280e fix(live-room-like-heart): 키보드가 올라올 때 중앙 하트 오버레이가 위로 이동하여 키보드에 가려지지 않도록 수정 2025-11-05 18:08:49 +09:00
Yu Sung
34eed366bd feat(live-room-like-heart): 하트가 가득 차면 폭죽처럼 터지는 애니메이션 반영 2025-11-05 18:01:32 +09:00
Yu Sung
95c2e992de fix(live-room): 좋아해요 미가용 시 롱프레스 중단 및 안내
- 하트 버튼 롱프레스 중 이면 즉시 진행 중단
  및 로 안내 다이얼로그 표시.
- pressing 핸들러에서 시작 즉시 가용성 체크 + 상태 초기화:
  , , , .
- onLongPress 콜백에서도 동일 조건 재검사(2차 방어).

관련 파일: LiveRoomViewV2.swift
2025-11-05 16:18:28 +09:00
Yu Sung
76757215cf feat(live-room): 하트 롱프레스 시 가운데 빈 하트가 표시되고 물 채워지는 애니메이션 추가 2025-11-05 16:07:41 +09:00
Yu Sung
0a59c6f575 feat(live-room): 하트를 길게(2초)간 누르면 표시 되는 왕하트(100캔) 추가, 애니메이션 제외 2025-11-05 12:20:01 +09:00
479 changed files with 18949 additions and 3280 deletions

1
.gitignore vendored
View File

@@ -279,5 +279,6 @@ xcuserdata
.kiro/ .kiro/
.junie/ .junie/
docs/
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods # End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods

18
AGENTS.md Normal file
View File

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

View File

@@ -8,6 +8,8 @@ target 'SodaLive' do
# Pods for SodaLive # Pods for SodaLive
pod 'BootpayUI', '4.4.10' pod 'BootpayUI', '4.4.10'
pod 'AgoraRtm', '2.2.4' pod 'AgoraRtm', '2.2.4'
pod 'GoogleSignIn'
pod 'GoogleSignInSwiftSupport'
end end
@@ -18,6 +20,8 @@ target 'SodaLive-dev' do
# Pods for SodaLive-dev # Pods for SodaLive-dev
pod 'BootpayUI', '4.4.10' pod 'BootpayUI', '4.4.10'
pod 'AgoraRtm', '2.2.4' pod 'AgoraRtm', '2.2.4'
pod 'GoogleSignIn'
pod 'GoogleSignInSwiftSupport'
end end

View File

@@ -5,6 +5,16 @@ PODS:
- AgoraRtm/RtmBasic (2.2.4) - AgoraRtm/RtmBasic (2.2.4)
- AgoraRtm/RtmKit (2.2.4) - AgoraRtm/RtmKit (2.2.4)
- Alamofire (5.10.2) - Alamofire (5.10.2)
- AppAuth (2.0.0):
- AppAuth/Core (= 2.0.0)
- AppAuth/ExternalUserAgent (= 2.0.0)
- AppAuth/Core (2.0.0)
- AppAuth/ExternalUserAgent (2.0.0):
- AppAuth/Core
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- Bootpay (4.4.6): - Bootpay (4.4.6):
- CryptoSwift - CryptoSwift
- NVActivityIndicatorView - NVActivityIndicatorView
@@ -17,40 +27,79 @@ PODS:
- SnapKit - SnapKit
- SwiftyJSON - SwiftyJSON
- CryptoSwift (1.8.4) - CryptoSwift (1.8.4)
- GoogleSignIn (9.1.0):
- AppAuth (~> 2.0)
- AppCheckCore (~> 11.0)
- GTMAppAuth (~> 5.0)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleSignInSwiftSupport (9.1.0):
- GoogleSignIn (~> 9.0)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (5.0.0):
- AppAuth/Core (~> 2.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher/Core (3.5.0)
- NVActivityIndicatorView (5.2.0): - NVActivityIndicatorView (5.2.0):
- NVActivityIndicatorView/Base (= 5.2.0) - NVActivityIndicatorView/Base (= 5.2.0)
- NVActivityIndicatorView/Base (5.2.0) - NVActivityIndicatorView/Base (5.2.0)
- ObjectMapper (4.4.2) - ObjectMapper (4.4.2)
- PromisesObjC (2.4.0)
- SnapKit (5.7.1) - SnapKit (5.7.1)
- SwiftyJSON (5.0.2) - SwiftyJSON (5.0.2)
DEPENDENCIES: DEPENDENCIES:
- AgoraRtm (= 2.2.4) - AgoraRtm (= 2.2.4)
- BootpayUI (= 4.4.10) - BootpayUI (= 4.4.10)
- GoogleSignIn
- GoogleSignInSwiftSupport
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- AgoraRtm - AgoraRtm
- Alamofire - Alamofire
- AppAuth
- AppCheckCore
- Bootpay - Bootpay
- BootpayUI - BootpayUI
- CryptoSwift - CryptoSwift
- GoogleSignIn
- GoogleSignInSwiftSupport
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- NVActivityIndicatorView - NVActivityIndicatorView
- ObjectMapper - ObjectMapper
- PromisesObjC
- SnapKit - SnapKit
- SwiftyJSON - SwiftyJSON
SPEC CHECKSUMS: SPEC CHECKSUMS:
AgoraRtm: 534144434383d41b3b0ebfae2a961ef0f51b0645 AgoraRtm: 534144434383d41b3b0ebfae2a961ef0f51b0645
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
Bootpay: cd7f0542b096ab0af0b09a6e12a6b87f2cbbb531 Bootpay: cd7f0542b096ab0af0b09a6e12a6b87f2cbbb531
BootpayUI: beec5b0bba002b4dbced8c0ecace571ed6a017bc BootpayUI: beec5b0bba002b4dbced8c0ecace571ed6a017bc
CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
GoogleSignInSwiftSupport: aca902e4e15b234611ecac74ef5c8f61278f774e
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
NVActivityIndicatorView: fe52a6a68664c2df8991d7d9e3d86d8d19453c53 NVActivityIndicatorView: fe52a6a68664c2df8991d7d9e3d86d8d19453c53
ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677 ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
PODFILE CHECKSUM: 197d8c8b434dbcc335438281fc68e94718f6a8e1 PODFILE CHECKSUM: 70c5639090824ff26cfad959985347579609e1e6
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "1f28da3687662a2a9efe60ffc2ca2499be411b5b0a1e07f72559059c40728121", "originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37",
"pins" : [ "pins" : [
{ {
"identity" : "abseil-cpp-binary", "identity" : "abseil-cpp-binary",
@@ -127,6 +127,15 @@
"version" : "100.0.0" "version" : "100.0.0"
} }
}, },
{
"identity" : "kakao-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kakao/kakao-ios-sdk",
"state" : {
"revision" : "5978979157a5a0521c9c56fd0156aec794caa21c",
"version" : "2.27.2"
}
},
{ {
"identity" : "kingfisher", "identity" : "kingfisher",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -145,6 +154,15 @@
"version" : "1.22.5" "version" : "1.22.5"
} }
}, },
{
"identity" : "line-sdk-ios-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/line/line-sdk-ios-swift.git",
"state" : {
"revision" : "51ef2ebefb05db8f748e80208b3281ca723abcdb",
"version" : "5.14.0"
}
},
{ {
"identity" : "moya", "identity" : "moya",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_follow_big_en.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_follow_big_ja.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_following_big_en.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_following_big_ja.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_following_no_alarm_big_en.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_following_no_alarm_big_ja.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -9,7 +9,7 @@
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "btn_make_live.png", "filename" : "ic_login_line.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_login_x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_make_live.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_no_preview.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "img_apply_creator.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "img_rank_1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "img_rank_2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "img_rank_3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "rank_1.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "rank_2.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "rank_3.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@@ -18,6 +18,31 @@
<string>voiceon-test</string> <string>voiceon-test</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>kakao20cf19413d63bfdfd30e8e6dff933d33</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.758414412471-3cf403jb4s405eu17qrfrcbs9ofhq369</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- Specify URL scheme to use when returning from LINE to your app. -->
<string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array> </array>
<key>FacebookAppID</key> <key>FacebookAppID</key>
<string>608674328645232</string> <string>608674328645232</string>
@@ -31,6 +56,8 @@
<false/> <false/>
<key>GADApplicationIdentifier</key> <key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~3447556960</string> <string>ca-app-pub-1299501215847962~3447556960</string>
<key>GIDClientID</key>
<string>758414412471-3cf403jb4s405eu17qrfrcbs9ofhq369.apps.googleusercontent.com</string>
<key>NSAdvertisingAttributionReportEndpoint</key> <key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://appsflyer-skadnetwork.com/</string> <string>https://appsflyer-skadnetwork.com/</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
@@ -239,13 +266,14 @@
</array> </array>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>gmarket_sans_bold.otf</string>
<string>gmarket_sans_medium.otf</string>
<string>gmarket_sans_light.otf</string>
<string>Pretendard-Bold.otf</string> <string>Pretendard-Bold.otf</string>
<string>Pretendard-Medium.otf</string> <string>Pretendard-Medium.otf</string>
<string>Pretendard-Light.otf</string> <string>Pretendard-Light.otf</string>
<string>Pretendard-Regular.otf</string> <string>Pretendard-Regular.otf</string>
<string>NotoSansJP-Bold.ttf</string>
<string>NotoSansJP-Medium.ttf</string>
<string>NotoSansJP-Light.ttf</string>
<string>NotoSansJP-Regular.ttf</string>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
@@ -253,5 +281,16 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
</dict> <key>LSApplicationQueriesSchemes</key>
<array>
<!-- 카카오톡으로 로그인 -->
<string>kakaokompassauth</string>
<!-- 카카오톡 공유 -->
<string>kakaolink</string>
<!-- 카카오톡 채널 -->
<string>kakaoplus</string>
<!-- 라인 -->
<string>lineauth2</string>
</array>
</dict>
</plist> </plist>

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "VoiceOn-Test";
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
"NSCameraUsageDescription" = "Allow access to your camera to take photos.";

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "ボイスオン-テスト";
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
"NSCameraUsageDescription" = "写真を撮影するためにカメラへのアクセスが必要です。";

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "보이스온-테스트";
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
"NSCameraUsageDescription" = "사진을 촬영하려면 카메라 접근이 필요합니다.";

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@@ -18,6 +18,31 @@
<string>voiceon</string> <string>voiceon</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>kakao231cf78acfa8252fca38b9eedf87c5cb</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.983594297130-m6bv7lvc1lsetsvv3rk92etqc98uopqj</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- Specify URL scheme to use when returning from LINE to your app. -->
<string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array> </array>
<key>FacebookAppID</key> <key>FacebookAppID</key>
<string>612448298237287</string> <string>612448298237287</string>
@@ -31,6 +56,8 @@
<false/> <false/>
<key>GADApplicationIdentifier</key> <key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~8852459715</string> <string>ca-app-pub-1299501215847962~8852459715</string>
<key>GIDClientID</key>
<string>983594297130-m6bv7lvc1lsetsvv3rk92etqc98uopqj.apps.googleusercontent.com</string>
<key>NSAdvertisingAttributionReportEndpoint</key> <key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://appsflyer-skadnetwork.com/</string> <string>https://appsflyer-skadnetwork.com/</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
@@ -239,13 +266,14 @@
</array> </array>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>gmarket_sans_bold.otf</string>
<string>gmarket_sans_medium.otf</string>
<string>gmarket_sans_light.otf</string>
<string>Pretendard-Bold.otf</string> <string>Pretendard-Bold.otf</string>
<string>Pretendard-Medium.otf</string> <string>Pretendard-Medium.otf</string>
<string>Pretendard-Light.otf</string> <string>Pretendard-Light.otf</string>
<string>Pretendard-Regular.otf</string> <string>Pretendard-Regular.otf</string>
<string>NotoSansJP-Bold.ttf</string>
<string>NotoSansJP-Medium.ttf</string>
<string>NotoSansJP-Light.ttf</string>
<string>NotoSansJP-Regular.ttf</string>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
@@ -253,5 +281,16 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
</dict> <key>LSApplicationQueriesSchemes</key>
<array>
<!-- 카카오톡으로 로그인 -->
<string>kakaokompassauth</string>
<!-- 카카오톡 공유 -->
<string>kakaolink</string>
<!-- 카카오톡 채널 -->
<string>kakaoplus</string>
<!-- Specify URL scheme to use when launching LINE from your app. -->
<string>lineauth2</string>
</array>
</dict>
</plist> </plist>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "VoiceOn";
"NSPhotoLibraryUsageDescription" = "Allow access to your photo library to select photos.";
"NSMicrophoneUsageDescription" = "Allow access to your microphone to record audio.";
"NSUserTrackingUsageDescription" = "Allow tracking to deliver personalized ads.";
"NSCameraUsageDescription" = "Allow access to your camera to take photos.";

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "ボイスオン";
"NSPhotoLibraryUsageDescription" = "写真を選択するために写真ライブラリへのアクセスが必要です。";
"NSMicrophoneUsageDescription" = "音声を録音するためにマイクへのアクセスが必要です。";
"NSUserTrackingUsageDescription" = "パーソナライズされた広告のためにトラッキングを許可してください。";
"NSCameraUsageDescription" = "写真を撮影するためにカメラへのアクセスが必要です。";

View File

@@ -0,0 +1,12 @@
/*
InfoPlist.strings
SodaLive
Created by klaus on 12/5/25.
*/
"CFBundleDisplayName" = "보이스온";
"NSPhotoLibraryUsageDescription" = "사진을 선택하려면 사진 라이브러리 접근이 필요합니다.";
"NSMicrophoneUsageDescription" = "음성 녹음을 위해 마이크 접근이 필요합니다.";
"NSUserTrackingUsageDescription" = "맞춤형 광고 제공을 위해 추적을 허용해 주세요.";
"NSCameraUsageDescription" = "사진을 촬영하려면 카메라 접근이 필요합니다.";

View File

@@ -4,6 +4,10 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:voiceon.onelink.me</string> <string>applinks:voiceon.onelink.me</string>

View File

@@ -14,6 +14,7 @@ import FBSDKCoreKit
import FirebaseCore import FirebaseCore
import FirebaseAnalytics import FirebaseAnalytics
import FirebaseMessaging import FirebaseMessaging
import LineSDK
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -24,6 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure() FirebaseApp.configure()
LoginManager.shared.setup(channelID: LINE_CHANNEL_ID, universalLinkURL: nil)
Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD) Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD)
Messaging.messaging().delegate = self Messaging.messaging().delegate = self
setupAppsFlyer() setupAppsFlyer()
@@ -75,6 +77,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
_ = LoginManager.shared.application(application, open: userActivity.webpageURL)
AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil) AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil)
return true return true
} }

View File

@@ -43,7 +43,7 @@ class AppState: ObservableObject {
@Published var purchasedContentId = 0 @Published var purchasedContentId = 0
@Published var purchasedContentOrderType = OrderType.KEEP @Published var purchasedContentOrderType = OrderType.KEEP
@Published var isChangeAdultContentVisible = false @Published var isRestartApp = false
@Published var startTab: HomeViewModel.CurrentTab = .home @Published var startTab: HomeViewModel.CurrentTab = .home
@Published var marketingUtmSource = "" @Published var marketingUtmSource = ""
@@ -74,4 +74,13 @@ class AppState: ObservableObject {
self.appStep = .main self.appStep = .main
} }
} }
// ( -> ) UI
func softRestart() {
isRestartApp = true
setAppStep(step: .splash)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.setAppStep(step: .main)
}
}
} }

View File

@@ -26,6 +26,8 @@ enum AppStep {
case settings case settings
case languageSettings
case notices case notices
case noticeDetail(notice: NoticeItem) case noticeDetail(notice: NoticeItem)
@@ -128,7 +130,7 @@ enum AppStep {
case seriesDetail(seriesId: Int) case seriesDetail(seriesId: Int)
case seriesAll(creatorId: Int) case seriesAll(creatorId: Int? = nil, creatorNickname: String? = nil, isOriginal: Bool = false, isCompleted: Bool = false)
case seriesContentAll(seriesId: Int, seriesTitle: String) case seriesContentAll(seriesId: Int, seriesTitle: String)
@@ -169,4 +171,8 @@ enum AppStep {
case newCharacterAll case newCharacterAll
case originalWorkDetail(originalId: Int) case originalWorkDetail(originalId: Int)
case contentAll(isFree: Bool = false, isPointOnly: Bool = false)
case seriesMain
} }

View File

@@ -10,6 +10,10 @@ import Kingfisher
import FBSDKCoreKit import FBSDKCoreKit
import AppsFlyerLib import AppsFlyerLib
import GoogleSignIn
import KakaoSDKCommon
import KakaoSDKAuth
import LineSDK
@main @main
struct SodaLiveApp: App { struct SodaLiveApp: App {
@@ -19,9 +23,13 @@ struct SodaLiveApp: App {
@ObservedObject var viewModel = AppViewModel() @ObservedObject var viewModel = AppViewModel()
@StateObject var canPgPaymentViewModel = CanPgPaymentViewModel() @StateObject var canPgPaymentViewModel = CanPgPaymentViewModel()
@StateObject private var languageEnvironment = LanguageContainer.environment
init() { init() {
configureImageCache() configureImageCache()
// ,
LanguageHeaderProvider.initialize()
KakaoSDK.initSDK(appKey: KAKAO_APP_KEY)
} }
private func configureImageCache() { private func configureImageCache() {
@@ -40,6 +48,10 @@ struct SodaLiveApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView(canPgPaymentViewModel: canPgPaymentViewModel) ContentView(canPgPaymentViewModel: canPgPaymentViewModel)
.environment(\.locale, languageEnvironment.locale)
.task {
await LanguageContainer.service.bootstrap()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
CreatorCommunityMediaPlayerManager.shared.pauseContent() CreatorCommunityMediaPlayerManager.shared.pauseContent()
// //
@@ -61,14 +73,21 @@ struct SodaLiveApp: App {
.onOpenURL { url in .onOpenURL { url in
DEBUG_LOG("I have received a URL through a custom scheme! \(url.absoluteString)") DEBUG_LOG("I have received a URL through a custom scheme! \(url.absoluteString)")
if KakaoSDKAuth.AuthApi.isKakaoTalkLoginUrl(url) {
_ = AuthController.handleOpenUrl(url: url)
return
}
if let comps = URLComponents(url: url, resolvingAgainstBaseURL: false), if let comps = URLComponents(url: url, resolvingAgainstBaseURL: false),
url.scheme?.lowercased() == APPSCHEME.lowercased(), url.scheme?.lowercased() == APPSCHEME.lowercased(),
comps.host?.lowercased() == "payverse", comps.host?.lowercased() == "payverse",
comps.path.lowercased() == "/result" { comps.path.lowercased() == "/result" {
canPgPaymentViewModel.handleVerifyOpenURL(url) canPgPaymentViewModel.handleVerifyOpenURL(url)
} else { } else {
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:])
ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:]) ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:])
AppsFlyerLib.shared().handleOpen(url) AppsFlyerLib.shared().handleOpen(url)
GIDSignIn.sharedInstance.handle(url)
} }
} }
} }

View File

@@ -31,7 +31,7 @@ struct ApplyMethodView: View {
} }
Text("오디션 지원방식") Text("오디션 지원방식")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.padding(.top, 33.3) .padding(.top, 33.3)
@@ -40,7 +40,7 @@ struct ApplyMethodView: View {
Image("ic_upload") Image("ic_upload")
Text("파일 업로드") Text("파일 업로드")
.font(.custom(Font.medium.rawValue, size: 14.7)) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
.padding(.vertical, 8) .padding(.vertical, 8)
@@ -61,7 +61,7 @@ struct ApplyMethodView: View {
Image("ic_mic_color_button") Image("ic_mic_color_button")
Text("바로 녹음") Text("바로 녹음")
.font(.custom(Font.medium.rawValue, size: 14.7)) .appFont(size: 14.7, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
} }
.padding(.vertical, 8) .padding(.vertical, 8)
@@ -82,7 +82,7 @@ struct ApplyMethodView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("※ 파일은 mp3, aac만 업로드 가능") Text("※ 파일은 mp3, aac만 업로드 가능")
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
.padding(.top, 13.3) .padding(.top, 13.3)

View File

@@ -43,7 +43,7 @@ struct AuditionApplicantItemView: View {
VStack(spacing: 8) { VStack(spacing: 8) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text(item.nickname.count > 9 ? "\(item.nickname.prefix(9))..." : item.nickname) Text(item.nickname.count > 9 ? "\(item.nickname.prefix(9))..." : item.nickname)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.onTapGesture { .onTapGesture {
AppState.shared.setAppStep(step: .creatorDetail(userId: item.memberId)) AppState.shared.setAppStep(step: .creatorDetail(userId: item.memberId))
@@ -53,7 +53,7 @@ struct AuditionApplicantItemView: View {
if soundManager.applicantId == item.applicantId { if soundManager.applicantId == item.applicantId {
Text("\(secondsToMinutesSeconds(Int(soundManager.currentTime)))/\(secondsToMinutesSeconds(Int(soundManager.duration)))") Text("\(secondsToMinutesSeconds(Int(soundManager.currentTime)))/\(secondsToMinutesSeconds(Int(soundManager.duration)))")
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
} }
} }
@@ -68,7 +68,7 @@ struct AuditionApplicantItemView: View {
Image("ic_heart_vote") Image("ic_heart_vote")
Text("\(item.voteCount)") Text("\(item.voteCount)")
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
} }
.onTapGesture { .onTapGesture {

View File

@@ -31,7 +31,7 @@ struct AuditionApplicantRecordingView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션 녹음") Text("오디션 녹음")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
@@ -46,7 +46,7 @@ struct AuditionApplicantRecordingView: View {
} }
Text(soundManager.timeString) Text(soundManager.timeString)
.font(.custom(Font.bold.rawValue, size: 33.3)) .appFont(size: 33.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.top, 80) .padding(.top, 80)
@@ -77,7 +77,7 @@ struct AuditionApplicantRecordingView: View {
Spacer() Spacer()
Text("삭제") Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3)) .appFont(size: 15.3, weight: .medium)
.foregroundColor(Color.graybb.opacity(0)) .foregroundColor(Color.graybb.opacity(0))
Spacer() Spacer()
@@ -100,7 +100,7 @@ struct AuditionApplicantRecordingView: View {
Spacer() Spacer()
Text("삭제") Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3)) .appFont(size: 15.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
.onTapGesture { .onTapGesture {
soundManager.stopAudio() soundManager.stopAudio()
@@ -114,7 +114,7 @@ struct AuditionApplicantRecordingView: View {
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("다시 녹음") Text("다시 녹음")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.frame(width: (proxy.size.width - 40) / 3, height: 50) .frame(width: (proxy.size.width - 40) / 3, height: 50)
.background(Color.button.opacity(0.2)) .background(Color.button.opacity(0.2))
@@ -130,7 +130,7 @@ struct AuditionApplicantRecordingView: View {
} }
Text("녹음완료") Text("녹음완료")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.frame(width: (proxy.size.width - 40) * 2 / 3, height: 50) .frame(width: (proxy.size.width - 40) * 2 / 3, height: 50)
.background(Color.button) .background(Color.button)
@@ -178,7 +178,7 @@ struct AuditionApplicantRecordingView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.padding(.horizontal, 6.7) .padding(.horizontal, 6.7)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -34,7 +34,7 @@ struct AuditionApplyView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션 지원") Text("오디션 지원")
.font(.custom(Font.medium.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
@@ -46,7 +46,7 @@ struct AuditionApplyView: View {
} }
Text("녹음파일") Text("녹음파일")
.font(.custom(Font.bold.rawValue, size: 16.7)) .appFont(size: 16.7, weight: .bold)
.foregroundColor(.grayee) .foregroundColor(.grayee)
.padding(.top, 20) .padding(.top, 20)
@@ -54,7 +54,7 @@ struct AuditionApplyView: View {
Image("ic_note_square") Image("ic_note_square")
Text(filename) Text(filename)
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.grayd2) .foregroundColor(.grayd2)
Spacer() Spacer()
@@ -67,7 +67,7 @@ struct AuditionApplyView: View {
.padding(.top, 10) .padding(.top, 10)
Text("연락처") Text("연락처")
.font(.custom(Font.bold.rawValue, size: 16.7)) .appFont(size: 16.7, weight: .bold)
.foregroundColor(.grayee) .foregroundColor(.grayee)
.padding(.top, 15) .padding(.top, 15)
@@ -75,7 +75,7 @@ struct AuditionApplyView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.grayee) .foregroundColor(.grayee)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
.padding(.vertical, 17) .padding(.vertical, 17)
@@ -90,7 +90,7 @@ struct AuditionApplyView: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.") Text("보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.lineSpacing(3) .lineSpacing(3)
} }
@@ -101,7 +101,7 @@ struct AuditionApplyView: View {
} }
Text("오디션 지원하기") Text("오디션 지원하기")
.font(.custom(Font.bold.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -137,7 +137,7 @@ struct AuditionApplyView: View {
Text(errorMessage) Text(errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -31,7 +31,7 @@ struct AuditionItemView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Text(item.title) Text(item.title)
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)

View File

@@ -25,7 +25,7 @@ struct AuditionView: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("오디션") Text("오디션")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }
@@ -44,13 +44,13 @@ struct AuditionView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("보이스온 오디션 이용방법") Text("보이스온 오디션 이용방법")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
Text("자세히>") Text("자세히>")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@@ -74,17 +74,17 @@ struct AuditionView: View {
VStack(alignment: .leading, spacing: 25) { VStack(alignment: .leading, spacing: 25) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션") Text("오디션")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(" ON") Text(" ON")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.mainRed) .foregroundColor(Color.mainRed)
Spacer() Spacer()
Text("\(viewModel.inProgressCount)") Text("\(viewModel.inProgressCount)")
.font(.custom(Font.medium.rawValue, size: 11.3)) .appFont(size: 11.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
} }
@@ -112,17 +112,17 @@ struct AuditionView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("오디션") Text("오디션")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(" OFF") Text(" OFF")
.font(.custom(Font.bold.rawValue, size: 18.3)) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Spacer() Spacer()
Text("\(viewModel.completedCount)") Text("\(viewModel.completedCount)")
.font(.custom(Font.medium.rawValue, size: 11.3)) .appFont(size: 11.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
} }
.padding(.top, 30) .padding(.top, 30)

View File

@@ -31,7 +31,7 @@ struct AuditionDetailView: View {
.cornerRadius(6.7) .cornerRadius(6.7)
Text("오디션 정보") Text("오디션 정보")
.font(.custom(Font.bold.rawValue, size: 14.7)) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)
@@ -39,7 +39,7 @@ struct AuditionDetailView: View {
.padding(.top, 13.3) .padding(.top, 13.3)
Text("오디션 캐릭터") Text("오디션 캐릭터")
.font(.custom(Font.bold.rawValue, size: 14.7)) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)
@@ -74,7 +74,7 @@ struct AuditionDetailView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)

View File

@@ -29,7 +29,7 @@ struct AuditionDetailRoleItemView: View {
) )
Text(item.isComplete ? "모집완료" : "모집중") Text(item.isComplete ? "모집완료" : "모집중")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(.horizontal, 9) .padding(.horizontal, 9)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -43,7 +43,7 @@ struct AuditionDetailRoleItemView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Text(item.name) Text(item.name)
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)

View File

@@ -46,7 +46,7 @@ struct AuditionRoleDetailView: View {
HStack(spacing: 14) { HStack(spacing: 14) {
if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) { if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) {
Text("원작 보러가기") Text("원작 보러가기")
.font(.custom(Font.bold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 12) .padding(.vertical, 12)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -60,7 +60,7 @@ struct AuditionRoleDetailView: View {
if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) { if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) {
Text("오디션 대본 확인") Text("오디션 대본 확인")
.font(.custom(Font.bold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.vertical, 12) .padding(.vertical, 12)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -75,7 +75,7 @@ struct AuditionRoleDetailView: View {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
Text("오디션 캐릭터 정보") Text("오디션 캐릭터 정보")
.font(.custom(Font.bold.rawValue, size: 14.7)) .appFont(size: 14.7, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
ExpandableTextView(text: roleDetail.information) ExpandableTextView(text: roleDetail.information)
@@ -84,28 +84,28 @@ struct AuditionRoleDetailView: View {
if viewModel.applicantList.isEmpty { if viewModel.applicantList.isEmpty {
Text("지원자가 없습니다.") Text("지원자가 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13)) .appFont(size: 13, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(.top, 15) .padding(.top, 15)
} else { } else {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("참여자") Text("참여자")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Text("\(viewModel.totalCount)") Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(.leading, 2.3) .padding(.leading, 2.3)
Text("") Text("")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.graybb) .foregroundColor(Color.graybb)
Spacer() Spacer()
Text("최신순") Text("최신순")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
viewModel.sortType == .NEWEST ? Color.button : Color.graybb viewModel.sortType == .NEWEST ? Color.button : Color.graybb
) )
@@ -114,7 +114,7 @@ struct AuditionRoleDetailView: View {
} }
Text("좋아요순") Text("좋아요순")
.font(.custom(Font.medium.rawValue, size: 13.3)) .appFont(size: 13.3, weight: .medium)
.foregroundColor( .foregroundColor(
viewModel.sortType == .LIKES ? Color.button : Color.graybb viewModel.sortType == .LIKES ? Color.button : Color.graybb
) )
@@ -156,7 +156,7 @@ struct AuditionRoleDetailView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -171,7 +171,7 @@ struct AuditionRoleDetailView: View {
Text(soundManager.errorMessage) Text(soundManager.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -190,7 +190,7 @@ struct AuditionRoleDetailView: View {
if let roleDetail = viewModel.auditionRoleDetail { if let roleDetail = viewModel.auditionRoleDetail {
Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원") Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원")
.font(.custom(Font.bold.rawValue, size: 15.3)) .appFont(size: 15.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.padding(14) .padding(14)
.background(Color.button) .background(Color.button)

View File

@@ -79,11 +79,10 @@ private struct AutoSlideCharacterBannerPage: View {
let width: CGFloat let width: CGFloat
let height: CGFloat let height: CGFloat
let onTap: (CharacterBannerResponse) -> Void let onTap: (CharacterBannerResponse) -> Void
@State private var boundURL: URL?
var body: some View { var body: some View {
Group { Group {
if let boundURL { if let boundURL = URL(string: item.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? item.imageUrl){
DownsampledKFImage( DownsampledKFImage(
url: boundURL, url: boundURL,
size: CGSize(width: width, height: height) size: CGSize(width: width, height: height)
@@ -96,14 +95,5 @@ private struct AutoSlideCharacterBannerPage: View {
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { onTap(item) } .onTapGesture { onTap(item) }
.onAppear {
let encoded = item.imageUrl.addingPercentEncoding(
withAllowedCharacters: .urlQueryAllowed
) ?? item.imageUrl
boundURL = URL(string: encoded)
}
.onDisappear {
boundURL = nil
}
} }
} }

View File

@@ -10,4 +10,5 @@ struct Character: Decodable {
let name: String let name: String
let description: String? let description: String?
let imageUrl: String let imageUrl: String
let isNew: Bool
} }

View File

@@ -15,6 +15,7 @@ enum CharacterApi {
case getMyCharacterImageList(characterId: Int64, page: Int, size: Int) case getMyCharacterImageList(characterId: Int64, page: Int, size: Int)
case purchaseCharacterImage(imageId: Int) case purchaseCharacterImage(imageId: Int)
case getRecentCharacters(page: Int, size: Int) case getRecentCharacters(page: Int, size: Int)
case refreshRecommendCharacters
} }
extension CharacterApi: TargetType { extension CharacterApi: TargetType {
@@ -39,6 +40,9 @@ extension CharacterApi: TargetType {
case .getRecentCharacters: case .getRecentCharacters:
return "/api/chat/character/recent" return "/api/chat/character/recent"
case .refreshRecommendCharacters:
return "/api/chat/character/recommend"
} }
} }
@@ -54,7 +58,7 @@ extension CharacterApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { switch self {
case .getCharacterHome, .getCharacterDetail: case .getCharacterHome, .getCharacterDetail, .refreshRecommendCharacters:
return .requestPlain return .requestPlain
case .getRecentCharacters(let page, let size): case .getRecentCharacters(let page, let size):

View File

@@ -10,5 +10,5 @@ struct CharacterHomeResponse: Decodable {
let recentCharacters: [RecentCharacter] let recentCharacters: [RecentCharacter]
let popularCharacters: [Character] let popularCharacters: [Character]
let newCharacters: [Character] let newCharacters: [Character]
let curationSections: [CurationSection] let recommendCharacters: [Character]
} }

View File

@@ -21,31 +21,50 @@ struct CharacterItemView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .leading) {
DownsampledKFImage( DownsampledKFImage(
url: URL(string: character.imageUrl), url: URL(string: character.imageUrl),
size: CGSize(width: size, height: size) size: CGSize(width: size, height: size)
) )
.cornerRadius(12) .cornerRadius(12)
VStack {
if character.isNew {
HStack {
Spacer()
Text("N")
.appFont(size: 18, weight: .regular)
.foregroundColor(.white)
.frame(width: 30, height: 30)
.background(Color.button)
.clipShape(Circle())
}
.padding(.top, 8)
.padding(.trailing, 8)
}
Spacer()
if isShowRank { if isShowRank {
Text("\(rank)") Text("\(rank)")
.font(.custom(Font.preBold.rawValue, size: 72)) .appFont(size: 72, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.frame(height: capHeight) .frame(height: capHeight)
} }
} }
}
Text(character.name) Text(character.name)
.font(.custom(Font.preRegular.rawValue, size: 18)) .appFont(size: 18, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
if let desc = character.description, !desc.isEmpty { if let desc = character.description, !desc.isEmpty {
Text(desc) Text(desc)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
.lineLimit(1) .lineLimit(1)
} }
@@ -56,7 +75,7 @@ struct CharacterItemView: View {
#Preview { #Preview {
CharacterItemView( CharacterItemView(
character: Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300"), character: Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300", isNew: true),
size: 168, size: 168,
rank: 20, rank: 20,
isShowRank: true isShowRank: true

View File

@@ -16,4 +16,8 @@ class CharacterRepository {
func getCharacterMain() -> AnyPublisher<Response, MoyaError> { func getCharacterMain() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getCharacterHome) return api.requestPublisher(.getCharacterHome)
} }
func refreshRecommendCharacters() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.refreshRecommendCharacters)
}
} }

View File

@@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct CharacterSectionView: View { struct CharacterSectionView: View {
let title: String let title: LocalizedStringResource
let items: [Character] let items: [Character]
let isShowRank: Bool let isShowRank: Bool
var trailingTitle: String? = nil var trailingTitle: String? = nil
@@ -19,12 +19,12 @@ struct CharacterSectionView: View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text(title) Text(title)
.font(.custom(Font.preBold.rawValue, size: 20)) .appFont(size: 24, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
if let trailingTitle = trailingTitle { if let trailingTitle = trailingTitle {
Text(trailingTitle) Text(trailingTitle)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "90A4AE")) .foregroundColor(Color(hex: "90A4AE"))
.onTapGesture { onTapTrailing?() } .onTapGesture { onTapTrailing?() }
} }
@@ -54,8 +54,8 @@ struct CharacterSectionView: View {
CharacterSectionView( CharacterSectionView(
title: "신규 캐릭터", title: "신규 캐릭터",
items: [ items: [
Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300"), Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300", isNew: true),
Character(characterId: 2, name: "데이지", description: "", imageUrl: "https://picsum.photos/300") Character(characterId: 2, name: "데이지", description: "", imageUrl: "https://picsum.photos/300", isNew: false)
], ],
isShowRank: true isShowRank: true
) )

View File

@@ -54,7 +54,7 @@ struct CharacterView: View {
title: "신규 캐릭터", title: "신규 캐릭터",
items: viewModel.newCharacters, items: viewModel.newCharacters,
isShowRank: false, isShowRank: false,
trailingTitle: "전체보기", trailingTitle: I18n.Common.viewAll,
onTapTrailing: { onTapTrailing: {
onSelectNewCharacterAll() onSelectNewCharacterAll()
}, },
@@ -64,21 +64,51 @@ struct CharacterView: View {
) )
} }
// ( ) if !viewModel.recommendCharacters.isEmpty {
if !viewModel.curations.isEmpty { VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 48) { HStack {
ForEach(viewModel.curations.indices, id: \.self) { idx in Text("추천 캐릭터")
let section = viewModel.curations[idx] .appFont(size: 24, weight: .bold)
CharacterSectionView( .foregroundColor(.white)
title: section.title,
items: section.characters, Spacer()
isShowRank: false,
onTap: { ch in Image("ic_refresh")
onSelectCharacter(ch.characterId) .onTapGesture {
viewModel.refreshRecommendCharacters()
} }
}
.padding(.horizontal, 24)
let horizontalPadding: CGFloat = 24
let gridSpacing: CGFloat = 16
let width = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
LazyVGrid(
columns: Array(
repeating: GridItem(
.flexible(),
spacing: gridSpacing,
alignment: .topLeading
),
count: 2
),
alignment: .leading,
spacing: gridSpacing
) {
ForEach(viewModel.recommendCharacters.indices, id: \.self) { idx in
let character = viewModel.recommendCharacters[idx]
CharacterItemView(
character: character,
size: width,
rank: idx + 1,
isShowRank: false
) )
.onTapGesture { onSelectCharacter(character.characterId) }
} }
} }
.padding(.horizontal, horizontalPadding)
}
} }
} }
.padding(.bottom, 24) .padding(.bottom, 24)
@@ -97,7 +127,7 @@ struct CharacterView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center) .frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -15,7 +15,7 @@ final class CharacterViewModel: ObservableObject {
@Published private(set) var recentCharacters: [RecentCharacter] = [] @Published private(set) var recentCharacters: [RecentCharacter] = []
@Published private(set) var popularCharacters: [Character] = [] @Published private(set) var popularCharacters: [Character] = []
@Published private(set) var newCharacters: [Character] = [] @Published private(set) var newCharacters: [Character] = []
@Published private(set) var curations: [CurationSection] = [] @Published private(set) var recommendCharacters: [Character] = []
@Published var isLoading: Bool = false @Published var isLoading: Bool = false
@Published var errorMessage: String = "" @Published var errorMessage: String = ""
@@ -49,7 +49,46 @@ final class CharacterViewModel: ObservableObject {
self.recentCharacters = data.recentCharacters self.recentCharacters = data.recentCharacters
self.popularCharacters = data.popularCharacters self.popularCharacters = data.popularCharacters
self.newCharacters = data.newCharacters self.newCharacters = data.newCharacters
self.curations = data.curationSections.filter { !$0.characters.isEmpty } self.recommendCharacters = data.recommendCharacters
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
self.isLoading = false
} catch {
self.isLoading = false
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func refreshRecommendCharacters() {
isLoading = true
repository.refreshRecommendCharacters()
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<[Character]>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.recommendCharacters = data
} else { } else {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message

View File

@@ -1,12 +0,0 @@
//
// CurationSection.swift
// SodaLive
//
// Created by klaus on 8/29/25.
//
struct CurationSection: Decodable {
let characterCurationId: Int
let title: String
let characters: [Character]
}

View File

@@ -22,6 +22,7 @@ struct CharacterDetailResponse: Decodable {
let others: [OtherCharacter] let others: [OtherCharacter]
let latestComment: CharacterCommentResponse? let latestComment: CharacterCommentResponse?
let totalComments: Int let totalComments: Int
let translated: TranslatedAiCharacterDetail?
} }
enum CharacterType: String, Decodable { enum CharacterType: String, Decodable {
@@ -44,3 +45,22 @@ struct CharacterBackgroundResponse: Decodable {
let topic: String let topic: String
let description: String let description: String
} }
struct TranslatedAiCharacterDetail: Decodable {
let name: String?
let description: String?
let gender: String?
let personality: TranslatedAiCharacterPersonality?
let background: TranslatedAiCharacterBackground?
let tags: String?
}
struct TranslatedAiCharacterPersonality: Decodable {
let trait: String?
let description: String?
}
struct TranslatedAiCharacterBackground: Decodable {
let topic: String?
let description: String?
}

View File

@@ -25,8 +25,8 @@ struct CharacterDetailView: View {
var title: String { var title: String {
switch self { switch self {
case .detail: return "상세" case .detail: return I18n.Tab.detail
case .gallery: return "갤러리" case .gallery: return I18n.Tab.gallery
} }
} }
} }
@@ -34,7 +34,7 @@ struct CharacterDetailView: View {
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "캐릭터 정보") { DetailNavigationBar(title: String(localized: "캐릭터 정보")) {
if presentationMode.wrappedValue.isPresented { if presentationMode.wrappedValue.isPresented {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
} else { } else {
@@ -59,7 +59,7 @@ struct CharacterDetailView: View {
// //
if let backgrounds = viewModel.characterDetail?.backgrounds { if let backgrounds = viewModel.characterDetail?.backgrounds {
worldViewSection(backgrounds: backgrounds) worldViewSection(backgrounds: viewModel.characterDetail?.translated?.background?.description ?? backgrounds.description)
} }
// //
@@ -70,7 +70,7 @@ struct CharacterDetailView: View {
// //
if let personalities = viewModel.characterDetail?.personalities { if let personalities = viewModel.characterDetail?.personalities {
personalitySection(personalities: personalities) personalitySection(personalities: viewModel.characterDetail?.translated?.personality?.description ?? personalities.description)
} }
// //
@@ -78,7 +78,7 @@ struct CharacterDetailView: View {
VStack(spacing: 16) { VStack(spacing: 16) {
HStack { HStack {
Text("장르의 다른 캐릭터") Text("장르의 다른 캐릭터")
.font(.custom(Font.preBold.rawValue, size: 26)) .appFont(size: 26, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
@@ -93,7 +93,8 @@ struct CharacterDetailView: View {
characterId: otherCharacter.characterId, characterId: otherCharacter.characterId,
name: otherCharacter.name, name: otherCharacter.name,
description: otherCharacter.tags, description: otherCharacter.tags,
imageUrl: otherCharacter.imageUrl imageUrl: otherCharacter.imageUrl,
isNew: false
), ),
size: screenSize().width * 0.42, size: screenSize().width * 0.42,
rank: 0, rank: 0,
@@ -140,7 +141,7 @@ struct CharacterDetailView: View {
.frame(alignment: .center) .frame(alignment: .center)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.horizontal, 33.3) .padding(.horizontal, 33.3)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -203,8 +204,8 @@ extension CharacterDetailView {
{ {
HStack(spacing: 4) { HStack(spacing: 4) {
if let gender = viewModel.characterDetail?.gender { if let gender = viewModel.characterDetail?.gender {
Text(gender) Text(viewModel.characterDetail?.translated?.gender ?? gender)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor( .foregroundColor(
gender == "남성" ? gender == "남성" ?
Color.button : Color.button :
@@ -227,7 +228,7 @@ extension CharacterDetailView {
if let age = viewModel.characterDetail?.age { if let age = viewModel.characterDetail?.age {
Text("\(age)") Text("\(age)")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
.padding(.horizontal, 7) .padding(.horizontal, 7)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -243,7 +244,7 @@ extension CharacterDetailView {
if let mbti = viewModel.characterDetail?.mbti { if let mbti = viewModel.characterDetail?.mbti {
Text(mbti) Text(mbti)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
.padding(.horizontal, 7) .padding(.horizontal, 7)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -261,8 +262,8 @@ extension CharacterDetailView {
// //
HStack(spacing: 8) { HStack(spacing: 8) {
Text(viewModel.characterDetail?.name ?? "") Text(viewModel.characterDetail?.translated?.name ?? viewModel.characterDetail?.name ?? "")
.font(.custom(Font.preBold.rawValue, size: 26)) .appFont(size: 26, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
@@ -270,7 +271,7 @@ extension CharacterDetailView {
if let characterType = viewModel.characterDetail?.characterType { if let characterType = viewModel.characterDetail?.characterType {
HStack(spacing: 8) { HStack(spacing: 8) {
Text(characterType.rawValue) Text(characterType.rawValue)
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 5) .padding(.horizontal, 5)
.padding(.vertical, 1) .padding(.vertical, 1)
@@ -281,12 +282,12 @@ extension CharacterDetailView {
} }
// //
Text(viewModel.characterDetail?.description ?? "") Text(viewModel.characterDetail?.translated?.description ?? viewModel.characterDetail?.description ?? "")
.font(.custom(Font.preRegular.rawValue, size: 18)) .appFont(size: 18, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
Text(viewModel.characterDetail?.tags ?? "") Text(viewModel.characterDetail?.translated?.tags ?? viewModel.characterDetail?.tags ?? "")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "3BB9F1")) .foregroundColor(Color(hex: "3BB9F1"))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
@@ -296,17 +297,17 @@ extension CharacterDetailView {
// MARK: - World View Section // MARK: - World View Section
extension CharacterDetailView { extension CharacterDetailView {
private func worldViewSection(backgrounds: CharacterBackgroundResponse) -> some View { private func worldViewSection(backgrounds: String) -> some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Text("[세계관 및 작품 소개]") Text("[세계관 및 작품 소개]")
.font(.custom(Font.preBold.rawValue, size: 18)) .appFont(size: 18, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
} }
CharacterExpandableTextView(text: backgrounds.description) CharacterExpandableTextView(text: backgrounds)
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
} }
@@ -318,7 +319,7 @@ extension CharacterDetailView {
VStack(spacing: 8) { VStack(spacing: 8) {
HStack { HStack {
Text("원작") Text("원작")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(.white) .foregroundColor(.white)
@@ -327,7 +328,7 @@ extension CharacterDetailView {
HStack { HStack {
Text(title) Text(title)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
Spacer() Spacer()
@@ -339,7 +340,7 @@ extension CharacterDetailView {
} }
}) { }) {
Text("원작 보러가기") Text("원작 보러가기")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(Color(hex: "3BB9F1")) .foregroundColor(Color(hex: "3BB9F1"))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -356,23 +357,23 @@ extension CharacterDetailView {
// MARK: - Personality Section // MARK: - Personality Section
extension CharacterDetailView { extension CharacterDetailView {
private func personalitySection(personalities: CharacterPersonalityResponse) -> some View { private func personalitySection(personalities: String) -> some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
Text("[성격 및 특징]") Text("[성격 및 특징]")
.font(.custom(Font.preBold.rawValue, size: 18)) .appFont(size: 18, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
} }
CharacterExpandableTextView(text: personalities.description) CharacterExpandableTextView(text: personalities)
// //
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack { HStack {
Text("⚠️ 캐릭터톡 대화 가이드") Text("⚠️ 캐릭터톡 대화 가이드")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
Spacer() Spacer()
@@ -381,7 +382,7 @@ extension CharacterDetailView {
Text(""" Text("""
보이스온의 오픈월드 캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다. 세계관 속 연관 캐릭터가 되어 대화를 하거나 완전히 새로운 인물이 되어 캐릭터와 당신만의 스토리를 만들어 갈 수 있습니다. 보이스온의 오픈월드 캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다. 세계관 속 연관 캐릭터가 되어 대화를 하거나 완전히 새로운 인물이 되어 캐릭터와 당신만의 스토리를 만들어 갈 수 있습니다.
""") """)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "AEAEB2")) .foregroundColor(Color(hex: "AEAEB2"))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -389,7 +390,7 @@ extension CharacterDetailView {
오픈월드 캐릭터톡은 캐릭터를 정교하게 설계하였지만, 대화가 어색하거나 불완전할 수도 있습니다. 오픈월드 캐릭터톡은 캐릭터를 정교하게 설계하였지만, 대화가 어색하거나 불완전할 수도 있습니다.
대화 도중 캐릭터의 대화가 이상하거나 새로운 캐릭터로 대화를 나누고 싶다면 대화를 초기화 하고 새롭게 캐릭터와 대화를 나눠보세요. 대화 도중 캐릭터의 대화가 이상하거나 새로운 캐릭터로 대화를 나누고 싶다면 대화를 초기화 하고 새롭게 캐릭터와 대화를 나눠보세요.
""") """)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "AEAEB2")) .foregroundColor(Color(hex: "AEAEB2"))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
@@ -411,7 +412,7 @@ extension CharacterDetailView {
extension CharacterDetailView { extension CharacterDetailView {
private var chatButton: some View { private var chatButton: some View {
Text("대화하기") Text("대화하기")
.font(.custom(Font.preBold.rawValue, size: 18)) .appFont(size: 18, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 54) .frame(height: 54)
@@ -442,7 +443,7 @@ struct CharacterExpandableTextView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(text) Text(text)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(isExpanded ? nil : 3) .lineLimit(isExpanded ? nil : 3)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@@ -463,12 +464,12 @@ struct CharacterExpandableTextView: View {
Spacer() Spacer()
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
.font(.system(size: 16)) .appFont(size: 16)
.foregroundColor(Color(hex: "607D8B")) .foregroundColor(Color(hex: "607D8B"))
.rotationEffect(.degrees(isExpanded ? 180 : 0)) .rotationEffect(.degrees(isExpanded ? 180 : 0))
Text(isExpanded ? "간략히" : "더보기") Text(isExpanded ? "간략히" : "더보기")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "607D8B")) .foregroundColor(Color(hex: "607D8B"))
} }
.onTapGesture { .onTapGesture {

View File

@@ -54,14 +54,14 @@ final class CharacterDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self?.errorMessage = message self?.errorMessage = message
} else { } else {
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
} }
self?.isShowPopup = true self?.isShowPopup = true
} }
} catch { } catch {
ERROR_LOG(String(describing: error)) ERROR_LOG(String(describing: error))
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
self?.isShowPopup = true self?.isShowPopup = true
} }
} }
@@ -94,14 +94,14 @@ final class CharacterDetailViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self?.errorMessage = message self?.errorMessage = message
} else { } else {
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
} }
self?.isShowPopup = true self?.isShowPopup = true
} }
} catch { } catch {
ERROR_LOG(String(describing: error)) ERROR_LOG(String(describing: error))
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
self?.isShowPopup = true self?.isShowPopup = true
} }
} }

View File

@@ -54,7 +54,7 @@ struct CharacterDetailGalleryView: View {
.frame(alignment: .center) .frame(alignment: .center)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.horizontal, 33.3) .padding(.horizontal, 33.3)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -78,11 +78,11 @@ struct CharacterDetailGalleryView: View {
.overlay { .overlay {
if viewModel.isShowPurchaseDialog { if viewModel.isShowPurchaseDialog {
SodaDialog( SodaDialog(
title: "구매 확인", title: I18n.CharacterDetailGallery.purchaseConfirmTitle,
desc: "선택한 이미지를 구매하시겠습니까?", desc: I18n.CharacterDetailGallery.purchaseConfirmDescription,
confirmButtonTitle: viewModel.selectedItemPrice, confirmButtonTitle: viewModel.selectedItemPrice,
confirmButtonAction: viewModel.onPurchaseConfirm, confirmButtonAction: viewModel.onPurchaseConfirm,
cancelButtonTitle: "취소", cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: viewModel.onPurchaseCancel cancelButtonAction: viewModel.onPurchaseCancel
) )
} }
@@ -95,22 +95,22 @@ struct CharacterDetailGalleryView: View {
// ( % , , ) // ( % , , )
HStack { HStack {
Text("\(viewModel.ownershipPercentage)% 보유중") Text("\(viewModel.ownershipPercentage)% 보유중")
.font(.custom(Font.preBold.rawValue, size: 18)) .appFont(size: 18, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Spacer() Spacer()
HStack(spacing: 4) { HStack(spacing: 4) {
Text("\(viewModel.ownedCount)") Text("\(viewModel.ownedCount)")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(Color(hex: "#FDD453")) .foregroundColor(Color(hex: "#FDD453"))
Text("/") Text("/")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
Text("\(viewModel.totalCount)") Text("\(viewModel.totalCount)")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }
@@ -181,7 +181,7 @@ struct CharacterDetailGalleryView: View {
.frame(width: 16) .frame(width: 16)
Text("\(item.imagePriceCan)") Text("\(item.imagePriceCan)")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "#263238")) .foregroundColor(Color(hex: "#263238"))
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)

View File

@@ -44,7 +44,7 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
} }
var selectedItemPrice: String { var selectedItemPrice: String {
return "\(selectedItem?.imagePriceCan ?? 0)캔으로 구매" return I18n.CharacterDetailGallery.purchaseWithCans(selectedItem?.imagePriceCan ?? 0)
} }
// MARK: - Public Methods // MARK: - Public Methods
@@ -112,7 +112,7 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
} else { } else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
} }
self.isShowPopup = true self.isShowPopup = true
@@ -124,7 +124,7 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
self.selectedImageIndex = 0 self.selectedImageIndex = 0
self.selectedItem = nil self.selectedItem = nil
ERROR_LOG(String(describing: error)) ERROR_LOG(String(describing: error))
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.errorMessage = I18n.Common.commonError
self.isShowPopup = true self.isShowPopup = true
} }
} }
@@ -172,14 +172,14 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self?.errorMessage = message self?.errorMessage = message
} else { } else {
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
} }
self?.isShowPopup = true self?.isShowPopup = true
} }
} catch { } catch {
ERROR_LOG(String(describing: error)) ERROR_LOG(String(describing: error))
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
self?.isShowPopup = true self?.isShowPopup = true
} }
} }
@@ -219,14 +219,14 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
if let message = decoded.message { if let message = decoded.message {
self?.errorMessage = message self?.errorMessage = message
} else { } else {
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
} }
self?.isShowPopup = true self?.isShowPopup = true
} }
} catch { } catch {
ERROR_LOG(String(describing: error)) ERROR_LOG(String(describing: error))
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self?.errorMessage = I18n.Common.commonError
self?.isShowPopup = true self?.isShowPopup = true
} }
} }

View File

@@ -18,19 +18,19 @@ struct NewCharacterListView: View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 8) { VStack(spacing: 8) {
// Toolbar // Toolbar
DetailNavigationBar(title: "신규 캐릭터 전체보기") DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
// n // n
HStack(spacing: 0) { HStack(spacing: 0) {
Text("전체 ") Text("전체")
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2")) .foregroundColor(Color(hex: "e2e2e2"))
Text("\(viewModel.totalCount)") Text(" \(viewModel.totalCount)")
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "ff5c49")) .foregroundColor(Color(hex: "ff5c49"))
Text("") Text("")
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2")) .foregroundColor(Color(hex: "e2e2e2"))
Spacer() Spacer()
} }
@@ -99,7 +99,7 @@ struct NewCharacterListView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center) .frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -20,7 +20,7 @@ struct RecentCharacterItemView: View {
.clipShape(Circle()) .clipShape(Circle())
Text(character.name) Text(character.name)
.font(.custom(Font.preRegular.rawValue, size: 18)) .appFont(size: 18, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.frame(maxWidth: 76) .frame(maxWidth: 76)

View File

@@ -16,11 +16,11 @@ struct RecentCharacterSectionView: View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("최근 대화한 캐릭터 ") Text("최근 대화한 캐릭터 ")
.font(.custom(Font.preBold.rawValue, size: 20)) .appFont(size: 20, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Text("\(titleCount)") Text("\(titleCount)")
.font(.custom(Font.preBold.rawValue, size: 20)) .appFont(size: 20, weight: .bold)
.foregroundColor(Color(hex: "FDCA2F")) .foregroundColor(Color(hex: "FDCA2F"))
Spacer() Spacer()

View File

@@ -14,14 +14,18 @@ struct ChatTabView: View {
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token) @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth) @AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
@StateObject var mypageViewModel = MyPageViewModel()
private enum InnerTab: Int, CaseIterable { private enum InnerTab: Int, CaseIterable {
case character = 0 case character = 0
case original = 1
case talk = 2 case talk = 2
var title: String { var title: String {
switch self { switch self {
case .character: return "캐릭터" case .character: return I18n.Tab.character
case .talk: return "" case .original: return I18n.Tab.work
case .talk: return I18n.Tab.talk
} }
} }
} }
@@ -103,6 +107,13 @@ struct ChatTabView: View {
onTap: { if selectedTab != .character { selectedTab = .character } } onTap: { if selectedTab != .character { selectedTab = .character } }
) )
ChatInnerTab(
title: InnerTab.original.title,
isSelected: selectedTab == .original,
onTap: { if selectedTab != .original { selectedTab = .original } }
)
ChatInnerTab( ChatInnerTab(
title: InnerTab.talk.title, title: InnerTab.talk.title,
isSelected: selectedTab == .talk, isSelected: selectedTab == .talk,
@@ -115,6 +126,8 @@ struct ChatTabView: View {
switch selectedTab { switch selectedTab {
case .character: case .character:
CharacterView(onSelectCharacter: handleCharacterSelection, onSelectNewCharacterAll: handleCharacterSelection) CharacterView(onSelectCharacter: handleCharacterSelection, onSelectNewCharacterAll: handleCharacterSelection)
case .original:
OriginalTabView()
case .talk: case .talk:
TalkView() TalkView()
} }
@@ -142,7 +155,9 @@ struct ChatTabView: View {
AppState.shared.isShowErrorPopup = true AppState.shared.isShowErrorPopup = true
isShowAuthView = false isShowAuthView = false
} }
.onDone { _ in .onDone {
DEBUG_LOG("onDone: \($0)")
mypageViewModel.authVerify($0) {
auth = true auth = true
isShowAuthView = false isShowAuthView = false
if let action = pendingAction { if let action = pendingAction {
@@ -150,6 +165,7 @@ struct ChatTabView: View {
action() action()
} }
} }
}
.onClose { .onClose {
isShowAuthView = false isShowAuthView = false
} }
@@ -188,12 +204,7 @@ struct ChatInnerTab: View {
Spacer() Spacer()
Text(title) Text(title)
.font( .appFont(size: 18, weight: isSelected ? .bold : .regular)
.custom(
isSelected ? Font.preBold.rawValue : Font.preRegular.rawValue,
size: 18
)
)
.foregroundColor(Color(hex: isSelected ? "3bb9f1" : "b0bec5")) .foregroundColor(Color(hex: isSelected ? "3bb9f1" : "b0bec5"))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)

View File

@@ -24,14 +24,15 @@ struct OriginalWorkDetailHeaderView: View {
.cornerRadius(16) .cornerRadius(16)
} }
Text(item.title) //
.font(.custom(Font.preBold.rawValue, size: 26)) Text(item.translated?.title ?? item.title)
.appFont(size: 26, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.top, 40) .padding(.top, 40)
HStack(spacing: 4) { HStack(spacing: 4) {
Text(item.contentType) Text(item.translated?.contentType ?? item.contentType)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
.padding(.horizontal, 7) .padding(.horizontal, 7)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -43,8 +44,8 @@ struct OriginalWorkDetailHeaderView: View {
.foregroundColor(.white) .foregroundColor(.white)
} }
Text(item.category) Text(item.translated?.category ?? item.category)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.button) .foregroundColor(.button)
.padding(.horizontal, 7) .padding(.horizontal, 7)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -58,7 +59,7 @@ struct OriginalWorkDetailHeaderView: View {
if item.isAdult { if item.isAdult {
Text("19+") Text("19+")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "ff5c49")) .foregroundColor(Color(hex: "ff5c49"))
.padding(.horizontal, 7) .padding(.horizontal, 7)
.padding(.vertical, 3) .padding(.vertical, 3)
@@ -74,11 +75,11 @@ struct OriginalWorkDetailHeaderView: View {
.padding(.top, 14) .padding(.top, 14)
Text( Text(
item.tags (item.translated?.tags ?? item.tags)
.map { $0.hasPrefix("#") ? $0 : "#\($0)" } .map { $0.hasPrefix("#") ? $0 : "#\($0)" }
.joined(separator: " ") .joined(separator: " ")
) )
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.top, 14) .padding(.top, 14)
@@ -101,7 +102,8 @@ struct OriginalWorkDetailHeaderView: View {
studio: nil, studio: nil,
originalLinks: [], originalLinks: [],
tags: [], tags: [],
characters: [] characters: [],
translated: nil
) )
) )
} }

View File

@@ -19,4 +19,13 @@ struct OriginalWorkDetailResponse: Decodable {
let originalLinks: [String] let originalLinks: [String]
let tags: [String] let tags: [String]
let characters: [Character] let characters: [Character]
let translated: TranslatedOriginalWork?
}
struct TranslatedOriginalWork: Decodable {
let title: String
let contentType: String
let category: String
let description: String
let tags: [String]
} }

View File

@@ -53,7 +53,7 @@ struct OriginalWorkDetailView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
SeriesDetailTabView( SeriesDetailTabView(
title: "캐릭터", title: I18n.Tab.character,
width: screenSize().width / 2, width: screenSize().width / 2,
isSelected: viewModel.currentTab == .character isSelected: viewModel.currentTab == .character
) { ) {
@@ -63,7 +63,7 @@ struct OriginalWorkDetailView: View {
} }
SeriesDetailTabView( SeriesDetailTabView(
title: "작품정보", title: I18n.Tab.workInfo,
width: screenSize().width / 2, width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info isSelected: viewModel.currentTab == .info
) { ) {
@@ -157,11 +157,11 @@ struct OriginalWorkInfoView: View {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("작품 소개") Text("작품 소개")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
Text(response.description) Text(response.translated?.description ?? response.description)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(isExpandDesc ? Int.max : 3) .lineLimit(isExpandDesc ? Int.max : 3)
.truncationMode(.tail) .truncationMode(.tail)
@@ -176,7 +176,7 @@ struct OriginalWorkInfoView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("원작 보러 가기") Text("원작 보러 가기")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
@@ -185,7 +185,7 @@ struct OriginalWorkInfoView: View {
let link = response.originalLinks[$0] let link = response.originalLinks[$0]
Text(link) Text(link)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.onTapGesture { .onTapGesture {
if let url = URL(string: link) { if let url = URL(string: link) {
@@ -203,26 +203,26 @@ struct OriginalWorkInfoView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("상세 정보") Text("상세 정보")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
HStack(spacing: 16) { HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
if let _ = response.writer { if let _ = response.writer {
Text("작가") Text("작가")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
} }
if let _ = response.studio { if let _ = response.studio {
Text("제작사") Text("제작사")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
} }
if let _ = response.originalWork { if let _ = response.originalWork {
Text("원작") Text("원작")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5")) .foregroundColor(Color(hex: "B0BEC5"))
} }
} }
@@ -230,19 +230,19 @@ struct OriginalWorkInfoView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
if let writer = response.writer { if let writer = response.writer {
Text(writer) Text(writer)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
if let studio = response.studio { if let studio = response.studio {
Text(studio) Text(studio)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
if let originalWork = response.originalWork { if let originalWork = response.originalWork {
Text(originalWork) Text(originalWork)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.underline(response.originalLink != nil ? true : false) .underline(response.originalLink != nil ? true : false)
.onTapGesture { .onTapGesture {

View File

@@ -21,13 +21,13 @@ struct OriginalTabItemView: View {
).cornerRadius(16) ).cornerRadius(16)
Text(item.title) Text(item.title)
.font(.custom(Font.preRegular.rawValue, size: 18)) .appFont(size: 18, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
Text(item.contentType) Text(item.contentType)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)

View File

@@ -76,7 +76,7 @@ struct OriginalTabView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center) .frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -46,13 +46,13 @@ struct ChatRoomView: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(viewModel.characterName) Text(viewModel.characterName)
.font(.custom(Font.preBold.rawValue, size: 12)) .appFont(size: 12, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
Text(viewModel.characterType.rawValue) Text(viewModel.characterType.rawValue)
.font(.custom(Font.preBold.rawValue, size: 10)) .appFont(size: 10, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 4) .padding(.horizontal, 4)
.padding(.vertical, 2) .padding(.vertical, 2)
@@ -73,7 +73,7 @@ struct ChatRoomView: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("\(can)") Text("\(can)")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -103,7 +103,7 @@ struct ChatRoomView: View {
? "보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다." ? "보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다."
: "AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다." : "AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다."
) )
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
Image(systemName: "chevron.up") Image(systemName: "chevron.up")
@@ -187,12 +187,12 @@ struct ChatRoomView: View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
if viewModel.messageText.isEmpty { if viewModel.messageText.isEmpty {
Text("메시지를 입력하세요.") Text("메시지를 입력하세요.")
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
} }
TextField("", text: $viewModel.messageText) TextField("", text: $viewModel.messageText)
.font(.custom(Font.preRegular.rawValue, size: 14)) .appFont(size: 14, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
.onSubmit { .onSubmit {
viewModel.sendMessage() viewModel.sendMessage()
@@ -227,13 +227,13 @@ struct ChatRoomView: View {
if let message = viewModel.selectedMessage, viewModel.selectedMessageIndex >= 0 { if let message = viewModel.selectedMessage, viewModel.selectedMessageIndex >= 0 {
SodaDialog( SodaDialog(
title: "잠금된 메시지", title: I18n.ChatRoom.lockedMessageTitle,
desc: "이 메시지를 \(message.price ?? 5)캔으로 잠금해제 하시겠습니까?", desc: I18n.ChatRoom.unlockMessageDescription(message.price ?? 5),
confirmButtonTitle: "잠금해제", confirmButtonTitle: I18n.ChatRoom.unlock,
confirmButtonAction: { confirmButtonAction: {
viewModel.purchaseChatMessage() viewModel.purchaseChatMessage()
}, },
cancelButtonTitle: "취소" cancelButtonTitle: I18n.Common.cancel
) { ) {
viewModel.selectedMessage = nil viewModel.selectedMessage = nil
viewModel.selectedMessageIndex = -1 viewModel.selectedMessageIndex = -1
@@ -265,14 +265,14 @@ struct ChatRoomView: View {
Color.black.opacity(0.7) Color.black.opacity(0.7)
SodaDialog( SodaDialog(
title: "대화 초기화", title: I18n.ChatRoom.resetTitle,
desc: "지금까지의 대화가 모두 초기화 되고 새롭게 대화를 시작합니다.", desc: I18n.ChatRoom.resetDescription,
confirmButtonTitle: "30캔으로 초기화", confirmButtonTitle: I18n.ChatRoom.resetWithCans(30),
confirmButtonAction: { confirmButtonAction: {
viewModel.resetChatRoom() viewModel.resetChatRoom()
viewModel.isShowingChatResetConfirmDialog = false viewModel.isShowingChatResetConfirmDialog = false
}, },
cancelButtonTitle: "취소", cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: { cancelButtonAction: {
viewModel.isShowingChatResetConfirmDialog = false viewModel.isShowingChatResetConfirmDialog = false
} }
@@ -315,7 +315,7 @@ struct ChatRoomView: View {
Text(viewModel.errorMessage) Text(viewModel.errorMessage)
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center) .frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .appFont(size: 12, weight: .medium)
.background(Color.button) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -86,7 +86,7 @@ struct AiMessageItemView: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) { HStack(spacing: 4) {
Text(characterName) Text(characterName)
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -117,7 +117,7 @@ struct AiMessageItemView: View {
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
Text("\(message.price ?? 5)") Text("\(message.price ?? 5)")
.font(.custom(Font.preBold.rawValue, size: 16)) .appFont(size: 16, weight: .bold)
.foregroundColor(Color(hex: "263238")) .foregroundColor(Color(hex: "263238"))
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -131,7 +131,7 @@ struct AiMessageItemView: View {
} }
Text("눌러서 잠금해제") Text("눌러서 잠금해제")
.font(.custom(Font.preBold.rawValue, size: 18)) .appFont(size: 18, weight: .bold)
.foregroundColor(.white) .foregroundColor(.white)
} }
.frame(width: maxWidth, height: imageHeight) .frame(width: maxWidth, height: imageHeight)
@@ -162,7 +162,7 @@ struct AiMessageItemView: View {
// //
VStack { VStack {
Text(formatTime(from: message.createdAt)) Text(formatTime(from: message.createdAt))
.font(.custom(Font.preRegular.rawValue, size: 10)) .appFont(size: 10, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
@@ -185,7 +185,7 @@ struct AiMessageItemView: View {
// //
if !component.isEmpty { if !component.isEmpty {
result = result + Text(component) result = result + Text(component)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
} else { } else {
@@ -202,13 +202,13 @@ struct AiMessageItemView: View {
// ( ) // ( )
if !afterClose.isEmpty { if !afterClose.isEmpty {
result = result + Text(afterClose) result = result + Text(afterClose)
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
} else { } else {
// //
result = result + Text("(\(component)") result = result + Text("(\(component)")
.font(.custom(Font.preRegular.rawValue, size: 16)) .appFont(size: 16, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }

View File

@@ -1,18 +0,0 @@
//
// MessageInputView.swift
// SodaLive
//
// Created by klaus on 9/3/25.
//
import SwiftUI
struct MessageInputView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
MessageInputView()
}

View File

@@ -37,7 +37,7 @@ struct TypingIndicatorItemView: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) { HStack(spacing: 4) {
Text(characterName) Text(characterName)
.font(.custom(Font.preRegular.rawValue, size: 12)) .appFont(size: 12, weight: .regular)
.foregroundColor(.white) .foregroundColor(.white)
} }

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