Compare commits

58 Commits

Author SHA1 Message Date
Yu Sung
afca24c221 docs(plan): 라이브 이미지 관련 작업 계획 문서를 추가한다 2026-03-17 17:05:57 +09:00
Yu Sung
606470ae04 feat(image): 이미지 선택 후 크롭 편집 흐름을 안정화한다 2026-03-17 17:05:22 +09:00
Yu Sung
039355c088 fix(profile): 프로필 후원 랭킹 전체보기 왕관 UI 위치와 크기를 통일한다 2026-03-17 16:11:55 +09:00
Yu Sung
5e0f6fd3e3 fix(profile): 프로필 후원 랭킹 왕관 UI를 홈과 동일하게 조정한다 2026-03-17 15:57:59 +09:00
Yu Sung
99fcf3a94c feat(image): 이미지 선택 후 크롭 편집 흐름을 적용한다 2026-03-17 14:42:31 +09:00
Yu Sung
408c3b7619 fix(community): 게시글 고정 표시인 pin 크기를 20x20으로 수정 2026-03-17 11:58:40 +09:00
Yu Sung
37e361b1e9 fix(community): 유료 미구매 게시물 롱프레스 메뉴 노출을 차단한다 2026-03-17 11:29:05 +09:00
Yu Sung
2b20e7a9a3 chore(docs): 커밋 검증 로그를 기록한다 2026-03-17 10:42:17 +09:00
Yu Sung
5e08711b29 feat(community): 크리에이터 커뮤니티 게시물 고정 기능을 추가한다 2026-03-17 10:41:28 +09:00
Yu Sung
de627e1700 fix(deeplink): 커뮤니티 댓글 딥링크를 보강한다 2026-03-13 21:39:45 +09:00
Yu Sung
3d4f67dbd5 fix(ui): 탭 상단 로고 영역 간격을 통일한다 2026-03-13 17:55:15 +09:00
Yu Sung
82889f405a fix(image) - 메시지 페이지 이동 아이콘 변경 2026-03-13 17:37:28 +09:00
Yu Sung
4d39a07fbd fix(live): 라이브 종료 시 참여자 블랙 스크린을 방지한다 2026-03-13 17:23:45 +09:00
Yu Sung
026f855bc5 fix(live): 종료 라이브 토스트를 노출한다 2026-03-13 16:49:44 +09:00
Yu Sung
fb85f3e90c refactor(toast): 공통 토스트 모디파이어를 적용한다 2026-03-13 16:32:57 +09:00
Yu Sung
19f5cc8ad6 fix(notification): 알림 리스트 라이브 이동 분기를 보정한다 2026-03-13 15:45:39 +09:00
Yu Sung
abe939e768 feat(notification): 알림 수신 설정 페이지와 이동 경로를 추가한다 2026-03-13 13:56:59 +09:00
Yu Sung
d5d5d97c2a fix(notification): 푸시 딥링크 우선 실행 분기를 보정한다 2026-03-13 11:34:10 +09:00
Yu Sung
af8813685e feat(notification): 알림함 진입 및 딥링크 라우팅을 추가한다 2026-03-12 18:35:43 +09:00
Yu Sung
2b58a0147b fix(community): 유료 잠금 배경색을 조정한다 2026-03-09 10:20:25 +09:00
Yu Sung
4fc7f6a39a fix(community): 세로 여백 조정 2026-03-06 19:21:59 +09:00
Yu Sung
cab9795557 fix(navigation): 라이브 재생 중 외부 이동을 확인 후 처리한다 2026-03-06 18:56:49 +09:00
Yu Sung
33f9ddfd12 fix(live): 프로필 라이브 상세를 프로필 화면에서 표시한다 2026-03-06 18:13:09 +09:00
Yu Sung
298c02b83f feat(live): 라이브 상세를 전역 바텀시트로 표시한다 2026-03-06 17:46:26 +09:00
Yu Sung
42ce09d927 refactor(navigation): 전역 경로 기반 단일 내비게이션 흐름으로 전환한다 2026-03-06 16:34:44 +09:00
Yu Sung
f145de87aa feat(community): 커뮤니티 보기 전환 탭과 아이콘을 추가한다 2026-03-06 14:25:04 +09:00
Yu Sung
d29e23b9cf fix(live): 라이브룸 팔로우 버튼 스타일을 정렬한다 2026-03-05 15:41:37 +09:00
Yu Sung
ca565a2b5f feat(live): 라이브룸 게스트 상단에 팔로우 버튼과 알림 옵션을 추가한다 2026-03-05 10:55:55 +09:00
Yu Sung
f0763d75c2 fix(community): 커뮤니티 전체 아이템 말줄임과 폰트를 정렬, 텍스트 확장 동작을 개선한다 2026-03-04 17:32:56 +09:00
Yu Sung
9d6f0c648b fix(profile): 프로필 소셜 URL 필드를 신규 명세로 정리한다 2026-02-27 13:45:49 +09:00
Yu Sung
38fb818f4b fix(detail): 콘텐츠/시리즈 상세 로딩 실패 시 자동 복귀를 적용한다 2026-02-26 01:20:37 +09:00
Yu Sung
db68aa90d2 fix(profile): 채널 후원 비밀 문구를 분리하고 자기 프로필 후원 버튼을 숨긴다 2026-02-26 00:44:37 +09:00
Yu Sung
b84b996059 fix(profile): 차단 유저 프로필 진입 실패 시 자동 복귀를 추가한다 2026-02-25 22:51:06 +09:00
Yu Sung
a4d6de83db fix(report): 사용자 차단 다이얼로그 문구를 국제화한다 2026-02-25 22:30:22 +09:00
Yu Sung
c7ec9045ff feat(profile): 크리에이터 상세정보에서 닉네임의 크기 32, SNS 아이콘 margin 16 2026-02-25 21:33:58 +09:00
Yu Sung
32d1d970e4 feat(explorer): 채널 후원 목록/등록 기능을 추가한다 2026-02-25 20:57:23 +09:00
Yu Sung
e9bd1e7396 feat(explorer): 크리에이터 상세정보 다이얼로그와 SNS 링크를 추가한다 2026-02-25 16:28:48 +09:00
Yu Sung
7ff9360b1e fix(live-room): 유료 라이브 최소 30캔 검증을 추가한다 2026-02-25 15:30:38 +09:00
Yu Sung
aaffd08cb5 docs(commit-policy): 커밋 정책 스킬과 검증 절차를 정비한다 2026-02-25 14:58:12 +09:00
Yu Sung
b796f6d9c5 라이브룸 V2V 번역 자막 기능을 추가한다
라이브룸에서 진행자 언어와 기기 언어가 다를 때 자막 토글을 제공한다.
룸 정보 응답에 V2V 워커 토큰과 진행자 언어 코드를 포함한다.
Agora V2V 에이전트 참여와 종료 API 연동을 추가한다
2026-02-09 21:11:17 +09:00
Yu Sung
7f703024d8 룰렛 설정 입력 가시성과 프로필 메뉴 레이아웃 개선
룰렛 설정에서 입력 필드 포커스 시 항목을 중앙으로 이동한다.
키보드에 가려지지 않도록 입력 가시성을 높인다.
프로필 메뉴 오버레이의 하단 안전영역 패딩을 제거한다.
2026-02-09 17:43:45 +09:00
Yu Sung
7cba6de2fc 프로필 상단 탐색 사용성을 개선한다 2026-02-09 17:24:54 +09:00
Yu Sung
5e0899419e 응원 항목 너비가 화면 폭에 맞게 표시된다
프로필 팬톡의 응원 항목이 기기 화면 폭에 맞춰 표시되어
콘텐츠 정렬이 안정적으로 보인다
2026-02-09 11:02:54 +09:00
Yu Sung
68976e221c 라이브 방이 19금일 때 제목 앞에 🔞 대신 방패(ic_shield)가 표시되도록 수정 2026-02-04 17:41:55 +09:00
Yu Sung
3590db82be 라이브 룸 - 라이브 크리에이터 프로필 영역에 팔로우 버튼 제거 2026-02-04 16:58:14 +09:00
Yu Sung
3456510eec 팬톡 TextField가 Focus 되었을 때 자판이 해당 영역을 가리는 버그 수정 2026-02-04 16:12:37 +09:00
Yu Sung
8d3aed41c2 크리 채널 커뮤니티 등록 - 썸네일 선택시 보이지 않던 버그 수정 2026-02-04 15:53:30 +09:00
Yu Sung
c0288b5eb8 크리에이터 채널
- 후원랭킹이 없어도 내 채널에서는 후원랭킹 영역이 보이도록 수정
- 채널 주인이 아닌 다른 유저는 커뮤니티 게시물이 없으면 커뮤니티 영역이 보이지 않도록 수정
2026-02-04 11:26:04 +09:00
Yu Sung
13f8d924c0 기부 랭킹 기간 선택 추가
프로필 기부 랭킹 조회와 프로필 갱신 요청에\n기간 값을 전달한다.
2026-02-03 18:38:36 +09:00
Yu Sung
d686223362 연령 제한에 따른 성별 제한 전송 조정 2026-02-03 14:09:59 +09:00
Yu Sung
652fe3dc13 라이브 전체보기 - 성인 라이브 입장에 본인인증 흐름 추가 2026-02-03 11:28:13 +09:00
Yu Sung
36bf533269 라이브 상세, 라이브 룸 - 19금 표시를 이모지로 변경 2026-02-02 19:00:00 +09:00
Yu Sung
5159debf7f 라이브 성별 제한 옵션 추가
라이브 생성과 수정 요청에 성별 제한 값을 포함한다.
라이브 정보 조회 응답에 성별 제한 값을 제공한다.
2026-02-02 18:20:26 +09:00
Yu Sung
b985af4497 성인 라이브 입장에 본인인증 흐름 추가
라이브 지금 항목 탭을 상위에서 처리 가능하도록 노출
2026-02-02 11:48:12 +09:00
Yu Sung
9e97c301b8 지금 라이브 중 19금 방송 방패 표시 2026-01-30 18:03:00 +09:00
Yu Sung
f9d84efbe1 라이브 프로필 이미지 크기 비율 조정 2026-01-30 17:48:07 +09:00
Yu Sung
5352d28fe3 지금 라이브 중 아이템 하단에 크리에이터가 설정한 언어와 관심사 1개 랜덤 표시 2026-01-30 17:43:33 +09:00
Yu Sung
26f67028cf 라이브 전체보기 그리드 레이아웃 조정 2026-01-30 16:42:00 +09:00
255 changed files with 9513 additions and 3415 deletions

1
.gitignore vendored
View File

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

View File

@@ -0,0 +1,21 @@
---
description: commit-policy 스킬을 로드해 커밋 메시지 생성과 전후 검증을 수행한다
agent: build
subtask: true
---
작업 목표:
현재 변경사항을 안전하게 커밋한다.
필수 시작 단계:
1. `skill` 도구로 `commit-policy` 스킬을 먼저 로드한다.
- `skill({ name: "commit-policy" })`
실행 단계:
1. 로드한 `commit-policy` 스킬의 Hard Requirements와 Execution Flow를 그대로 수행한다.
2. `AGENTS.md`의 최소 정책(형식/한글 description/검증 스크립트)을 항상 만족한다.
3. `$ARGUMENTS`가 있으면 scope 또는 description 의도에 반영하되, 스킬 규칙과 형식을 깨지 않는다.
4. 마지막에 실행 명령과 pre-check/post-check PASS/FAIL 핵심 결과를 간단히 보고한다.
추가 사용자 의도:
$ARGUMENTS

View File

@@ -0,0 +1,46 @@
---
name: commit-policy
description: Apply this skill for any git commit task in this repository. It enforces commit message format and validation flow defined in AGENTS.md and work/scripts/check-commit-message-rules.sh, including pre-commit and post-commit verification.
---
# Commit Policy Skill
Use this workflow whenever the task includes creating a commit.
## Required References
- `@AGENTS.md`
- `@work/scripts/check-commit-message-rules.sh`
## Hard Requirements
1. Use commit subject format: `<type>(scope): <description>`.
2. `type` must be lowercase (for example `feat`, `fix`, `chore`, `docs`, `refactor`, `test`).
3. `description` must include Korean text and stay concise in imperative present tone.
4. Optional footer must use `Refs: #123` or `Refs: #123, #456` format.
5. Never commit secret files (`.env`, key/token/secret credential files).
6. Never bypass hooks with `--no-verify`.
## Execution Flow
1. Inspect context with:
- `git status`
- `git diff --cached`
- `git diff`
- `git log -5 --oneline`
2. Stage commit target files only. Exclude suspicious secret-bearing files.
3. Draft commit message from the change intent (focus on why, not only what).
4. Run pre-commit validation with the full draft message:
- `./work/scripts/check-commit-message-rules.sh --message "<full message>"`
5. If validation fails, revise message and re-run until PASS.
6. Commit using the validated message.
7. Run post-commit validation:
- `./work/scripts/check-commit-message-rules.sh`
8. Report executed commands and PASS/FAIL summary.
## Output Checklist
- Final commit subject.
- Whether pre-check passed.
- Whether post-check passed.
- Any excluded files and reason.

174
AGENTS.md
View File

@@ -1,18 +1,160 @@
질문에 대한 답변과 설명은 한국어로 한다. # AGENTS.md
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
## Quality Assurance Guidelines ## 커뮤니케이션 규칙
- **"질문에 대한 답변과 설명은 한국어로 한다."**
- 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
- 코드 식별자, 파일 경로, 명령어는 원문(영문) 그대로 유지한다.
### Commit Standards ## 저장소 범위
1. Write in Korean. - 앱 소스: `SodaLive/Sources/**`
2. Use the present tense in the subject line (e.g., "Add feature" not "Added feature"). - 프로젝트/스킴: `SodaLive.xcodeproj`, `SodaLive.xcworkspace`
3. Keep the subject line to 50 characters or less. - 의존성 설정: `Podfile`, `Podfile.lock`
4. Add a blank line between the subject and body. - 운영 스크립트: `work/scripts/**`
5. Keep the body to 72 characters or less per line. - 생성/외부 결과물: `Pods/**`, `generated/**`, `build/**`
6. Within a paragraph, only break lines when the text exceeds 72 characters. - 작업 계획 문서: `docs/**`
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. - 기능 변경은 `SodaLive/Sources/**`에서 해결한다.
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. - `Pods/**`, `generated/**`는 직접 수정하지 않는다.
12. Use separate git commands to stage files before committing. - `build/**`는 빌드 산출물로 간주하며 수정 대상이 아니다.
13. Always validate commits using `work/scripts/check-commit-message-rules.sh` and fix until validation passes.
## 빌드/테스트/검증 명령
아래 명령은 현재 저장소에서 확인된 공식 진입점이다.
### 1) 의존성 설치
- `pod install`
- 근거: `Podfile`에 CocoaPods 타깃(`SodaLive`, `SodaLive-dev`) 정의.
### 2) 스킴/타깃 확인
- `xcodebuild -workspace "SodaLive.xcworkspace" -list`
- 근거: 공유 스킴 `SodaLive`, `SodaLive-dev` 존재.
### 3) 빌드
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
### 4) 테스트(전체)
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
### 5) 단일 테스트 실행
- 일반 형식(테스트 타깃이 있는 경우):
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -only-testing:"SodaLiveTests/TestClass/testMethod" test`
- **현재 주의사항**:
- `SodaLive.xcodeproj/project.pbxproj` 기준으로 앱 타깃 중심 구성이고 테스트 번들 타깃이 확인되지 않는다.
- 따라서 현재 상태에서는 단일 테스트 지정이 실질적으로 동작하지 않을 수 있다.
### 6) 린트/포맷
- 저장소에 공식 `swiftlint`/`swiftformat` 실행 스크립트는 확인되지 않았다.
- `generated/*.generated.swift``swiftlint:disable all` 주석은 존재하나, 이는 생성 코드 보호 목적이다.
- 린트 도구를 도입/추가하면 본 문서 명령 섹션을 즉시 갱신한다.
## 코드 스타일 가이드
### 아키텍처/레이어
- 기본 흐름은 `View -> ViewModel -> Repository -> Api(TargetType)`를 따른다.
- API는 `enum ...Api: TargetType`, 저장소는 `final class ...Repository` 형태를 우선 사용한다.
- 상태 모델은 `struct`/`enum` 중심으로 두고, 화면 상태는 `ObservableObject`에서 관리한다.
### 임포트 규칙
- 시스템 프레임워크(`Foundation`, `SwiftUI`, `Combine`)를 먼저 배치한다.
- 서드파티(`Moya`, `CombineMoya`, SDK들)는 이후 배치한다.
- import 그룹 사이에는 한 줄 공백으로 의미 단위를 분리한다.
### 포맷/구조
- 들여쓰기는 4칸 스페이스를 사용한다.
- 프로퍼티 선언, 비즈니스 로직, 헬퍼 메서드는 공백 줄로 구획한다.
- 클로저 체인은 줄바꿈해 가독성을 유지한다.
### 타입/상태 관리
- ViewModel은 `final class ...: ObservableObject` 패턴을 우선한다.
- View가 소유하는 객체는 `@StateObject`, 외부 주입 객체는 `@ObservedObject`를 사용한다.
- 네트워크 반환은 `AnyPublisher<Response, MoyaError>` 패턴을 기본으로 따른다.
### 네이밍 규칙
- 타입명은 PascalCase (`HomeViewModel`, `UserRepository`, `UserApi`).
- 변수/함수는 camelCase (`errorMessage`, `getMemberInfo`).
- 역할을 이름에 반영한다 (`*View`, `*ViewModel`, `*Repository`, `*Api`, `*Request`, `*Response`).
### 비동기/Combine 규칙
- 비동기 처리는 Combine의 `sink`, `receiveValue`, `.store(in: &subscription)` 패턴을 따른다.
- `sink` 완료 블록에서 `.failure`를 반드시 처리한다.
- 클로저 캡처는 상황에 맞게 `[weak self]` 또는 `[unowned self]`를 선택한다.
### 에러 처리 규칙
- 사용자 노출 오류는 `errorMessage`와 팝업 플래그(`isShowPopup`)로 일관되게 처리한다.
- JSON 파싱은 `do/catch + JSONDecoder` 패턴을 따른다.
- **신규 코드에서 빈 `catch`는 금지**하고, 최소한 로깅 또는 명시적 무시 사유를 남긴다.
### 로깅 규칙
- 디버그 로그는 `DEBUG_LOG`, 오류 로그는 `ERROR_LOG`를 사용한다.
- `print`는 임시 디버깅 목적 외 신규 코드에서 지양한다.
### 네트워크/헤더 규칙
- 공통 Moya 플러그인(`AuthPlugin`, `AcceptLanguagePlugin`) 흐름을 유지한다.
- 언어 헤더는 `LanguageHeaderProvider.current`를 기준으로 사용한다.
### 문자열/다국어
- 신규 사용자 노출 문자열은 가능하면 `I18n` 경로를 우선 사용한다.
- 다국어 기능 수정 시 `Settings/Language` 모듈과 `Accept-Language` 헤더 흐름을 함께 점검한다.
### 주석/문서화
- 자명한 코드에는 주석을 남기지 않는다.
- 복잡한 분기, 외부 제약, 부작용이 있는 로직에만 주석을 추가한다.
## Cursor/Copilot 규칙 반영
- 아래 파일 존재 여부를 확인해 AGENTS와 함께 유지한다.
- `.cursor/rules/**`
- `.cursorrules`
- `.github/copilot-instructions.md`
- 현재 저장소에서는 위 파일들이 확인되지 않았다.
- 추후 파일이 추가되면 AGENTS.md에 요약 규칙을 동기화한다.
- 충돌 우선순위 기본값:
- 범위가 더 구체적인 규칙이 우선한다(경로 특화 > 저장소 전역).
- Cursor: `.cursor/rules/**` > `.cursorrules` > `AGENTS.md`
- Copilot: `.github/instructions/**`(존재 시) > `.github/copilot-instructions.md` > `AGENTS.md`
## 커밋 메시지 규칙 (표준 Conventional Commits)
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test`)를 사용한다.
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
### 커밋 메시지 검증 절차
- `git commit` 직후 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
- 스크립트 결과가 `[FAIL]`이면 커밋 메시지를 수정한 뒤 다시 검증한다.
## 작업 절차 체크리스트
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
- 변경 중: 공개 API 스키마를 임의 변경하지 말고 작은 단위로 안전하게 수정한다.
- 변경 후: 영향 범위 파일에 대해 빌드/테스트/로그/다국어 키를 점검한다.
- 커밋 직후: `commit-policy` 스킬을 로드하고 메시지 검증 스크립트를 실행한다.
## 작업 계획 문서 규칙 (docs)
- 모든 작업 시작 전에 `docs` 폴더 아래 계획 문서를 먼저 생성하고, 해당 문서를 기준으로 구현한다.
- 계획 문서 파일명은 `[날짜]_구현할내용한글.md` 형식을 사용한다.
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
- 구현 항목은 기능/작업 단위 체크박스(`- [ ]`)로 작성하고 완료 즉시 `- [x]`로 갱신한다.
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
- 결과 보고 시 문서 하단에 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)을 한국어로 남긴다.
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰기 없이 누적한다.
## 문서 유지보수 규칙
- 불확실한 규칙은 추측으로 채우지 말고 근거 파일 경로를 먼저 확인한다.
- 에이전트 안내 문구는 한국어 중심으로 유지한다.
- 명령/경로/타깃명이 바뀌면 본 문서를 즉시 업데이트한다.
## 에이전트 동작 원칙
- 추측하지 말고 근거 파일을 읽고 결정한다.
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
## 설정/보안 유의사항
- 토큰/키/개인정보를 코드/로그/문서에 하드코딩하지 않는다.
- 인증 관련 헤더/토큰 처리 로직(`AuthPlugin`, `UserDefaultsKey.token`) 수정 시 회귀 위험을 함께 점검한다.
- 외부 SDK 키 변경 시 빌드 설정과 런타임 초기화 지점을 함께 검토한다.

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37", "originHash" : "cf552e0db687218f4a2207a39678af43731c56f6f8ea12b111a15ac39574aa38",
"pins" : [ "pins" : [
{ {
"identity" : "abseil-cpp-binary", "identity" : "abseil-cpp-binary",
@@ -199,15 +199,6 @@
"revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811" "revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811"
} }
}, },
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "1b99d6e9872ef91fd57aaef657661b5a00069638",
"version" : "1.3.1"
}
},
{ {
"identity" : "promises", "identity" : "promises",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_live_creator_follow_no_alarm.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"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"
},
{
"filename" : "ic_live_creator_follow_plus.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,6 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "ic_message.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
@@ -9,7 +10,6 @@
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "ic_message.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic_shield.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: 474 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -16,6 +16,9 @@
} }
} }
} }
},
" · %@" : {
}, },
" (" : { " (" : {
"localizations" : { "localizations" : {
@@ -2503,6 +2506,9 @@
} }
} }
} }
},
"고정 해제" : {
}, },
"공개 설정" : { "공개 설정" : {
"localizations" : { "localizations" : {
@@ -2967,6 +2973,9 @@
} }
} }
} }
},
"누적" : {
}, },
"눌러서 잠금해제" : { "눌러서 잠금해제" : {
"localizations" : { "localizations" : {
@@ -4120,22 +4129,6 @@
} }
} }
}, },
"모든 기기에서 로그아웃" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log out from all devices"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "全端末からログアウト"
}
}
}
},
"모집완료" : { "모집완료" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -4167,6 +4160,25 @@
} }
} }
} }
},
"모든 기기에서 로그아웃" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log out from all devices"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "全端末からログアウト"
}
}
}
},
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
}, },
"모집중" : { "모집중" : {
"localizations" : { "localizations" : {
@@ -5207,6 +5219,9 @@
} }
} }
} }
},
"서비스 알림" : {
}, },
"설정" : { "설정" : {
"localizations" : { "localizations" : {
@@ -5815,6 +5830,9 @@
} }
} }
} }
},
"알림이 없습니다." : {
}, },
"앱 버전 정보" : { "앱 버전 정보" : {
"localizations" : { "localizations" : {
@@ -6888,54 +6906,6 @@
} }
} }
}, },
"인기 캐릭터" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Popular"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "人気キャラ"
}
}
}
},
"인기 캐릭터 채팅" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Top character"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "人気キャラチャット"
}
}
}
},
"일" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sun"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "日"
}
}
}
},
"이메일" : { "이메일" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -7096,6 +7066,38 @@
} }
} }
}, },
"인기 캐릭터" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Popular"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "人気キャラ"
}
}
}
},
"인기 캐릭터 채팅" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Top character"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "人気キャラチャット"
}
}
}
},
"인기 콘텐츠" : { "인기 콘텐츠" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -7160,6 +7162,22 @@
} }
} }
}, },
"일" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sun"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "日"
}
}
}
},
"일간 랭킹" : { "일간 랭킹" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -7831,6 +7849,9 @@
} }
} }
} }
},
"주간" : {
}, },
"중복확인" : { "중복확인" : {
"localizations" : { "localizations" : {
@@ -8391,6 +8412,9 @@
} }
} }
} }
},
"최상단에 고정" : {
}, },
"최신 콘텐츠" : { "최신 콘텐츠" : {
"localizations" : { "localizations" : {
@@ -8632,22 +8656,6 @@
} }
} }
}, },
"캔" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cans"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "CAN"
}
}
}
},
"캐릭터 정보" : { "캐릭터 정보" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -8664,6 +8672,22 @@
} }
} }
}, },
"캔" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cans"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "CAN"
}
}
}
},
"캔 충전" : { "캔 충전" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -9608,7 +9632,18 @@
} }
} }
}, },
"함께 보낼 %@메시지 입력(최대 %lld자)" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "함께 보낼 %1$@메시지 입력(최대 %2$lld자)"
}
}
}
},
"함께 보낼 %@메시지 입력(최대 1000자)" : { "함께 보낼 %@메시지 입력(최대 1000자)" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View File

@@ -0,0 +1,234 @@
import Foundation
enum AppDeepLinkAction {
case live(roomId: Int)
case content(contentId: Int)
case series(seriesId: Int)
case community(creatorId: Int, postId: Int?)
case message
case audition
}
enum AppDeepLinkSource {
case external
case notificationList
}
enum AppDeepLinkHandler {
static func handle(url: URL, source: AppDeepLinkSource = .external) -> Bool {
guard isSupportedScheme(url) else {
return false
}
guard let action = parseAction(url: url) else {
return false
}
DispatchQueue.main.async {
if case .splash = AppState.shared.rootStep {
AppState.shared.pendingDeepLinkAction = action
return
}
apply(action: action, source: source)
}
return true
}
static func handle(urlString: String, source: AppDeepLinkSource = .external) -> Bool {
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: trimmed) else {
return false
}
return handle(url: url, source: source)
}
static func apply(action: AppDeepLinkAction) {
apply(action: action, source: .external)
}
private static func apply(action: AppDeepLinkAction, source: AppDeepLinkSource) {
switch action {
case .live(let roomId):
guard roomId > 0 else { return }
AppState.shared.isPushRoomFromDeepLink = source == .external
AppState.shared.pushRoomId = 0
AppState.shared.pushRoomId = roomId
case .content(let contentId):
guard contentId > 0 else { return }
AppState.shared.setAppStep(step: .contentDetail(contentId: contentId))
case .series(let seriesId):
guard seriesId > 0 else { return }
AppState.shared.setAppStep(step: .seriesDetail(seriesId: seriesId))
case .community(let creatorId, let postId):
guard creatorId > 0 else { return }
if let postId = postId, postId > 0 {
AppState.shared.setPendingCommunityCommentDeepLink(creatorId: creatorId, postId: postId)
} else {
AppState.shared.clearPendingCommunityCommentDeepLink()
}
AppState.shared.setAppStep(step: .creatorCommunityAll(creatorId: creatorId))
case .message:
AppState.shared.setAppStep(step: .message)
case .audition:
AppState.shared.setAppStep(step: .audition)
}
}
private static func isSupportedScheme(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased() else {
return false
}
let appScheme = APPSCHEME.lowercased()
return scheme == appScheme || scheme == "voiceon" || scheme == "voiceon-test"
}
private static func parseAction(url: URL) -> AppDeepLinkAction? {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let routeAction = parsePathStyle(url: url, components: components) {
return routeAction
}
return parseQueryStyle(components: components)
}
private static func parsePathStyle(url: URL, components: URLComponents?) -> AppDeepLinkAction? {
let pathComponents = url.pathComponents.filter { $0 != "/" && !$0.isEmpty }
let host = components?.host?.lowercased() ?? ""
if !host.isEmpty {
let identifier = pathComponents.first
return makeAction(route: host, identifier: identifier, components: components)
}
guard !pathComponents.isEmpty else {
return nil
}
let route = pathComponents[0].lowercased()
let identifier = pathComponents.count > 1 ? pathComponents[1] : nil
return makeAction(route: route, identifier: identifier, components: components)
}
private static func parseQueryStyle(components: URLComponents?) -> AppDeepLinkAction? {
guard let queryItems = components?.queryItems else {
return nil
}
var queryMap: [String: String] = [:]
for item in queryItems {
queryMap[item.name.lowercased()] = item.value
}
if let roomId = queryMap["room_id"], let value = Int(roomId), value > 0 {
return .live(roomId: value)
}
if let contentId = queryMap["content_id"], let value = Int(contentId), value > 0 {
return .content(contentId: value)
}
if let seriesId = queryMap["series_id"], let value = Int(seriesId), value > 0 {
return .series(seriesId: value)
}
if let communityId = queryMap["community_id"], let value = Int(communityId), value > 0 {
return .community(creatorId: value, postId: communityPostId(queryMap: queryMap))
}
if queryMap["message_id"] != nil {
return .message
}
if queryMap["audition_id"] != nil {
return .audition
}
return nil
}
private static func makeAction(route: String, identifier: String?, components: URLComponents?) -> AppDeepLinkAction? {
switch route {
case "live":
guard let identifier = identifier, let roomId = Int(identifier), roomId > 0 else {
return nil
}
return .live(roomId: roomId)
case "content":
guard let identifier = identifier, let contentId = Int(identifier), contentId > 0 else {
return nil
}
return .content(contentId: contentId)
case "series":
guard let identifier = identifier, let seriesId = Int(identifier), seriesId > 0 else {
return nil
}
return .series(seriesId: seriesId)
case "community":
let postId = communityPostId(queryItems: components?.queryItems)
if let identifier = identifier, let creatorId = Int(identifier), creatorId > 0 {
return .community(creatorId: creatorId, postId: postId)
}
guard let creatorId = fallbackCommunityCreatorId() else {
return nil
}
return .community(creatorId: creatorId, postId: postId)
case "message":
return .message
case "audition":
return .audition
default:
return nil
}
}
private static func communityPostId(queryItems: [URLQueryItem]?) -> Int? {
guard let queryItems = queryItems else {
return nil
}
var queryMap: [String: String] = [:]
for item in queryItems {
queryMap[item.name.lowercased()] = item.value
}
return communityPostId(queryMap: queryMap)
}
private static func communityPostId(queryMap: [String: String]) -> Int? {
if let postId = queryMap["postid"], let value = Int(postId), value > 0 {
return value
}
if let postId = queryMap["post_id"], let value = Int(postId), value > 0 {
return value
}
return nil
}
private static func fallbackCommunityCreatorId() -> Int? {
let userId = UserDefaults.int(forKey: .userId)
return userId > 0 ? userId : nil
}
}

View File

@@ -266,6 +266,15 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
Messaging.messaging().appDidReceiveMessage(userInfo) Messaging.messaging().appDidReceiveMessage(userInfo)
Notifly.userNotificationCenter(center, didReceive: response) Notifly.userNotificationCenter(center, didReceive: response)
let deepLinkString = (userInfo["deep_link"] as? String ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if !deepLinkString.isEmpty {
_ = AppDeepLinkHandler.handle(urlString: deepLinkString)
completionHandler()
return
}
let roomIdString = userInfo["room_id"] as? String let roomIdString = userInfo["room_id"] as? String
let contentIdString = userInfo["content_id"] as? String let contentIdString = userInfo["content_id"] as? String
let channelIdString = userInfo["channel_id"] as? String let channelIdString = userInfo["channel_id"] as? String

View File

@@ -7,13 +7,31 @@
import Foundation import Foundation
struct AppRoute: Hashable {
let id = UUID()
}
struct LiveDetailSheetState {
let roomId: Int
let onClickParticipant: () -> Void
let onClickReservation: () -> Void
let onClickStart: () -> Void
let onClickCancel: () -> Void
}
class AppState: ObservableObject { class AppState: ObservableObject {
static let shared = AppState() static let shared = AppState()
private var appStepBackStack = [AppStep]() private var routeStepMap: [AppRoute: AppStep] = [:]
@Published var alreadyUpdatedMarketingInfo = false @Published var alreadyUpdatedMarketingInfo = false
@Published private(set) var appStep: AppStep = .splash @Published private(set) var appStep: AppStep = .splash
@Published private(set) var rootStep: AppStep = .splash
@Published var navigationPath: [AppRoute] = [] {
didSet {
syncStepWithNavigationPath()
}
}
@Published var isShowPlayer = false { @Published var isShowPlayer = false {
didSet { didSet {
@@ -31,6 +49,10 @@ class AppState: ObservableObject {
@Published var pushMessageId = 0 @Published var pushMessageId = 0
@Published var pushAudioContentId = 0 @Published var pushAudioContentId = 0
@Published var pushSeriesId = 0 @Published var pushSeriesId = 0
@Published var pendingDeepLinkAction: AppDeepLinkAction? = nil
@Published var pendingCommunityCommentCreatorId = 0
@Published var pendingCommunityCommentPostId = 0
@Published var isPushRoomFromDeepLink = false
@Published var roomId = 0 { @Published var roomId = 0 {
didSet { didSet {
if roomId <= 0 { if roomId <= 0 {
@@ -52,29 +74,113 @@ class AppState: ObservableObject {
@Published var isShowErrorPopup = false @Published var isShowErrorPopup = false
@Published var errorMessage = "" @Published var errorMessage = ""
@Published var liveDetailSheet: LiveDetailSheetState? = nil
private func syncStepWithNavigationPath() {
let validRoutes = Set(navigationPath)
routeStepMap = routeStepMap.filter { validRoutes.contains($0.key) }
if let route = navigationPath.last,
let step = routeStepMap[route] {
appStep = step
} else {
appStep = rootStep
}
}
func appStep(for route: AppRoute) -> AppStep? {
routeStepMap[route]
}
func setAppStep(step: AppStep) { func setAppStep(step: AppStep) {
switch step {
case .splash, .main:
appStepBackStack.removeAll()
default:
appStepBackStack.append(appStep)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.appStep = step switch step {
case .splash, .main:
self.liveDetailSheet = nil
self.rootStep = step
self.routeStepMap.removeAll()
self.navigationPath.removeAll()
self.appStep = step
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
self.liveDetailSheet = LiveDetailSheetState(
roomId: roomId,
onClickParticipant: onClickParticipant,
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel
)
self.appStep = step
default:
let route = AppRoute()
self.routeStepMap[route] = step
self.navigationPath.append(route)
self.appStep = step
}
} }
} }
func back() { func back() {
if let step = appStepBackStack.popLast() { DispatchQueue.main.async {
self.appStep = step if self.liveDetailSheet != nil {
} else { self.liveDetailSheet = nil
self.appStep = .main self.syncStepWithNavigationPath()
return
}
if self.navigationPath.isEmpty {
self.rootStep = .main
self.appStep = .main
return
}
_ = self.navigationPath.popLast()
} }
} }
func hideLiveDetailSheet() {
DispatchQueue.main.async {
self.liveDetailSheet = nil
self.syncStepWithNavigationPath()
}
}
func consumePendingDeepLinkAction() -> AppDeepLinkAction? {
let action = pendingDeepLinkAction
pendingDeepLinkAction = nil
return action
}
func setPendingCommunityCommentDeepLink(creatorId: Int, postId: Int) {
guard creatorId > 0, postId > 0 else {
return
}
pendingCommunityCommentCreatorId = creatorId
pendingCommunityCommentPostId = postId
}
func consumePendingCommunityCommentPostId(creatorId: Int) -> Int? {
guard creatorId > 0 else {
return nil
}
guard pendingCommunityCommentCreatorId == creatorId,
pendingCommunityCommentPostId > 0 else {
return nil
}
let postId = pendingCommunityCommentPostId
clearPendingCommunityCommentDeepLink()
return postId
}
func clearPendingCommunityCommentDeepLink() {
pendingCommunityCommentCreatorId = 0
pendingCommunityCommentPostId = 0
}
// ( -> ) UI // ( -> ) UI
func softRestart() { func softRestart() {
isRestartApp = true isRestartApp = true

View File

@@ -42,6 +42,8 @@ enum AppStep {
case notificationSettings case notificationSettings
case notificationReceiveSettings
case contentViewSettings case contentViewSettings
case signOut case signOut
@@ -76,6 +78,8 @@ enum AppStep {
case userProfileDonationAll(userId: Int) case userProfileDonationAll(userId: Int)
case channelDonationAll(creatorId: Int)
case userProfileFanTalkAll(userId: Int) case userProfileFanTalkAll(userId: Int)
case createLive( case createLive(
@@ -160,6 +164,8 @@ enum AppStep {
case message case message
case notificationList
case pointStatus(refresh: () -> Void) case pointStatus(refresh: () -> Void)
case audition case audition

View File

@@ -84,6 +84,10 @@ struct SodaLiveApp: App {
comps.path.lowercased() == "/result" { comps.path.lowercased() == "/result" {
canPgPaymentViewModel.handleVerifyOpenURL(url) canPgPaymentViewModel.handleVerifyOpenURL(url)
} else { } else {
if AppDeepLinkHandler.handle(url: url) {
return
}
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:]) _ = 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)

View File

@@ -170,29 +170,7 @@ struct AuditionApplicantRecordingView: View {
} }
} }
} }
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(soundManager.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
.onDisappear {
if soundManager.onClose {
isShowing = false
}
}
}
}
} }
} }

View File

@@ -130,23 +130,7 @@ struct AuditionApplyView: View {
} }
.ignoresSafeArea() .ignoresSafeArea()
} }
.popup(isPresented: $isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $isShowPopup, message: errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear { .onAppear {
withAnimation { withAnimation {
isShow = true isShow = true

View File

@@ -68,21 +68,7 @@ struct AuditionDetailView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.getAuditionDetail(auditionId: auditionId) { viewModel.getAuditionDetail(auditionId: auditionId) {
AppState.shared.back() AppState.shared.back()

View File

@@ -150,36 +150,8 @@ struct AuditionRoleDetailView: View {
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack { .sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(soundManager.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.onFailure = { AppState.shared.back() } viewModel.onFailure = { AppState.shared.back() }
viewModel.auditionRoleId = roleId viewModel.auditionRoleId = roleId

View File

@@ -120,23 +120,7 @@ struct CharacterView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
} }

View File

@@ -132,25 +132,7 @@ struct CharacterDetailView: View {
} }
.navigationTitle("") .navigationTitle("")
.navigationBarBackButtonHidden() .navigationBarBackButtonHidden()
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(alignment: .center)
.frame(maxWidth: .infinity)
.padding(.horizontal, 33.3)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
.onAppear { .onAppear {
viewModel.characterId = characterId viewModel.characterId = characterId

View File

@@ -45,25 +45,7 @@ struct CharacterDetailGalleryView: View {
Spacer() Spacer()
} }
.padding(.top, 24) .padding(.top, 24)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(alignment: .center)
.frame(maxWidth: .infinity)
.padding(.horizontal, 33.3)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
.onAppear { .onAppear {
viewModel.characterId = characterId viewModel.characterId = characterId

View File

@@ -14,104 +14,83 @@ struct NewCharacterListView: View {
private let gridSpacing: CGFloat = 12 private let gridSpacing: CGFloat = 12
var body: some View { var body: some View {
NavigationStack { Group { BaseView(isLoading: $viewModel.isLoading) {
BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 8) {
VStack(spacing: 8) { // Toolbar
// Toolbar DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
// n // n
HStack(spacing: 0) { HStack(spacing: 0) {
Text("전체") Text("전체")
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2")) .foregroundColor(Color(hex: "e2e2e2"))
Text(" \(viewModel.totalCount)") Text(" \(viewModel.totalCount)")
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "ff5c49")) .foregroundColor(Color(hex: "ff5c49"))
Text("") Text("")
.appFont(size: 12, weight: .regular) .appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2")) .foregroundColor(Color(hex: "e2e2e2"))
Spacer() Spacer()
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
// Grid 3 // Grid 3
GeometryReader { geo in GeometryReader { geo in
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2 let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
LazyVGrid( LazyVGrid(
columns: Array( columns: Array(
repeating: GridItem( repeating: GridItem(
.flexible(), .flexible(),
spacing: gridSpacing, spacing: gridSpacing,
alignment: .topLeading alignment: .topLeading
),
count: 2
), ),
alignment: .leading, count: 2
spacing: gridSpacing ),
) { alignment: .leading,
ForEach(viewModel.items.indices, id: \.self) { idx in spacing: gridSpacing
let item = viewModel.items[idx] ) {
ForEach(viewModel.items.indices, id: \.self) { idx in
let item = viewModel.items[idx]
NavigationLink(value: item.characterId) { CharacterItemView(
CharacterItemView( character: item,
character: item, size: width,
size: width, rank: 0,
rank: 0, isShowRank: false
isShowRank: false )
) .onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) } .onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
}
}
} }
.padding(.horizontal, horizontalPadding) }
.padding(.horizontal, horizontalPadding)
if viewModel.isLoadingMore { if viewModel.isLoadingMore {
HStack { HStack {
Spacer() Spacer()
ProgressView() ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 16) .padding(.vertical, 16)
Spacer() Spacer()
}
} }
} }
} }
.frame(minHeight: 0, maxHeight: .infinity)
}
.padding(.vertical, 12)
.onAppear {
// 1
if viewModel.items.isEmpty {
viewModel.fetch()
}
} }
.frame(minHeight: 0, maxHeight: .infinity)
} }
.background(Color.black) .padding(.vertical, 12)
} .onAppear {
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { // 1
GeometryReader { geo in if viewModel.items.isEmpty {
HStack { viewModel.fetch()
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
} }
} }
} }
.navigationDestination(for: Int.self) { characterId in .background(Color.black)
CharacterDetailView(characterId: characterId) }
} .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
} }
} }
} }

View File

@@ -8,8 +8,6 @@
import SwiftUI import SwiftUI
import Bootpay import Bootpay
import BootpayUI import BootpayUI
import PopupView
struct ChatTabView: View { 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)
@@ -75,7 +73,7 @@ struct ChatTabView: View {
ZStack { ZStack {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
// //
HStack(spacing: 24) { HStack(spacing: 20) {
Image("img_text_logo") Image("img_text_logo")
Spacer() Spacer()

View File

@@ -15,91 +15,87 @@ struct OriginalWorkDetailView: View {
let originalId: Int let originalId: Int
var body: some View { var body: some View {
NavigationStack { Group { BaseView(isLoading: $viewModel.isLoading) {
BaseView(isLoading: $viewModel.isLoading) { ZStack(alignment: .top) {
ZStack(alignment: .top) { if let imageUrl = viewModel.response?.imageUrl {
if let imageUrl = viewModel.response?.imageUrl { KFImage(URL(string: imageUrl))
KFImage(URL(string: imageUrl)) .cancelOnDisappear(true)
.cancelOnDisappear(true) .resizable()
.scaledToFill()
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
.clipped()
.blur(radius: 25)
}
Color.black.opacity(0.5).ignoresSafeArea()
VStack(spacing: 0) {
HStack(spacing: 0) {
Image("ic_back")
.resizable() .resizable()
.scaledToFill() .frame(width: 24, height: 24)
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56) .onTapGesture {
.clipped() AppState.shared.back()
.blur(radius: 25) }
Spacer()
} }
.padding(.horizontal, 24)
.frame(height: 56)
Color.black.opacity(0.5).ignoresSafeArea() if let response = viewModel.response {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
OriginalWorkDetailHeaderView(item: response)
.padding(.horizontal, 24)
.padding(.bottom, 24)
VStack(spacing: 0) { HStack(spacing: 0) {
HStack(spacing: 0) { SeriesDetailTabView(
Image("ic_back") title: I18n.Tab.character,
.resizable() width: screenSize().width / 2,
.frame(width: 24, height: 24) isSelected: viewModel.currentTab == .character
.onTapGesture { ) {
AppState.shared.back() if viewModel.currentTab != .character {
viewModel.currentTab = .character
}
}
SeriesDetailTabView(
title: I18n.Tab.workInfo,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info
) {
if viewModel.currentTab != .info {
viewModel.currentTab = .info
}
}
} }
.background(Color.black)
Spacer() Rectangle()
} .foregroundColor(Color.gray90.opacity(0.5))
.padding(.horizontal, 24) .frame(height: 1)
.frame(height: 56) .frame(maxWidth: .infinity)
if let response = viewModel.response { switch(viewModel.currentTab) {
ScrollView(.vertical, showsIndicators: false) { case .info:
VStack(spacing: 0) { OriginalWorkInfoView(response: response)
OriginalWorkDetailHeaderView(item: response) default:
.padding(.horizontal, 24) OriginalWorkCharacterView(characters: viewModel.characters)
.padding(.bottom, 24)
HStack(spacing: 0) {
SeriesDetailTabView(
title: I18n.Tab.character,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .character
) {
if viewModel.currentTab != .character {
viewModel.currentTab = .character
}
}
SeriesDetailTabView(
title: I18n.Tab.workInfo,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info
) {
if viewModel.currentTab != .info {
viewModel.currentTab = .info
}
}
}
.background(Color.black)
Rectangle()
.foregroundColor(Color.gray90.opacity(0.5))
.frame(height: 1)
.frame(maxWidth: .infinity)
switch(viewModel.currentTab) {
case .info:
OriginalWorkInfoView(response: response)
default:
OriginalWorkCharacterView(characters: viewModel.characters)
}
} }
} }
} }
} }
} }
} }
.onAppear { }
if viewModel.response == nil { .onAppear {
viewModel.originalId = originalId if viewModel.response == nil {
} viewModel.originalId = originalId
}
.navigationDestination(for: Int.self) { characterId in
CharacterDetailView(characterId: characterId)
} }
} }
}
} }
} }
@@ -129,14 +125,13 @@ struct OriginalWorkCharacterView: View {
ForEach(characters.indices, id: \.self) { idx in ForEach(characters.indices, id: \.self) { idx in
let item = characters[idx] let item = characters[idx]
NavigationLink(value: item.characterId) { CharacterItemView(
CharacterItemView( character: item,
character: item, size: width,
size: width, rank: 0,
rank: 0, isShowRank: false
isShowRank: false )
) .onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
}
} }
} }
.padding(.horizontal, horizontalPadding) .padding(.horizontal, horizontalPadding)

View File

@@ -69,23 +69,7 @@ struct OriginalTabView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
} }

View File

@@ -308,23 +308,7 @@ struct ChatRoomView: View {
.onDisappear { .onDisappear {
viewModel.stopTimer() viewModel.stopTimer()
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
} }

View File

@@ -44,25 +44,7 @@ struct ChatBgSelectionView: View {
Spacer() Spacer()
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(alignment: .center)
.frame(maxWidth: .infinity)
.padding(.horizontal, 33.3)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
.onAppear { .onAppear {
viewModel.characterId = characterId viewModel.characterId = characterId

View File

@@ -36,3 +36,99 @@ struct BaseView_Previews: PreviewProvider {
BaseView(isLoading: .constant(false)) {} BaseView(isLoading: .constant(false)) {}
} }
} }
private struct SodaToastModifier: ViewModifier {
@Binding var isPresented: Bool
let message: String
let autohideIn: Double
private let toastBackgroundColor = Color(
red: 59.0 / 255.0,
green: 185.0 / 255.0,
blue: 241.0 / 255.0,
opacity: 0.92
)
@State private var dismissWorkItem: DispatchWorkItem?
func body(content: Content) -> some View {
content
.overlay(alignment: .top) {
GeometryReader { geo in
if isPresented, !message.isEmpty {
Text(message)
.appFont(size: 12, weight: .medium)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.horizontal, 18)
.padding(.vertical, 12)
.background(
Capsule()
.fill(toastBackgroundColor)
)
.overlay(
Capsule()
.stroke(Color.white.opacity(0.15), lineWidth: 1)
)
.padding(.top, geo.safeAreaInsets.top + 8)
.padding(.horizontal, 24)
.frame(maxWidth: .infinity, alignment: .top)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.allowsHitTesting(false)
}
.animation(.easeInOut(duration: 0.2), value: isPresented)
.onAppear {
if isPresented {
scheduleDismiss()
}
}
.onChange(of: isPresented) { newValue in
if newValue {
scheduleDismiss()
} else {
cancelDismiss()
}
}
.onDisappear {
cancelDismiss()
}
}
private func scheduleDismiss() {
cancelDismiss()
guard autohideIn > 0 else {
return
}
let workItem = DispatchWorkItem {
withAnimation {
isPresented = false
}
}
dismissWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: workItem)
}
private func cancelDismiss() {
dismissWorkItem?.cancel()
dismissWorkItem = nil
}
}
extension View {
func sodaToast(isPresented: Binding<Bool>, message: String, autohideIn: Double = 2) -> some View {
modifier(
SodaToastModifier(
isPresented: isPresented,
message: message,
autohideIn: autohideIn
)
)
}
}

View File

@@ -21,6 +21,7 @@ enum DateParser {
{ ISO8601.fractional.date(from: $0) }, { ISO8601.fractional.date(from: $0) },
{ ISO8601.basic.date(from: $0) }, { ISO8601.basic.date(from: $0) },
{ DF.rfc3339.date(from: $0) }, { DF.rfc3339.date(from: $0) },
{ DF.isoLocalDateTime.date(from: $0) },
{ DF.basic.date(from: $0) } { DF.basic.date(from: $0) }
] ]
@@ -56,5 +57,13 @@ enum DateParser {
f.dateFormat = "yyyy-MM-dd HH:mm:ss" f.dateFormat = "yyyy-MM-dd HH:mm:ss"
return f return f
}() }()
static let isoLocalDateTime: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.timeZone = TimeZone(secondsFromGMT: 0)
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return f
}()
} }
} }

View File

@@ -9,11 +9,12 @@ import SwiftUI
struct ContentAllByThemeView: View { struct ContentAllByThemeView: View {
@StateObject var viewModel = ContentAllByThemeViewModel() @StateObject var viewModel = ContentAllByThemeViewModel()
@State private var isInitialized = false
let themeId: Int let themeId: Int
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
DetailNavigationBar(title: viewModel.theme) DetailNavigationBar(title: viewModel.theme)
@@ -111,8 +112,11 @@ struct ContentAllByThemeView: View {
} }
} }
.onAppear { .onAppear {
viewModel.themeId = themeId if !isInitialized || viewModel.themeId != themeId {
viewModel.getContentList() viewModel.themeId = themeId
viewModel.getContentList()
isInitialized = true
}
} }
} }
} }

View File

@@ -10,12 +10,13 @@ import SwiftUI
struct ContentAllView: View { struct ContentAllView: View {
@StateObject var viewModel = ContentAllViewModel() @StateObject var viewModel = ContentAllViewModel()
@State private var isInitialized = false
var isFree: Bool = false var isFree: Bool = false
var isPointAvailableOnly: Bool = false var isPointAvailableOnly: Bool = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체")) DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
@@ -78,63 +79,63 @@ struct ContentAllView: View {
ForEach(viewModel.contentList.indices, id: \.self) { idx in ForEach(viewModel.contentList.indices, id: \.self) { idx in
let item = viewModel.contentList[idx] let item = viewModel.contentList[idx]
NavigationLink { VStack(alignment: .leading, spacing: 0) {
ContentDetailView(contentId: item.contentId) ZStack(alignment: .top) {
} label: { DownsampledKFImage(
VStack(alignment: .leading, spacing: 0) { url: URL(string: item.coverImageUrl),
ZStack(alignment: .top) { size: CGSize(width: itemSize, height: itemSize)
DownsampledKFImage( )
url: URL(string: item.coverImageUrl), .cornerRadius(16)
size: CGSize(width: itemSize, height: itemSize)
)
.cornerRadius(16)
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
Spacer() Spacer()
if item.isPointAvailable { if item.isPointAvailable {
Image("ic_point") Image("ic_point")
.padding(.top, 6) .padding(.top, 6)
.padding(.trailing, 6) .padding(.trailing, 6)
}
} }
} }
Text(item.title)
.appFont(size: 18, weight: .regular)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 8)
Text(item.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C"))
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 4)
} }
.frame(width: itemSize)
.contentShape(Rectangle()) Text(item.title)
.onAppear { .appFont(size: 18, weight: .regular)
if idx == viewModel.contentList.count - 1 { .foregroundColor(.white)
viewModel.fetchData() .multilineTextAlignment(.leading)
} .fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 8)
Text(item.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C"))
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 4)
}
.frame(width: itemSize)
.contentShape(Rectangle())
.onAppear {
if idx == viewModel.contentList.count - 1 {
viewModel.fetchData()
} }
} }
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
} }
} }
.padding(horizontalPadding) .padding(horizontalPadding)
} }
} }
.onAppear { .onAppear {
viewModel.isFree = isFree if !isInitialized || viewModel.isFree != isFree || viewModel.isPointAvailableOnly != isPointAvailableOnly {
viewModel.isPointAvailableOnly = isPointAvailableOnly viewModel.isFree = isFree
viewModel.getThemeList() viewModel.isPointAvailableOnly = isPointAvailableOnly
viewModel.fetchData() viewModel.getThemeList()
viewModel.fetchData()
isInitialized = true
}
} }
} }
} }

View File

@@ -14,95 +14,92 @@ struct ContentNewAllItemView: View {
let item: GetAudioContentMainItem let item: GetAudioContentMainItem
var body: some View { var body: some View {
NavigationLink { VStack(alignment: .leading, spacing: 8) {
ContentDetailView(contentId: item.contentId) ZStack(alignment: .bottom) {
} label: { KFImage(URL(string: item.coverImageUrl))
VStack(alignment: .leading, spacing: 8) { .cancelOnDisappear(true)
ZStack(alignment: .bottom) { .downsampling(
KFImage(URL(string: item.coverImageUrl)) size: CGSize(
.cancelOnDisappear(true) width: width,
.downsampling( height: width
size: CGSize(
width: width,
height: width
)
) )
.resizable() )
.scaledToFill() .resizable()
.frame(width: width, height: width, alignment: .top) .scaledToFill()
.cornerRadius(2.7) .frame(width: width, height: width, alignment: .top)
.cornerRadius(2.7)
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
HStack(spacing: 0) { HStack(spacing: 0) {
HStack(spacing: 2) { HStack(spacing: 2) {
if item.price > 0 { if item.price > 0 {
Image("ic_card_can_gray") Image("ic_card_can_gray")
Text("\(item.price)") Text("\(item.price)")
.appFont(size: 8.5, weight: .medium) .appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
} else { } else {
Text("무료") Text("무료")
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
}
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.leading, 2.7)
.padding(.bottom, 2.7)
Spacer()
HStack(spacing: 2) {
Text(item.duration)
.appFont(size: 8.5, weight: .medium) .appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.trailing, 2.7)
.padding(.bottom, 2.7)
} }
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.leading, 2.7)
.padding(.bottom, 2.7)
Spacer()
HStack(spacing: 2) {
Text(item.duration)
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.trailing, 2.7)
.padding(.bottom, 2.7)
} }
} }
.frame(width: width, height: width)
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.frame(width: width, alignment: .leading)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 21.3,
height: 21.3
)
)
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3)
.clipShape(Circle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
Text(item.creatorNickname)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.lineLimit(1)
}
.padding(.bottom, 10)
} }
.frame(width: width) .frame(width: width, height: width)
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.frame(width: width, alignment: .leading)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 21.3,
height: 21.3
)
)
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3)
.clipShape(Circle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
Text(item.creatorNickname)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.lineLimit(1)
}
.padding(.bottom, 10)
} }
.frame(width: width)
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
} }
} }

View File

@@ -10,11 +10,12 @@ import SwiftUI
struct ContentNewAllView: View { struct ContentNewAllView: View {
@StateObject var viewModel = ContentNewAllViewModel() @StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
let isFree: Bool let isFree: Bool
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠") DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
@@ -82,9 +83,12 @@ struct ContentNewAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.isFree = isFree if !isInitialized || viewModel.isFree != isFree {
viewModel.getThemeList() viewModel.isFree = isFree
viewModel.getNewContentList() viewModel.getThemeList()
viewModel.getNewContentList()
isInitialized = true
}
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@@ -11,9 +11,10 @@ import Kingfisher
struct ContentRankingAllView: View { struct ContentRankingAllView: View {
@StateObject var viewModel = ContentRankingAllViewModel() @StateObject var viewModel = ContentRankingAllViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "인기 콘텐츠") DetailNavigationBar(title: "인기 콘텐츠")
@@ -44,97 +45,94 @@ struct ContentRankingAllView: View {
LazyVStack(spacing: 20) { LazyVStack(spacing: 20) {
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
let item = viewModel.contentRankingItemList[index] let item = viewModel.contentRankingItemList[index]
NavigationLink { HStack(spacing: 0) {
ContentDetailView(contentId: item.contentId) KFImage(URL(string: item.coverImageUrl))
} label: { .cancelOnDisappear(true)
HStack(spacing: 0) { .downsampling(
KFImage(URL(string: item.coverImageUrl)) size: CGSize(
.cancelOnDisappear(true) width: 66.7,
.downsampling( height: 66.7
size: CGSize(
width: 66.7,
height: 66.7
)
) )
.resizable() )
.scaledToFill() .resizable()
.frame(width: 66.7, height: 66.7, alignment: .top) .scaledToFill()
.clipped() .frame(width: 66.7, height: 66.7, alignment: .top)
.cornerRadius(5.3) .clipped()
.cornerRadius(5.3)
Text("\(index + 1)") Text("\(index + 1)")
.appFont(size: 16.7, weight: .bold) .appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color(hex: "3bb9f1"))
.padding(.horizontal, 12) .padding(.horizontal, 12)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 8) { HStack(spacing: 8) {
Text(item.themeStr) Text(item.themeStr)
.appFont(size: 8, weight: .medium) .appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "3bac6a")) .foregroundColor(Color(hex: "3bac6a"))
.padding(2.6) .padding(2.6)
.background(Color(hex: "28312b")) .background(Color(hex: "28312b"))
.cornerRadius(2.6)
Text(item.duration)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
if item.isPointAvailable {
Text("포인트")
.appFont(size: 8, weight: .medium)
.foregroundColor(.white)
.padding(2.6)
.background(Color(hex: "7849bc"))
.cornerRadius(2.6)
}
}
Text(item.creatorNickname)
.appFont(size: 10.7, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 8)
Text(item.title)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.lineLimit(2)
.padding(.top, 2.7)
}
Spacer()
if item.price > 0 {
HStack(spacing: 8) {
Image("ic_can")
.resizable()
.frame(width: 17, height: 17)
Text("\(item.price)")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090"))
}
} else {
Text("무료")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "cf5c37"))
.cornerRadius(2.6) .cornerRadius(2.6)
Text(item.duration)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
if item.isPointAvailable {
Text("포인트")
.appFont(size: 8, weight: .medium)
.foregroundColor(.white)
.padding(2.6)
.background(Color(hex: "7849bc"))
.cornerRadius(2.6)
}
} }
Text(item.creatorNickname)
.appFont(size: 10.7, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 8)
Text(item.title)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.lineLimit(2)
.padding(.top, 2.7)
} }
.frame(height: 66.7)
.contentShape(Rectangle()) Spacer()
.onAppear {
if index == viewModel.contentRankingItemList.count - 1 { if item.price > 0 {
viewModel.getContentRanking() HStack(spacing: 8) {
Image("ic_can")
.resizable()
.frame(width: 17, height: 17)
Text("\(item.price)")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090"))
} }
} else {
Text("무료")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "cf5c37"))
.cornerRadius(2.6)
} }
} }
.frame(height: 66.7)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.contentRankingItemList.count - 1 {
viewModel.getContentRanking()
}
}
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
} }
} }
} }
@@ -145,28 +143,13 @@ struct ContentRankingAllView: View {
LoadingView() LoadingView()
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear { .onAppear {
viewModel.getContentRankingSortType() if !isInitialized {
viewModel.getContentRanking() viewModel.getContentRankingSortType()
viewModel.getContentRanking()
isInitialized = true
}
} }
} }
} }

View File

@@ -21,7 +21,7 @@ struct ContentBoxView: View {
var body: some View { var body: some View {
ZStack { ZStack {
NavigationView { Group {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
DetailNavigationBar(title: I18n.ContentBox.title) DetailNavigationBar(title: I18n.ContentBox.title)

View File

@@ -11,9 +11,10 @@ struct ContentListView: View {
let userId: Int let userId: Int
@StateObject var viewModel = ContentListViewModel() @StateObject var viewModel = ContentListViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
@@ -128,17 +129,14 @@ struct ContentListView: View {
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
let audioContent = viewModel.audioContentList[index] let audioContent = viewModel.audioContentList[index]
NavigationLink { ContentListItemView(item: audioContent)
ContentDetailView(contentId: audioContent.contentId) .contentShape(Rectangle())
} label: { .onAppear {
ContentListItemView(item: audioContent) if index == viewModel.audioContentList.count - 1 {
.contentShape(Rectangle()) viewModel.getAudioContentList()
.onAppear {
if index == viewModel.audioContentList.count - 1 {
viewModel.getAudioContentList()
}
} }
} }
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: audioContent.contentId)) }
} }
} }
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@@ -147,25 +145,14 @@ struct ContentListView: View {
.padding(.top, 13.3) .padding(.top, 13.3)
} }
.onAppear { .onAppear {
viewModel.userId = userId if !isInitialized || viewModel.userId != userId {
viewModel.getCategoryList() viewModel.userId = userId
viewModel.getAudioContentList() viewModel.getCategoryList()
} viewModel.getAudioContentList()
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { isInitialized = true
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color(hex: "3bb9f1"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
} }
} }
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
} }
} }
} }

View File

@@ -14,6 +14,10 @@ struct ContentCreateView: View {
@StateObject private var viewModel = ContentCreateViewModel() @StateObject private var viewModel = ContentCreateViewModel()
@State private var isShowPhotoPicker = false @State private var isShowPhotoPicker = false
@State private var selectedPickedImage: UIImage?
@State private var cropSourceImage: UIImage?
@State private var isShowImageCropper = false
@State private var isImageLoading = false
@State private var isShowSelectAudioView = false @State private var isShowSelectAudioView = false
@State private var isShowSelectThemeView = false @State private var isShowSelectThemeView = false
@State private var isShowSelectDateView = false @State private var isShowSelectDateView = false
@@ -630,7 +634,7 @@ struct ContentCreateView: View {
if isShowPhotoPicker { if isShowPhotoPicker {
ImagePicker( ImagePicker(
isShowing: $isShowPhotoPicker, isShowing: $isShowPhotoPicker,
selectedImage: $viewModel.coverImage, selectedImage: $selectedPickedImage,
sourceType: .photoLibrary sourceType: .photoLibrary
) )
} }
@@ -659,25 +663,56 @@ struct ContentCreateView: View {
} }
} }
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
if isImageLoading {
ZStack {
Color.black.opacity(0.45)
.edgesIgnoringSafeArea(.all)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
}
} }
.onTapGesture { hideKeyboard() } .onTapGesture { hideKeyboard() }
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in .onChange(of: selectedPickedImage, perform: { newImage in
HStack { guard let newImage else {
Spacer() return
Text(viewModel.errorMessage) }
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) isImageLoading = true
.appFont(size: 12, weight: .medium)
.background(Color.button) DispatchQueue.global(qos: .userInitiated).async {
.foregroundColor(Color.white) let normalizedImage = newImage.normalizedForCrop()
.multilineTextAlignment(.center) DispatchQueue.main.async {
.cornerRadius(20) isImageLoading = false
.padding(.top, 66.7) selectedPickedImage = nil
Spacer() cropSourceImage = normalizedImage
isShowImageCropper = true
} }
} }
})
.onDisappear {
isImageLoading = false
}
.sheet(isPresented: $isShowImageCropper, onDismiss: {
cropSourceImage = nil
}) {
if let cropSourceImage {
ImageCropEditorView(
image: cropSourceImage,
aspectPolicy: .square,
onCancel: {
isShowImageCropper = false
},
onComplete: { croppedImage in
viewModel.coverImage = croppedImage
isShowImageCropper = false
}
)
}
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct ContentCurationView: View { struct ContentCurationView: View {
@StateObject var viewModel = ContentCurationViewModel() @StateObject var viewModel = ContentCurationViewModel()
@State private var isInitialized = false
let title: String let title: String
let curationId: Int let curationId: Int
@@ -21,7 +22,7 @@ struct ContentCurationView: View {
] ]
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: title) DetailNavigationBar(title: title)
@@ -119,8 +120,11 @@ struct ContentCurationView: View {
} }
} }
.onAppear { .onAppear {
viewModel.curationId = curationId if !isInitialized || viewModel.curationId != curationId {
viewModel.getContentList() viewModel.curationId = curationId
viewModel.getContentList()
isInitialized = true
}
} }
} }
} }

View File

@@ -25,7 +25,7 @@ struct AudioContentCommentListView: View {
@State private var isShowMemberProfilePopup: Bool = false @State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
NavigationView { Group {
ZStack { ZStack {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
@@ -188,23 +188,7 @@ struct AudioContentCommentListView: View {
viewModel.audioContentId = audioContentId viewModel.audioContentId = audioContentId
viewModel.getCommentList() viewModel.getCommentList()
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
} }
} }

View File

@@ -21,6 +21,8 @@ struct ContentDetailView: View {
@State private var isShowCommentListView = false @State private var isShowCommentListView = false
@State private var isShowFollowNotifyDialog: Bool = false @State private var isShowFollowNotifyDialog: Bool = false
@State private var creatorId: Int = 0 @State private var creatorId: Int = 0
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
@State private var isViewVisible: Bool = false
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
@@ -28,11 +30,7 @@ struct ContentDetailView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Button { Button {
if presentationMode.wrappedValue.isPresented { goBack()
presentationMode.wrappedValue.dismiss()
} else {
AppState.shared.back()
}
} label: { } label: {
Image("ic_back") Image("ic_back")
.resizable() .resizable()
@@ -217,9 +215,26 @@ struct ContentDetailView: View {
.navigationTitle("") .navigationTitle("")
.navigationBarBackButtonHidden() .navigationBarBackButtonHidden()
.onAppear { .onAppear {
isViewVisible = true
didTriggerAutoBackOnLoadFailure = false
viewModel.contentId = contentId viewModel.contentId = contentId
AppState.shared.pushAudioContentId = 0 AppState.shared.pushAudioContentId = 0
} }
.onDisappear {
isViewVisible = false
}
.onChange(of: viewModel.isShowPopup) { isShowing in
guard isShowing else { return }
guard viewModel.audioContent == nil else { return }
guard !didTriggerAutoBackOnLoadFailure else { return }
didTriggerAutoBackOnLoadFailure = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
guard isViewVisible else { return }
goBack()
}
}
if let audioContent = viewModel.audioContent, isShowOrderView { if let audioContent = viewModel.audioContent, isShowOrderView {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -407,31 +422,26 @@ struct ContentDetailView: View {
.sheet( .sheet(
isPresented: $isShowCommentListView, isPresented: $isShowCommentListView,
content: { content: {
AudioContentCommentListView( NavigationStack {
isPresented: $isShowCommentListView, AudioContentCommentListView(
creatorId: viewModel.audioContent!.creator.creatorId, isPresented: $isShowCommentListView,
audioContentId: viewModel.audioContent!.contentId, creatorId: viewModel.audioContent!.creator.creatorId,
isShowSecret: viewModel.audioContent!.existOrdered audioContentId: viewModel.audioContent!.contentId,
) isShowSecret: viewModel.audioContent!.existOrdered
)
}
.toolbar(.hidden, for: .navigationBar)
} }
) )
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in }
HStack { }
Spacer()
Text(viewModel.errorMessage) private func goBack() {
.padding(.vertical, 13.3) if presentationMode.wrappedValue.isPresented {
.frame(width: screenSize().width - 66.7, alignment: .center) presentationMode.wrappedValue.dismiss()
.appFont(size: 12, weight: .medium) } else {
.background(Color(hex: "9970ff")) AppState.shared.back()
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
} }
} }
} }

View File

@@ -22,10 +22,32 @@ struct LiveRoomDonationDialogView: View {
@Binding var isShowing: Bool @Binding var isShowing: Bool
let isAudioContentDonation: Bool let isAudioContentDonation: Bool
let messageLimit: Int
let secretLabel: String
let secretMinimumCanMessage: String
let shouldPrefixSecretInMessagePlaceholder: Bool
let onClickDonation: (Int, String, Bool) -> Void let onClickDonation: (Int, String, Bool) -> Void
@StateObject var keyboardHandler = KeyboardHandler() @StateObject var keyboardHandler = KeyboardHandler()
init(
isShowing: Binding<Bool>,
isAudioContentDonation: Bool,
messageLimit: Int = 1000,
secretLabel: String = I18n.LiveRoom.secretMissionLabel,
secretMinimumCanMessage: String = I18n.LiveRoom.secretMissionMinimumCanMessage,
shouldPrefixSecretInMessagePlaceholder: Bool = true,
onClickDonation: @escaping (Int, String, Bool) -> Void
) {
self._isShowing = isShowing
self.isAudioContentDonation = isAudioContentDonation
self.messageLimit = messageLimit
self.secretLabel = secretLabel
self.secretMinimumCanMessage = secretMinimumCanMessage
self.shouldPrefixSecretInMessagePlaceholder = shouldPrefixSecretInMessagePlaceholder
self.onClickDonation = onClickDonation
}
var body: some View { var body: some View {
ZStack { ZStack {
Color.black Color.black
@@ -92,7 +114,7 @@ struct LiveRoomDonationDialogView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text("비밀미션") Text(secretLabel)
.appFont(size: 14.7, weight: .medium) .appFont(size: 14.7, weight: .medium)
.foregroundColor(isSecret ? Color.button : Color.grayee) .foregroundColor(isSecret ? Color.button : Color.grayee)
} }
@@ -204,7 +226,10 @@ struct LiveRoomDonationDialogView: View {
.stroke(Color.graybb, lineWidth: 1) .stroke(Color.graybb, lineWidth: 1)
) )
TextField("함께 보낼 \(isSecret ? "비밀 " : "")메시지 입력(최대 1000자)", text: $donationMessage) TextField(
"함께 보낼 \((isSecret && shouldPrefixSecretInMessagePlaceholder) ? "비밀 " : "")메시지 입력(최대 \(messageLimit)자)",
text: $donationMessage
)
.appFont(size: 13.3, weight: .medium) .appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
.padding(13.3) .padding(13.3)
@@ -243,7 +268,7 @@ struct LiveRoomDonationDialogView: View {
.onTapGesture { .onTapGesture {
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, let can = Int(donationCan) { if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, let can = Int(donationCan) {
if isSecret && can < 10 { if isSecret && can < 10 {
errorMessage = "비밀 미션은 최소 10캔 이상부터 이용이 가능합니다." errorMessage = secretMinimumCanMessage
isShowErrorPopup = true isShowErrorPopup = true
} else if can < 1 { } else if can < 1 {
errorMessage = "1캔 이상 후원하실 수 있습니다." errorMessage = "1캔 이상 후원하실 수 있습니다."
@@ -266,28 +291,14 @@ struct LiveRoomDonationDialogView: View {
.background(Color.gray22) .background(Color.gray22)
.cornerRadius(20, corners: [.topLeft, .topRight]) .cornerRadius(20, corners: [.topLeft, .topRight])
} }
.popup(isPresented: $isShowErrorPopup, type: .toast, position: .bottom, autohideIn: 1.3) { .sodaToast(isPresented: $isShowErrorPopup, message: errorMessage, autohideIn: 1.3)
HStack {
Spacer()
Text(errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight) .offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight)
} }
} }
func limitText() { func limitText() {
if donationMessage.count > 1000 { if donationMessage.count > messageLimit {
donationMessage = String(donationMessage.prefix(1000)) donationMessage = String(donationMessage.prefix(messageLimit))
} }
} }
} }

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainAlarmAllView: View { struct ContentMainAlarmAllView: View {
@StateObject var viewModel = ContentMainAlarmAllViewModel() @StateObject var viewModel = ContentMainAlarmAllViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 알람") DetailNavigationBar(title: "새로운 알람")
@@ -81,7 +82,10 @@ struct ContentMainAlarmAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.getContentMainAlarmAll() if !isInitialized {
viewModel.getContentMainAlarmAll()
isInitialized = true
}
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@@ -50,21 +50,7 @@ struct ContentMainTabAlarmView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainAsmrAllView: View { struct ContentMainAsmrAllView: View {
@StateObject var viewModel = ContentNewAllViewModel() @StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 ASMR") DetailNavigationBar(title: "새로운 ASMR")
@@ -72,7 +73,14 @@ struct ContentMainAsmrAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.selectedTheme = "ASMR" if !isInitialized {
if viewModel.selectedTheme != "ASMR" {
viewModel.selectedTheme = "ASMR"
} else if viewModel.newContentList.isEmpty {
viewModel.getNewContentList()
}
isInitialized = true
}
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@@ -60,21 +60,7 @@ struct ContentMainTabAsmrView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -84,21 +84,7 @@ struct ContentMainTabContentView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -46,7 +46,7 @@ struct ContentMainViewV2: View {
} }
var body: some View { var body: some View {
NavigationView { Group {
ZStack { ZStack {
Color.black.ignoresSafeArea() Color.black.ignoresSafeArea()

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainIntroduceCreatorAllView: View { struct ContentMainIntroduceCreatorAllView: View {
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel() @StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
DetailNavigationBar(title: "크리에이터 소개") DetailNavigationBar(title: "크리에이터 소개")
@@ -48,25 +49,14 @@ struct ContentMainIntroduceCreatorAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.getIntroduceCreatorList() if !isInitialized {
viewModel.getIntroduceCreatorList()
isInitialized = true
}
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }
} }

View File

@@ -78,21 +78,7 @@ struct ContentMainTabFreeView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -291,21 +291,7 @@ struct ContentMainTabHomeView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }
} }

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainReplayAllView: View { struct ContentMainReplayAllView: View {
@StateObject var viewModel = ContentNewAllViewModel() @StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) { VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 라이브 다시듣기") DetailNavigationBar(title: "새로운 라이브 다시듣기")
@@ -72,7 +73,14 @@ struct ContentMainReplayAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.selectedTheme = "다시듣기" if !isInitialized {
if viewModel.selectedTheme != "다시듣기" {
viewModel.selectedTheme = "다시듣기"
} else if viewModel.newContentList.isEmpty {
viewModel.getNewContentList()
}
isInitialized = true
}
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@@ -60,21 +60,7 @@ struct ContentMainTabReplayView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -60,21 +60,7 @@ struct CompletedSeriesView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.getCompletedSeries() viewModel.getCompletedSeries()
} }

View File

@@ -86,21 +86,7 @@ struct ContentMainTabSeriesView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
} }
} }

View File

@@ -277,23 +277,7 @@ struct ContentModifyView: View {
} }
.onTapGesture { hideKeyboard() } .onTapGesture { hideKeyboard() }
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear { .onAppear {
viewModel.contentId = contentId viewModel.contentId = contentId
viewModel.getAudioContentDetail { viewModel.getAudioContentDetail {

View File

@@ -71,21 +71,7 @@ struct ContentPlaylistListView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.getPlaylistList() viewModel.getPlaylistList()
} }

View File

@@ -138,21 +138,7 @@ struct ContentPlaylistCreateView: View {
} }
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
if isShowAddContentView { if isShowAddContentView {
PlaylistAddContentView( PlaylistAddContentView(

View File

@@ -238,21 +238,7 @@ struct ContentPlaylistDetailView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.playlistId = playlistId viewModel.playlistId = playlistId
} }

View File

@@ -139,21 +139,7 @@ struct ContentPlaylistModifyView: View {
} }
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.playlistId = playlistId viewModel.playlistId = playlistId
} }

View File

@@ -17,6 +17,8 @@ struct SeriesDetailView: View {
@State private var isShowFollowNotifyDialog: Bool = false @State private var isShowFollowNotifyDialog: Bool = false
@State private var creatorId: Int = 0 @State private var creatorId: Int = 0
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
@State private var isViewVisible: Bool = false
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
@@ -38,11 +40,7 @@ struct SeriesDetailView: View {
.resizable() .resizable()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.onTapGesture { .onTapGesture {
if presentationMode.wrappedValue.isPresented { goBack()
presentationMode.wrappedValue.dismiss()
} else {
AppState.shared.back()
}
} }
Spacer() Spacer()
@@ -243,9 +241,35 @@ struct SeriesDetailView: View {
.navigationBarBackButtonHidden() .navigationBarBackButtonHidden()
} }
.onAppear { .onAppear {
isViewVisible = true
didTriggerAutoBackOnLoadFailure = false
viewModel.seriesId = seriesId viewModel.seriesId = seriesId
viewModel.getSeriesDetail() viewModel.getSeriesDetail()
} }
.onDisappear {
isViewVisible = false
}
.onChange(of: viewModel.isShowPopup) { isShowing in
guard isShowing else { return }
guard viewModel.seriesDetail == nil else { return }
guard !didTriggerAutoBackOnLoadFailure else { return }
didTriggerAutoBackOnLoadFailure = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
guard isViewVisible else { return }
goBack()
}
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
}
private func goBack() {
if presentationMode.wrappedValue.isPresented {
presentationMode.wrappedValue.dismiss()
} else {
AppState.shared.back()
}
} }
} }

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct SeriesMainByGenreView: View { struct SeriesMainByGenreView: View {
@StateObject var viewModel = SeriesMainByGenreViewModel() @StateObject var viewModel = SeriesMainByGenreViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
ZStack { ZStack {
@@ -41,39 +42,27 @@ struct SeriesMainByGenreView: View {
) { ) {
ForEach(viewModel.seriesList.indices, id: \.self) { index in ForEach(viewModel.seriesList.indices, id: \.self) { index in
let item = viewModel.seriesList[index] let item = viewModel.seriesList[index]
NavigationLink { SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
SeriesDetailView(seriesId: item.seriesId) .contentShape(Rectangle())
} label: { .onAppear {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160) if index == viewModel.seriesList.count - 1 {
.contentShape(Rectangle()) viewModel.getSeriesListByGenre()
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesListByGenre()
}
} }
} }
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
} }
} }
.padding(.horizontal, horizontalPadding) .padding(.horizontal, horizontalPadding)
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.getGenreList() if !isInitialized {
viewModel.getGenreList()
isInitialized = true
}
} }
if viewModel.isLoading { if viewModel.isLoading {

View File

@@ -75,37 +75,20 @@ struct SeriesMainDayOfWeekView: View {
) { ) {
ForEach(viewModel.seriesList.indices, id: \.self) { index in ForEach(viewModel.seriesList.indices, id: \.self) { index in
let item = viewModel.seriesList[index] let item = viewModel.seriesList[index]
NavigationLink { SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
SeriesDetailView(seriesId: item.seriesId) .contentShape(Rectangle())
} label: { .onAppear {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160) if index == viewModel.seriesList.count - 1 {
.contentShape(Rectangle()) viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
}
} }
} }
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
} }
} }
.padding(.horizontal, horizontalPadding) .padding(.horizontal, horizontalPadding)
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
if !isInitialized { if !isInitialized {
dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())] dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())]

View File

@@ -22,11 +22,8 @@ struct SeriesMainHomeBannerView: View {
ForEach(0..<bannerList.count, id: \.self) { index in ForEach(0..<bannerList.count, id: \.self) { index in
let item = bannerList[index] let item = bannerList[index]
NavigationLink { SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
SeriesDetailView(seriesId: item.seriesId) .onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
} label: {
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
}
} }
} }
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct SeriesMainHomeView: View { struct SeriesMainHomeView: View {
@StateObject var viewModel = SeriesMainHomeViewModel() @StateObject var viewModel = SeriesMainHomeViewModel()
@State private var isInitialized = false
var body: some View { var body: some View {
ZStack { ZStack {
@@ -43,11 +44,10 @@ struct SeriesMainHomeView: View {
LazyHStack(spacing: 16) { LazyHStack(spacing: 16) {
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) { ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
let item = viewModel.completedSeriesList[$0] let item = viewModel.completedSeriesList[$0]
NavigationLink { SeriesMainItemView(item: item)
SeriesDetailView(seriesId: item.seriesId) .onTapGesture {
} label: { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
SeriesMainItemView(item: item) }
}
} }
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
@@ -89,11 +89,10 @@ struct SeriesMainHomeView: View {
) { ) {
ForEach(viewModel.recommendSeriesList.indices, id: \.self) { ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
let item = viewModel.recommendSeriesList[$0] let item = viewModel.recommendSeriesList[$0]
NavigationLink { SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
SeriesDetailView(seriesId: item.seriesId) .onTapGesture {
} label: { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
SeriesMainItemView(item: item, width: width, height: width * 227 / 160) }
}
} }
} }
.padding(.horizontal, horizontalPadding) .padding(.horizontal, horizontalPadding)
@@ -101,23 +100,12 @@ struct SeriesMainHomeView: View {
} }
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
.onAppear { .onAppear {
viewModel.fetchHome() if !isInitialized {
viewModel.fetchHome()
isInitialized = true
}
} }
if viewModel.isLoading { if viewModel.isLoading {

View File

@@ -25,7 +25,7 @@ struct SeriesMainView: View {
@State private var selectedTab: InnerTab = .home @State private var selectedTab: InnerTab = .home
var body: some View { var body: some View {
NavigationView { Group {
BaseView { BaseView {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "시리즈 전체보기") DetailNavigationBar(title: "시리즈 전체보기")

View File

@@ -9,7 +9,8 @@ import SwiftUI
struct SeriesListAllView: View { struct SeriesListAllView: View {
@ObservedObject var viewModel = SeriesListAllViewModel() @StateObject var viewModel = SeriesListAllViewModel()
@State private var isInitialized = false
var creatorId: Int? = nil var creatorId: Int? = nil
var creatorNickname: String? = nil var creatorNickname: String? = nil
@@ -18,7 +19,7 @@ struct SeriesListAllView: View {
var isCompleted = false var isCompleted = false
var body: some View { var body: some View {
NavigationView { Group {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
if isCompleted { if isCompleted {
@@ -48,17 +49,16 @@ struct SeriesListAllView: View {
) { ) {
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
let item = viewModel.seriesList[index] let item = viewModel.seriesList[index]
NavigationLink { SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
SeriesDetailView(seriesId: item.seriesId) .contentShape(Rectangle())
} label: { .onAppear {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160) if index == viewModel.seriesList.count - 1 {
.contentShape(Rectangle()) viewModel.getSeriesList()
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesList()
}
} }
} }
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
} }
} }
.padding(horizontalPadding) .padding(horizontalPadding)
@@ -67,10 +67,24 @@ struct SeriesListAllView: View {
} }
} }
.onAppear { .onAppear {
viewModel.creatorId = creatorId let hasFilterChanged =
viewModel.isOriginal = isOriginal viewModel.creatorId != creatorId ||
viewModel.isCompleted = isCompleted viewModel.isOriginal != isOriginal ||
viewModel.getSeriesList() viewModel.isCompleted != isCompleted
if !isInitialized || hasFilterChanged {
if hasFilterChanged {
viewModel.page = 1
viewModel.isLast = false
viewModel.seriesList.removeAll()
}
viewModel.creatorId = creatorId
viewModel.isOriginal = isOriginal
viewModel.isCompleted = isCompleted
viewModel.getSeriesList()
isInitialized = true
}
} }
} }
} }

View File

@@ -15,300 +15,332 @@ struct ContentView: View {
@State private var message = "" @State private var message = ""
var body: some View { var body: some View {
ZStack { NavigationStack(path: $appState.navigationPath) {
Color.black.ignoresSafeArea() ZStack {
Color.black.ignoresSafeArea()
if appState.isRestartApp { if appState.isRestartApp {
EmptyView() EmptyView()
} else { } else {
HomeView() HomeView()
}
if case .splash = appState.rootStep {
AppStepLayerView(step: .splash, canPgPaymentViewModel: canPgPaymentViewModel)
.navigationBarBackButtonHidden(true)
}
if let liveDetailSheet = appState.liveDetailSheet {
LiveDetailView(
roomId: liveDetailSheet.roomId,
onClickParticipant: liveDetailSheet.onClickParticipant,
onClickReservation: liveDetailSheet.onClickReservation,
onClickStart: liveDetailSheet.onClickStart,
onClickCancel: liveDetailSheet.onClickCancel,
onClickClose: {
withAnimation {
appState.hideLiveDetailSheet()
}
}
)
}
if isShowDialog {
SodaDialog(
title: I18n.Common.pointGrantTitle,
desc: message,
confirmButtonTitle: I18n.Common.confirm
) {
isShowDialog = false
message = ""
}
}
} }
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
switch appState.appStep { if let msg = $0.object as? String {
case .splash: self.message = msg
SplashView() self.isShowDialog = true
}
case .login:
LoginView()
case .signUp:
SignUpView()
case .findPassword:
FindPasswordView()
case .textMessageDetail(let messageItem, let messageBox, let refresh):
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
case .writeTextMessage(let userId, let nickname):
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
case .settings:
SettingsView()
case .languageSettings:
LanguageSettingsView()
case .notices:
NoticeListView()
case .noticeDetail(let notice):
NoticeDetailView(notice: notice)
case .events:
EventListView()
case .eventDetail(let event):
EventDetailView(event: event)
case .terms:
TermsView(isPrivacyPolicy: false)
case .privacy:
TermsView(isPrivacyPolicy: true)
case .notificationSettings:
NotificationSettingsView()
case .contentViewSettings:
ContentSettingsView()
case .signOut:
SignOutView()
case .canStatus(let refresh):
CanStatusView(refresh: refresh)
case .canCharge(let refresh, let afterCompletionToGoBack):
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
.environmentObject(canPgPaymentViewModel)
case .liveReservation:
LiveReservationStatusView()
case .liveReservationCancel(let reservationId):
LiveReservationCancelView(reservationId: reservationId)
case .serviceCenter:
ServiceCenterView()
case .createContent:
ContentCreateView()
case .liveReservationComplete(let response):
LiveReservationCompleteView(reservationCompleteData: response)
case .creatorDetail(let userId):
UserProfileView(userId: userId)
case .followerList(let userId):
FollowerListView(userId: userId)
case .modifyContent(let contentId):
ContentModifyView(contentId: contentId)
case .contentListAll(let userId):
ContentListView(userId: userId)
case .contentDetail(let contentId):
ContentDetailView(contentId: contentId)
case .createLive(let timeSettingMode, let onSuccess):
LiveRoomCreateView(
timeSettingMode: timeSettingMode,
onSuccess: onSuccess
)
case .liveNowAll(let onClickParticipant):
LiveNowAllView(onClickParticipant: onClickParticipant)
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
LiveReservationAllView(
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel,
onTapCreateLive: onTapCreateLive
)
case .modifyLive(let room):
LiveRoomEditView(room: room)
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
LiveDetailView(
roomId: roomId,
onClickParticipant: onClickParticipant,
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel
)
case .modifyPassword:
ModifyPasswordView()
case .changeNickname:
NicknameUpdateView()
case .profileUpdate(let refresh):
ProfileUpdateView(refresh: refresh)
case .followingList:
FollowCreatorView()
case .orderListAll:
OrderListAllView()
case .userProfileDonationAll(let userId):
UserProfileDonationAllView(userId: userId)
case .userProfileFanTalkAll(let userId):
UserProfileFanTalkAllView(userId: userId)
case .newContentAll(let isFree):
ContentNewAllView(isFree: isFree)
case .curationAll(let title, let curationId):
ContentCurationView(title: title, curationId: curationId)
case .contentRankingAll:
ContentRankingAllView()
case .creatorCommunityAll(let creatorId):
CreatorCommunityAllView(creatorId: creatorId)
case .creatorCommunityWrite(let onSuccess):
CreatorCommunityWriteView(onSuccess: onSuccess)
case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
case .canCoupon(let refresh):
CanCouponView(refresh: refresh)
case .contentAllByTheme(let themeId):
ContentAllByThemeView(themeId: themeId)
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
case .seriesDetail(let seriesId):
SeriesDetailView(seriesId: seriesId)
case .seriesContentAll(let seriesId, let seriesTitle):
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
case .myBox(let currentTab):
ContentBoxView(initCurrentTab: currentTab)
case .auditionDetail(let auditionId):
AuditionDetailView(auditionId: auditionId)
case .auditionRoleDetail(let roleId, let auditionTitle):
AuditionRoleDetailView(
roleId: roleId,
auditionTitle: auditionTitle
)
case .search:
SearchView()
case .contentMain(let startTab):
ContentMainViewV2(selectedTab: startTab)
case .completedSeriesAll:
CompletedSeriesView()
case .newAlarmContentAll:
ContentMainAlarmAllView()
case .newAsmrContentAll:
ContentMainAsmrAllView()
case .newReplayContentAll:
ContentMainReplayAllView()
case .introduceCreatorAll:
ContentMainIntroduceCreatorAllView()
case .message:
MessageView()
case .pointStatus(let refresh):
PointStatusView(refresh: refresh)
case .audition:
AuditionView()
case .characterDetail(let characterId):
CharacterDetailView(characterId: characterId)
case .chatRoom(let id):
ChatRoomView(roomId: id)
case .newCharacterAll:
NewCharacterListView()
case .originalWorkDetail(let originalId):
OriginalWorkDetailView(originalId: originalId)
case .contentAll(let isFree, let isPointOnly):
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
case .seriesMain:
SeriesMainView()
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
} }
.sodaToast(isPresented: $appState.isShowErrorPopup, message: appState.errorMessage, autohideIn: 1)
if isShowDialog { .navigationDestination(for: AppRoute.self) { route in
SodaDialog( if let step = appState.appStep(for: route) {
title: I18n.Common.pointGrantTitle, AppStepLayerView(step: step, canPgPaymentViewModel: canPgPaymentViewModel)
desc: message, .navigationBarBackButtonHidden(true)
confirmButtonTitle: I18n.Common.confirm } else {
) { EmptyView()
isShowDialog = false .frame(width: 0, height: 0, alignment: .topLeading)
message = ""
} }
} }
} }
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) { }
if let msg = $0.object as? String { }
self.message = msg
self.isShowDialog = true struct AppStepLayerView: View {
} let step: AppStep
} @ObservedObject var canPgPaymentViewModel: CanPgPaymentViewModel
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
GeometryReader { geo in @ViewBuilder
HStack { var body: some View {
Spacer() switch step {
Text(appState.errorMessage) case .splash:
.padding(.vertical, 13.3) SplashView()
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium) case .login:
.background(Color.button) LoginView()
.foregroundColor(Color.white)
.multilineTextAlignment(.center) case .signUp:
.cornerRadius(20) SignUpView()
.padding(.top, 66.7)
Spacer() case .findPassword:
} FindPasswordView()
}
case .textMessageDetail(let messageItem, let messageBox, let refresh):
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
case .writeTextMessage(let userId, let nickname):
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
case .settings:
SettingsView()
case .languageSettings:
LanguageSettingsView()
case .notices:
NoticeListView()
case .noticeDetail(let notice):
NoticeDetailView(notice: notice)
case .events:
EventListView()
case .eventDetail(let event):
EventDetailView(event: event)
case .terms:
TermsView(isPrivacyPolicy: false)
case .privacy:
TermsView(isPrivacyPolicy: true)
case .notificationSettings:
NotificationSettingsView()
case .notificationReceiveSettings:
NotificationReceiveSettingsView()
case .contentViewSettings:
ContentSettingsView()
case .signOut:
SignOutView()
case .canStatus(let refresh):
CanStatusView(refresh: refresh)
case .canCharge(let refresh, let afterCompletionToGoBack):
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
.environmentObject(canPgPaymentViewModel)
case .liveReservation:
LiveReservationStatusView()
case .liveReservationCancel(let reservationId):
LiveReservationCancelView(reservationId: reservationId)
case .serviceCenter:
ServiceCenterView()
case .createContent:
ContentCreateView()
case .liveReservationComplete(let response):
LiveReservationCompleteView(reservationCompleteData: response)
case .creatorDetail(let userId):
UserProfileView(userId: userId)
case .followerList(let userId):
FollowerListView(userId: userId)
case .modifyContent(let contentId):
ContentModifyView(contentId: contentId)
case .contentListAll(let userId):
ContentListView(userId: userId)
case .contentDetail(let contentId):
ContentDetailView(contentId: contentId)
case .createLive(let timeSettingMode, let onSuccess):
LiveRoomCreateView(
timeSettingMode: timeSettingMode,
onSuccess: onSuccess
)
case .liveNowAll(let onClickParticipant):
LiveNowAllView(onClickParticipant: onClickParticipant)
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
LiveReservationAllView(
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel,
onTapCreateLive: onTapCreateLive
)
case .modifyLive(let room):
LiveRoomEditView(room: room)
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
LiveDetailView(
roomId: roomId,
onClickParticipant: onClickParticipant,
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel
)
case .modifyPassword:
ModifyPasswordView()
case .changeNickname:
NicknameUpdateView()
case .profileUpdate(let refresh):
ProfileUpdateView(refresh: refresh)
case .followingList:
FollowCreatorView()
case .orderListAll:
OrderListAllView()
case .userProfileDonationAll(let userId):
UserProfileDonationAllView(userId: userId)
case .channelDonationAll(let creatorId):
ChannelDonationAllView(creatorId: creatorId)
case .userProfileFanTalkAll(let userId):
UserProfileFanTalkAllView(userId: userId)
case .newContentAll(let isFree):
ContentNewAllView(isFree: isFree)
case .curationAll(let title, let curationId):
ContentCurationView(title: title, curationId: curationId)
case .contentRankingAll:
ContentRankingAllView()
case .creatorCommunityAll(let creatorId):
CreatorCommunityAllView(creatorId: creatorId)
case .creatorCommunityWrite(let onSuccess):
CreatorCommunityWriteView(onSuccess: onSuccess)
case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
case .canCoupon(let refresh):
CanCouponView(refresh: refresh)
case .contentAllByTheme(let themeId):
ContentAllByThemeView(themeId: themeId)
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
case .seriesDetail(let seriesId):
SeriesDetailView(seriesId: seriesId)
case .seriesContentAll(let seriesId, let seriesTitle):
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
case .myBox(let currentTab):
ContentBoxView(initCurrentTab: currentTab)
case .auditionDetail(let auditionId):
AuditionDetailView(auditionId: auditionId)
case .auditionRoleDetail(let roleId, let auditionTitle):
AuditionRoleDetailView(
roleId: roleId,
auditionTitle: auditionTitle
)
case .search:
SearchView()
case .contentMain(let startTab):
ContentMainViewV2(selectedTab: startTab)
case .completedSeriesAll:
CompletedSeriesView()
case .newAlarmContentAll:
ContentMainAlarmAllView()
case .newAsmrContentAll:
ContentMainAsmrAllView()
case .newReplayContentAll:
ContentMainReplayAllView()
case .introduceCreatorAll:
ContentMainIntroduceCreatorAllView()
case .message:
MessageView()
case .notificationList:
PushNotificationListView()
case .pointStatus(let refresh):
PointStatusView(refresh: refresh)
case .audition:
AuditionView()
case .characterDetail(let characterId):
CharacterDetailView(characterId: characterId)
case .chatRoom(let id):
ChatRoomView(roomId: id)
case .newCharacterAll:
NewCharacterListView()
case .originalWorkDetail(let originalId):
OriginalWorkDetailView(originalId: originalId)
case .contentAll(let isFree, let isPointOnly):
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
case .seriesMain:
SeriesMainView()
case .main:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
} }
} }
} }

View File

@@ -29,3 +29,6 @@ let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.g
let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33" let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33"
let LINE_CHANNEL_ID = "2008995582" let LINE_CHANNEL_ID = "2008995582"
let AGORA_CUSTOMER_ID = "de5dd9ea151f4a43ba1ad8411817b169"
let AGORA_CUSTOMER_SECRET = "3855da8bc5ae4743af8bf4f87408b515"

View File

@@ -121,26 +121,7 @@ struct MemberProfileDialog: View {
viewModel.getMemberProfile(memberId: memberId) viewModel.getMemberProfile(memberId: memberId)
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
.onDisappear {
if viewModel.dismissDialog {
isShowing = false
}
}
}
if viewModel.isShowUesrBlockConfirm { if viewModel.isShowUesrBlockConfirm {
UserBlockConfirmDialogView( UserBlockConfirmDialogView(

View File

@@ -13,12 +13,15 @@ enum ExplorerApi {
case getExplorer case getExplorer
case searchChannel(channel: String) case searchChannel(channel: String)
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool) case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
case getCreatorDetail(userId: Int)
case getFollowerList(userId: Int, page: Int, size: Int) case getFollowerList(userId: Int, page: Int, size: Int)
case getCreatorProfileCheers(userId: Int, page: Int, size: Int) case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String) case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
case modifyCheers(request: PutModifyCheersRequest) case modifyCheers(request: PutModifyCheersRequest)
case writeCreatorNotice(request: PostCreatorNoticeRequest) case writeCreatorNotice(request: PostCreatorNoticeRequest)
case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int) case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int, period: DonationRankingPeriod?)
case getChannelDonationList(creatorId: Int)
case postChannelDonation(request: PostChannelDonationRequest)
} }
extension ExplorerApi: TargetType { extension ExplorerApi: TargetType {
@@ -40,7 +43,10 @@ extension ExplorerApi: TargetType {
case .getCreatorProfile(let userId, _): case .getCreatorProfile(let userId, _):
return "/explorer/profile/\(userId)" return "/explorer/profile/\(userId)"
case .getCreatorProfileDonationRanking(let userId, _, _): case .getCreatorDetail(let userId):
return "/explorer/profile/\(userId)/detail"
case .getCreatorProfileDonationRanking(let userId, _, _, _):
return "/explorer/profile/\(userId)/donation-rank" return "/explorer/profile/\(userId)/donation-rank"
case .getFollowerList(let userId, _, _): case .getFollowerList(let userId, _, _):
@@ -57,15 +63,18 @@ extension ExplorerApi: TargetType {
case .writeCreatorNotice: case .writeCreatorNotice:
return "/explorer/profile/notice" return "/explorer/profile/notice"
case .getChannelDonationList, .postChannelDonation:
return "/explorer/profile/channel-donation"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank: case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank, .getChannelDonationList:
return .get return .get
case .writeCheers, .writeCreatorNotice: case .writeCheers, .writeCreatorNotice, .postChannelDonation:
return .post return .post
case .modifyCheers: case .modifyCheers:
@@ -75,7 +84,7 @@ extension ExplorerApi: TargetType {
var task: Task { var task: Task {
switch self { switch self {
case .getExplorer, .getCreatorRank: case .getExplorer, .getCreatorRank, .getCreatorDetail:
return .requestPlain return .requestPlain
case .searchChannel(let channel): case .searchChannel(let channel):
@@ -112,12 +121,22 @@ extension ExplorerApi: TargetType {
case .writeCreatorNotice(let request): case .writeCreatorNotice(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .getCreatorProfileDonationRanking(_, let page, let size): case .getChannelDonationList(let creatorId):
let parameters = [ return .requestParameters(parameters: ["creatorId": creatorId], encoding: URLEncoding.queryString)
case .postChannelDonation(let request):
return .requestJSONEncodable(request)
case .getCreatorProfileDonationRanking(_, let page, let size, let period):
var parameters = [
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
if let period {
parameters["period"] = period.rawValue
}
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
} }
} }

View File

@@ -30,6 +30,10 @@ final class ExplorerRepository {
) )
} }
func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getCreatorDetail(userId: id))
}
func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size)) return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size))
} }
@@ -51,7 +55,27 @@ final class ExplorerRepository {
return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice))) return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice)))
} }
func getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getCreatorProfileDonationRanking(
return api.requestPublisher(.getCreatorProfileDonationRanking(userId: userId, page: page, size: size)) userId: Int,
page: Int,
size: Int,
period: DonationRankingPeriod?
) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(
.getCreatorProfileDonationRanking(
userId: userId,
page: page,
size: size,
period: period
)
)
}
func getChannelDonationList(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getChannelDonationList(creatorId: creatorId))
}
func postChannelDonation(request: PostChannelDonationRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.postChannelDonation(request: request))
} }
} }

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