Compare commits
58 Commits
ed9c2d9d32
...
afca24c221
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afca24c221 | ||
|
|
606470ae04 | ||
|
|
039355c088 | ||
|
|
5e0f6fd3e3 | ||
|
|
99fcf3a94c | ||
|
|
408c3b7619 | ||
|
|
37e361b1e9 | ||
|
|
2b20e7a9a3 | ||
|
|
5e08711b29 | ||
|
|
de627e1700 | ||
|
|
3d4f67dbd5 | ||
|
|
82889f405a | ||
|
|
4d39a07fbd | ||
|
|
026f855bc5 | ||
|
|
fb85f3e90c | ||
|
|
19f5cc8ad6 | ||
|
|
abe939e768 | ||
|
|
d5d5d97c2a | ||
|
|
af8813685e | ||
|
|
2b58a0147b | ||
|
|
4fc7f6a39a | ||
|
|
cab9795557 | ||
|
|
33f9ddfd12 | ||
|
|
298c02b83f | ||
|
|
42ce09d927 | ||
|
|
f145de87aa | ||
|
|
d29e23b9cf | ||
|
|
ca565a2b5f | ||
|
|
f0763d75c2 | ||
|
|
9d6f0c648b | ||
|
|
38fb818f4b | ||
|
|
db68aa90d2 | ||
|
|
b84b996059 | ||
|
|
a4d6de83db | ||
|
|
c7ec9045ff | ||
|
|
32d1d970e4 | ||
|
|
e9bd1e7396 | ||
|
|
7ff9360b1e | ||
|
|
aaffd08cb5 | ||
|
|
b796f6d9c5 | ||
|
|
7f703024d8 | ||
|
|
7cba6de2fc | ||
|
|
5e0899419e | ||
|
|
68976e221c | ||
|
|
3590db82be | ||
|
|
3456510eec | ||
|
|
8d3aed41c2 | ||
|
|
c0288b5eb8 | ||
|
|
13f8d924c0 | ||
|
|
d686223362 | ||
|
|
652fe3dc13 | ||
|
|
36bf533269 | ||
|
|
5159debf7f | ||
|
|
b985af4497 | ||
|
|
9e97c301b8 | ||
|
|
f9d84efbe1 | ||
|
|
5352d28fe3 | ||
|
|
26f67028cf |
1
.gitignore
vendored
@@ -279,6 +279,5 @@ xcuserdata
|
||||
|
||||
.kiro/
|
||||
.junie/
|
||||
docs/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
|
||||
|
||||
21
.opencode/commands/commit.md
Normal 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
|
||||
46
.opencode/skills/commit-policy/SKILL.md
Normal 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
@@ -1,18 +1,160 @@
|
||||
질문에 대한 답변과 설명은 한국어로 한다.
|
||||
# AGENTS.md
|
||||
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
|
||||
|
||||
## Quality Assurance Guidelines
|
||||
## 커뮤니케이션 규칙
|
||||
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||
- 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||
- 코드 식별자, 파일 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||
|
||||
### Commit Standards
|
||||
1. Write in Korean.
|
||||
2. Use the present tense in the subject line (e.g., "Add feature" not "Added feature").
|
||||
3. Keep the subject line to 50 characters or less.
|
||||
4. Add a blank line between the subject and body.
|
||||
5. Keep the body to 72 characters or less per line.
|
||||
6. Within a paragraph, only break lines when the text exceeds 72 characters.
|
||||
7. Describe changes to public API features and do not include implementation details such as package-private code.
|
||||
8. Do not mention test code in commit messages.
|
||||
9. Do not use any prefix (such as "fix:", "update:", "docs:", "feat:", etc.) in the subject line.
|
||||
10. Do not start the subject line with a lowercase letter unless the first word is a function name or another identifier that is conventionally lowercase and there is a clear, justifiable reason for the exception. Otherwise, always start with an uppercase letter.
|
||||
11. Do not include tool advertisements, branding, or promotional content in commit messages.
|
||||
12. Use separate git commands to stage files before committing.
|
||||
13. Always validate commits using `work/scripts/check-commit-message-rules.sh` and fix until validation passes.
|
||||
## 저장소 범위
|
||||
- 앱 소스: `SodaLive/Sources/**`
|
||||
- 프로젝트/스킴: `SodaLive.xcodeproj`, `SodaLive.xcworkspace`
|
||||
- 의존성 설정: `Podfile`, `Podfile.lock`
|
||||
- 운영 스크립트: `work/scripts/**`
|
||||
- 생성/외부 결과물: `Pods/**`, `generated/**`, `build/**`
|
||||
- 작업 계획 문서: `docs/**`
|
||||
|
||||
### 수정 우선순위
|
||||
- 기능 변경은 `SodaLive/Sources/**`에서 해결한다.
|
||||
- 프로젝트 설정 변경은 필요한 경우에만 수행한다.
|
||||
- `Pods/**`, `generated/**`는 직접 수정하지 않는다.
|
||||
- `build/**`는 빌드 산출물로 간주하며 수정 대상이 아니다.
|
||||
|
||||
## 빌드/테스트/검증 명령
|
||||
아래 명령은 현재 저장소에서 확인된 공식 진입점이다.
|
||||
|
||||
### 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 키 변경 시 빌드 설정과 런타임 초기화 지점을 함께 검토한다.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37",
|
||||
"originHash" : "cf552e0db687218f4a2207a39678af43731c56f6f8ea12b111a15ac39574aa38",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
@@ -199,15 +199,6 @@
|
||||
"revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "popupview",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/exyte/PopupView.git",
|
||||
"state" : {
|
||||
"revision" : "1b99d6e9872ef91fd57aaef657661b5a00069638",
|
||||
"version" : "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
21
SodaLive/Resources/Assets.xcassets/ic_bell.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_bell.imageset/ic_bell.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_bell_settings.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_bell_settings.imageset/ic_bell_settings.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_community_grid.imageset/ic_community_grid.png
vendored
Normal file
|
After Width: | Height: | Size: 702 B |
21
SodaLive/Resources/Assets.xcassets/ic_community_grid_selected.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 594 B |
21
SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_community_list.imageset/ic_community_list.png
vendored
Normal file
|
After Width: | Height: | Size: 600 B |
21
SodaLive/Resources/Assets.xcassets/ic_community_list_selected.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 517 B |
21
SodaLive/Resources/Assets.xcassets/ic_live_creator_follow_alarm.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 703 B |
21
SodaLive/Resources/Assets.xcassets/ic_live_creator_follow_no_alarm.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_live_creator_follow_plus.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_message.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
@@ -9,7 +10,6 @@
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_message.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 385 B |
21
SodaLive/Resources/Assets.xcassets/ic_shield.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_shield.imageset/ic_shield.png
vendored
Normal file
|
After Width: | Height: | Size: 474 B |
21
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_fancimm.imageset/ic_sns_fancimm.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_instagram.imageset/ic_sns_instagram.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_kakao.imageset/ic_sns_kakao.png
vendored
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_x.imageset/ic_sns_x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
21
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_sns_youtube.imageset/ic_sns_youtube.png
vendored
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
@@ -16,6 +16,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
" · %@" : {
|
||||
|
||||
},
|
||||
" (" : {
|
||||
"localizations" : {
|
||||
@@ -2503,6 +2506,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"고정 해제" : {
|
||||
|
||||
},
|
||||
"공개 설정" : {
|
||||
"localizations" : {
|
||||
@@ -2967,6 +2973,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"누적" : {
|
||||
|
||||
},
|
||||
"눌러서 잠금해제" : {
|
||||
"localizations" : {
|
||||
@@ -4120,22 +4129,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"모든 기기에서 로그아웃" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Log out from all devices"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "全端末からログアウト"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"모집완료" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -4167,6 +4160,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"모든 기기에서 로그아웃" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Log out from all devices"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "全端末からログアウト"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
|
||||
|
||||
},
|
||||
"모집중" : {
|
||||
"localizations" : {
|
||||
@@ -5207,6 +5219,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"서비스 알림" : {
|
||||
|
||||
},
|
||||
"설정" : {
|
||||
"localizations" : {
|
||||
@@ -5815,6 +5830,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"알림이 없습니다." : {
|
||||
|
||||
},
|
||||
"앱 버전 정보" : {
|
||||
"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" : {
|
||||
"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" : {
|
||||
"en" : {
|
||||
@@ -7160,6 +7162,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"일" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sun"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "日"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"일간 랭킹" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -7831,6 +7849,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"주간" : {
|
||||
|
||||
},
|
||||
"중복확인" : {
|
||||
"localizations" : {
|
||||
@@ -8391,6 +8412,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"최상단에 고정" : {
|
||||
|
||||
},
|
||||
"최신 콘텐츠" : {
|
||||
"localizations" : {
|
||||
@@ -8632,22 +8656,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"캔" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cans"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "CAN"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"캐릭터 정보" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -8664,6 +8672,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"캔" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cans"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "CAN"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"캔 충전" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -9608,7 +9632,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"함께 보낼 %@메시지 입력(최대 %lld자)" : {
|
||||
"localizations" : {
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "함께 보낼 %1$@메시지 입력(최대 %2$lld자)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"함께 보낼 %@메시지 입력(최대 1000자)" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
|
||||
234
SodaLive/Sources/App/AppDeepLinkHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,15 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
|
||||
// With swizzling disabled you must let Messaging know about the message, for Analytics
|
||||
Messaging.messaging().appDidReceiveMessage(userInfo)
|
||||
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 contentIdString = userInfo["content_id"] as? String
|
||||
|
||||
@@ -7,13 +7,31 @@
|
||||
|
||||
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 {
|
||||
static let shared = AppState()
|
||||
|
||||
private var appStepBackStack = [AppStep]()
|
||||
private var routeStepMap: [AppRoute: AppStep] = [:]
|
||||
|
||||
@Published var alreadyUpdatedMarketingInfo = false
|
||||
@Published private(set) var appStep: AppStep = .splash
|
||||
@Published private(set) var rootStep: AppStep = .splash
|
||||
@Published var navigationPath: [AppRoute] = [] {
|
||||
didSet {
|
||||
syncStepWithNavigationPath()
|
||||
}
|
||||
}
|
||||
|
||||
@Published var isShowPlayer = false {
|
||||
didSet {
|
||||
@@ -31,6 +49,10 @@ class AppState: ObservableObject {
|
||||
@Published var pushMessageId = 0
|
||||
@Published var pushAudioContentId = 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 {
|
||||
didSet {
|
||||
if roomId <= 0 {
|
||||
@@ -52,29 +74,113 @@ class AppState: ObservableObject {
|
||||
|
||||
@Published var isShowErrorPopup = false
|
||||
@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) {
|
||||
switch step {
|
||||
case .splash, .main:
|
||||
appStepBackStack.removeAll()
|
||||
|
||||
default:
|
||||
appStepBackStack.append(appStep)
|
||||
}
|
||||
|
||||
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() {
|
||||
if let step = appStepBackStack.popLast() {
|
||||
self.appStep = step
|
||||
} else {
|
||||
self.appStep = .main
|
||||
DispatchQueue.main.async {
|
||||
if self.liveDetailSheet != nil {
|
||||
self.liveDetailSheet = nil
|
||||
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를 새로고침
|
||||
func softRestart() {
|
||||
isRestartApp = true
|
||||
|
||||
@@ -41,6 +41,8 @@ enum AppStep {
|
||||
case privacy
|
||||
|
||||
case notificationSettings
|
||||
|
||||
case notificationReceiveSettings
|
||||
|
||||
case contentViewSettings
|
||||
|
||||
@@ -75,7 +77,9 @@ enum AppStep {
|
||||
case followerList(userId: Int)
|
||||
|
||||
case userProfileDonationAll(userId: Int)
|
||||
|
||||
|
||||
case channelDonationAll(creatorId: Int)
|
||||
|
||||
case userProfileFanTalkAll(userId: Int)
|
||||
|
||||
case createLive(
|
||||
@@ -159,6 +163,8 @@ enum AppStep {
|
||||
case introduceCreatorAll
|
||||
|
||||
case message
|
||||
|
||||
case notificationList
|
||||
|
||||
case pointStatus(refresh: () -> Void)
|
||||
|
||||
|
||||
@@ -84,6 +84,10 @@ struct SodaLiveApp: App {
|
||||
comps.path.lowercased() == "/result" {
|
||||
canPgPaymentViewModel.handleVerifyOpenURL(url)
|
||||
} else {
|
||||
if AppDeepLinkHandler.handle(url: url) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:])
|
||||
ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:])
|
||||
AppsFlyerLib.shared().handleOpen(url)
|
||||
|
||||
@@ -170,29 +170,7 @@ struct AuditionApplicantRecordingView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,23 +130,7 @@ struct AuditionApplyView: View {
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.popup(isPresented: $isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $isShowPopup, message: errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
isShow = true
|
||||
|
||||
@@ -68,21 +68,7 @@ struct AuditionDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getAuditionDetail(auditionId: auditionId) {
|
||||
AppState.shared.back()
|
||||
|
||||
@@ -150,36 +150,8 @@ struct AuditionRoleDetailView: View {
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.onFailure = { AppState.shared.back() }
|
||||
viewModel.auditionRoleId = roleId
|
||||
|
||||
@@ -120,23 +120,7 @@ struct CharacterView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,25 +132,7 @@ struct CharacterDetailView: View {
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarBackButtonHidden()
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.characterId = characterId
|
||||
|
||||
@@ -45,25 +45,7 @@ struct CharacterDetailGalleryView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.characterId = characterId
|
||||
|
||||
@@ -14,104 +14,83 @@ struct NewCharacterListView: View {
|
||||
private let gridSpacing: CGFloat = 12
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 8) {
|
||||
// Toolbar
|
||||
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
||||
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 8) {
|
||||
// Toolbar
|
||||
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 전체 n개
|
||||
HStack(spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
Text(" \(viewModel.totalCount)")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
Text("개")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 전체 n개
|
||||
HStack(spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
Text(" \(viewModel.totalCount)")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
Text("개")
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
// Grid 3열
|
||||
GeometryReader { geo in
|
||||
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
// Grid 3열
|
||||
GeometryReader { geo in
|
||||
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
count: 2
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(viewModel.items.indices, id: \.self) { idx in
|
||||
let item = viewModel.items[idx]
|
||||
|
||||
NavigationLink(value: item.characterId) {
|
||||
CharacterItemView(
|
||||
character: item,
|
||||
size: width,
|
||||
rank: 0,
|
||||
isShowRank: false
|
||||
)
|
||||
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
||||
}
|
||||
}
|
||||
count: 2
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(viewModel.items.indices, id: \.self) { idx in
|
||||
let item = viewModel.items[idx]
|
||||
|
||||
CharacterItemView(
|
||||
character: item,
|
||||
size: width,
|
||||
rank: 0,
|
||||
isShowRank: false
|
||||
)
|
||||
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 16)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.padding(.vertical, 16)
|
||||
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)
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
.padding(.vertical, 12)
|
||||
.onAppear {
|
||||
// 최초 1회만 로드하여 상세 진입 후 복귀 시 스크롤 위치가 유지되도록 함
|
||||
if viewModel.items.isEmpty {
|
||||
viewModel.fetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: Int.self) { characterId in
|
||||
CharacterDetailView(characterId: characterId)
|
||||
}
|
||||
.background(Color.black)
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
import SwiftUI
|
||||
import Bootpay
|
||||
import BootpayUI
|
||||
import PopupView
|
||||
|
||||
struct ChatTabView: View {
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
||||
@@ -75,7 +73,7 @@ struct ChatTabView: View {
|
||||
ZStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// 앱 바
|
||||
HStack(spacing: 24) {
|
||||
HStack(spacing: 20) {
|
||||
Image("img_text_logo")
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -15,91 +15,87 @@ struct OriginalWorkDetailView: View {
|
||||
let originalId: Int
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ZStack(alignment: .top) {
|
||||
if let imageUrl = viewModel.response?.imageUrl {
|
||||
KFImage(URL(string: imageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||
ZStack(alignment: .top) {
|
||||
if let imageUrl = viewModel.response?.imageUrl {
|
||||
KFImage(URL(string: imageUrl))
|
||||
.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()
|
||||
.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()
|
||||
.frame(width: 24, height: 24)
|
||||
.onTapGesture {
|
||||
AppState.shared.back()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.frame(height: 56)
|
||||
.frame(width: 24, height: 24)
|
||||
.onTapGesture {
|
||||
AppState.shared.back()
|
||||
}
|
||||
|
||||
if let response = viewModel.response {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
OriginalWorkDetailHeaderView(item: response)
|
||||
.padding(.horizontal, 24)
|
||||
.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
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.frame(height: 56)
|
||||
|
||||
if let response = viewModel.response {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
OriginalWorkDetailHeaderView(item: response)
|
||||
.padding(.horizontal, 24)
|
||||
.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
|
||||
}
|
||||
}
|
||||
.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)
|
||||
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 {
|
||||
viewModel.originalId = originalId
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: Int.self) { characterId in
|
||||
CharacterDetailView(characterId: characterId)
|
||||
}
|
||||
.onAppear {
|
||||
if viewModel.response == nil {
|
||||
viewModel.originalId = originalId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +125,13 @@ struct OriginalWorkCharacterView: View {
|
||||
ForEach(characters.indices, id: \.self) { idx in
|
||||
let item = characters[idx]
|
||||
|
||||
NavigationLink(value: item.characterId) {
|
||||
CharacterItemView(
|
||||
character: item,
|
||||
size: width,
|
||||
rank: 0,
|
||||
isShowRank: false
|
||||
)
|
||||
}
|
||||
CharacterItemView(
|
||||
character: item,
|
||||
size: width,
|
||||
rank: 0,
|
||||
isShowRank: false
|
||||
)
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
|
||||
@@ -69,23 +69,7 @@ struct OriginalTabView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,23 +308,7 @@ struct ChatRoomView: View {
|
||||
.onDisappear {
|
||||
viewModel.stopTimer()
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,25 +44,7 @@ struct ChatBgSelectionView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.characterId = characterId
|
||||
|
||||
@@ -36,3 +36,99 @@ struct BaseView_Previews: PreviewProvider {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ enum DateParser {
|
||||
{ ISO8601.fractional.date(from: $0) },
|
||||
{ ISO8601.basic.date(from: $0) },
|
||||
{ DF.rfc3339.date(from: $0) },
|
||||
{ DF.isoLocalDateTime.date(from: $0) },
|
||||
{ DF.basic.date(from: $0) }
|
||||
]
|
||||
|
||||
@@ -56,5 +57,13 @@ enum DateParser {
|
||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
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
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import SwiftUI
|
||||
|
||||
struct ContentAllByThemeView: View {
|
||||
@StateObject var viewModel = ContentAllByThemeViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
let themeId: Int
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
DetailNavigationBar(title: viewModel.theme)
|
||||
@@ -111,8 +112,11 @@ struct ContentAllByThemeView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.themeId = themeId
|
||||
viewModel.getContentList()
|
||||
if !isInitialized || viewModel.themeId != themeId {
|
||||
viewModel.themeId = themeId
|
||||
viewModel.getContentList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import SwiftUI
|
||||
struct ContentAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var isFree: Bool = false
|
||||
var isPointAvailableOnly: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
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
|
||||
let item = viewModel.contentList[idx]
|
||||
|
||||
NavigationLink {
|
||||
ContentDetailView(contentId: item.contentId)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ZStack(alignment: .top) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.coverImageUrl),
|
||||
size: CGSize(width: itemSize, height: itemSize)
|
||||
)
|
||||
.cornerRadius(16)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ZStack(alignment: .top) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.coverImageUrl),
|
||||
size: CGSize(width: itemSize, height: itemSize)
|
||||
)
|
||||
.cornerRadius(16)
|
||||
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
if item.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.padding(.top, 6)
|
||||
.padding(.trailing, 6)
|
||||
}
|
||||
if item.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.padding(.top, 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())
|
||||
.onAppear {
|
||||
if idx == viewModel.contentList.count - 1 {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
|
||||
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())
|
||||
.onAppear {
|
||||
if idx == viewModel.contentList.count - 1 {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||
}
|
||||
}
|
||||
.padding(horizontalPadding)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.isFree = isFree
|
||||
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
||||
viewModel.getThemeList()
|
||||
viewModel.fetchData()
|
||||
if !isInitialized || viewModel.isFree != isFree || viewModel.isPointAvailableOnly != isPointAvailableOnly {
|
||||
viewModel.isFree = isFree
|
||||
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
||||
viewModel.getThemeList()
|
||||
viewModel.fetchData()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,95 +14,92 @@ struct ContentNewAllItemView: View {
|
||||
let item: GetAudioContentMainItem
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
ContentDetailView(contentId: item.contentId)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment: .bottom) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: width,
|
||||
height: width
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment: .bottom) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: width,
|
||||
height: width
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: width, height: width, alignment: .top)
|
||||
.cornerRadius(2.7)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: width, height: width, alignment: .top)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if item.price > 0 {
|
||||
Image("ic_card_can_gray")
|
||||
|
||||
Text("\(item.price)")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
} else {
|
||||
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)
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if item.price > 0 {
|
||||
Image("ic_card_can_gray")
|
||||
|
||||
Text("\(item.price)")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
} else {
|
||||
Text("무료")
|
||||
.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)
|
||||
}
|
||||
.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)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ import SwiftUI
|
||||
struct ContentNewAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentNewAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
let isFree: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
|
||||
@@ -82,9 +83,12 @@ struct ContentNewAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.isFree = isFree
|
||||
viewModel.getThemeList()
|
||||
viewModel.getNewContentList()
|
||||
if !isInitialized || viewModel.isFree != isFree {
|
||||
viewModel.isFree = isFree
|
||||
viewModel.getThemeList()
|
||||
viewModel.getNewContentList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
|
||||
@@ -11,9 +11,10 @@ import Kingfisher
|
||||
struct ContentRankingAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentRankingAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "인기 콘텐츠")
|
||||
@@ -44,97 +45,94 @@ struct ContentRankingAllView: View {
|
||||
LazyVStack(spacing: 20) {
|
||||
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
||||
let item = viewModel.contentRankingItemList[index]
|
||||
NavigationLink {
|
||||
ContentDetailView(contentId: item.contentId)
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 66.7,
|
||||
height: 66.7
|
||||
)
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 66.7,
|
||||
height: 66.7
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 66.7, height: 66.7, alignment: .top)
|
||||
.clipped()
|
||||
.cornerRadius(5.3)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Text(item.themeStr)
|
||||
.appFont(size: 8, weight: .medium)
|
||||
.foregroundColor(Color(hex: "3bac6a"))
|
||||
.padding(2.6)
|
||||
.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"))
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 66.7, height: 66.7, alignment: .top)
|
||||
.clipped()
|
||||
.cornerRadius(5.3)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Text(item.themeStr)
|
||||
.appFont(size: 8, weight: .medium)
|
||||
.foregroundColor(Color(hex: "3bac6a"))
|
||||
.padding(2.6)
|
||||
.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)
|
||||
}
|
||||
.frame(height: 66.7)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.contentRankingItemList.count - 1 {
|
||||
viewModel.getContentRanking()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getContentRankingSortType()
|
||||
viewModel.getContentRanking()
|
||||
if !isInitialized {
|
||||
viewModel.getContentRankingSortType()
|
||||
viewModel.getContentRanking()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct ContentBoxView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
NavigationView {
|
||||
Group {
|
||||
VStack(spacing: 13.3) {
|
||||
DetailNavigationBar(title: I18n.ContentBox.title)
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ struct ContentListView: View {
|
||||
|
||||
let userId: Int
|
||||
@StateObject var viewModel = ContentListViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
@@ -128,17 +129,14 @@ struct ContentListView: View {
|
||||
|
||||
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
|
||||
let audioContent = viewModel.audioContentList[index]
|
||||
NavigationLink {
|
||||
ContentDetailView(contentId: audioContent.contentId)
|
||||
} label: {
|
||||
ContentListItemView(item: audioContent)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.audioContentList.count - 1 {
|
||||
viewModel.getAudioContentList()
|
||||
}
|
||||
ContentListItemView(item: audioContent)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.audioContentList.count - 1 {
|
||||
viewModel.getAudioContentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: audioContent.contentId)) }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
@@ -147,25 +145,14 @@ struct ContentListView: View {
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.userId = userId
|
||||
viewModel.getCategoryList()
|
||||
viewModel.getAudioContentList()
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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(hex: "3bb9f1"))
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.cornerRadius(20)
|
||||
.padding(.bottom, 66.7)
|
||||
Spacer()
|
||||
if !isInitialized || viewModel.userId != userId {
|
||||
viewModel.userId = userId
|
||||
viewModel.getCategoryList()
|
||||
viewModel.getAudioContentList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ struct ContentCreateView: View {
|
||||
@StateObject private var viewModel = ContentCreateViewModel()
|
||||
|
||||
@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 isShowSelectThemeView = false
|
||||
@State private var isShowSelectDateView = false
|
||||
@@ -626,11 +630,11 @@ struct ContentCreateView: View {
|
||||
if isShowSelectTimeView {
|
||||
QuarterTimePickerView(selectedTime: $viewModel.releaseTime, isShowing: $isShowSelectTimeView)
|
||||
}
|
||||
|
||||
|
||||
if isShowPhotoPicker {
|
||||
ImagePicker(
|
||||
isShowing: $isShowPhotoPicker,
|
||||
selectedImage: $viewModel.coverImage,
|
||||
selectedImage: $selectedPickedImage,
|
||||
sourceType: .photoLibrary
|
||||
)
|
||||
}
|
||||
@@ -659,25 +663,56 @@ struct ContentCreateView: View {
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
|
||||
if isImageLoading {
|
||||
ZStack {
|
||||
Color.black.opacity(0.45)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { hideKeyboard() }
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onChange(of: selectedPickedImage, perform: { newImage in
|
||||
guard let newImage else {
|
||||
return
|
||||
}
|
||||
|
||||
isImageLoading = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let normalizedImage = newImage.normalizedForCrop()
|
||||
DispatchQueue.main.async {
|
||||
isImageLoading = false
|
||||
selectedPickedImage = nil
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct ContentCurationView: View {
|
||||
|
||||
@StateObject var viewModel = ContentCurationViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
let title: String
|
||||
let curationId: Int
|
||||
@@ -21,7 +22,7 @@ struct ContentCurationView: View {
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: title)
|
||||
@@ -119,8 +120,11 @@ struct ContentCurationView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.curationId = curationId
|
||||
viewModel.getContentList()
|
||||
if !isInitialized || viewModel.curationId != curationId {
|
||||
viewModel.curationId = curationId
|
||||
viewModel.getContentList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ struct AudioContentCommentListView: View {
|
||||
@State private var isShowMemberProfilePopup: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
@@ -188,23 +188,7 @@ struct AudioContentCommentListView: View {
|
||||
viewModel.audioContentId = audioContentId
|
||||
viewModel.getCommentList()
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ struct ContentDetailView: View {
|
||||
@State private var isShowCommentListView = false
|
||||
@State private var isShowFollowNotifyDialog: Bool = false
|
||||
@State private var creatorId: Int = 0
|
||||
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
||||
@State private var isViewVisible: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
@@ -28,11 +30,7 @@ struct ContentDetailView: View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Button {
|
||||
if presentationMode.wrappedValue.isPresented {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} else {
|
||||
AppState.shared.back()
|
||||
}
|
||||
goBack()
|
||||
} label: {
|
||||
Image("ic_back")
|
||||
.resizable()
|
||||
@@ -217,9 +215,26 @@ struct ContentDetailView: View {
|
||||
.navigationTitle("")
|
||||
.navigationBarBackButtonHidden()
|
||||
.onAppear {
|
||||
isViewVisible = true
|
||||
didTriggerAutoBackOnLoadFailure = false
|
||||
viewModel.contentId = contentId
|
||||
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 {
|
||||
VStack(spacing: 0) {
|
||||
@@ -407,31 +422,26 @@ struct ContentDetailView: View {
|
||||
.sheet(
|
||||
isPresented: $isShowCommentListView,
|
||||
content: {
|
||||
AudioContentCommentListView(
|
||||
isPresented: $isShowCommentListView,
|
||||
creatorId: viewModel.audioContent!.creator.creatorId,
|
||||
audioContentId: viewModel.audioContent!.contentId,
|
||||
isShowSecret: viewModel.audioContent!.existOrdered
|
||||
)
|
||||
NavigationStack {
|
||||
AudioContentCommentListView(
|
||||
isPresented: $isShowCommentListView,
|
||||
creatorId: viewModel.audioContent!.creator.creatorId,
|
||||
audioContentId: viewModel.audioContent!.contentId,
|
||||
isShowSecret: viewModel.audioContent!.existOrdered
|
||||
)
|
||||
}
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
}
|
||||
)
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
private func goBack() {
|
||||
if presentationMode.wrappedValue.isPresented {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} else {
|
||||
AppState.shared.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,31 @@ struct LiveRoomDonationDialogView: View {
|
||||
|
||||
@Binding var isShowing: Bool
|
||||
let isAudioContentDonation: Bool
|
||||
let messageLimit: Int
|
||||
let secretLabel: String
|
||||
let secretMinimumCanMessage: String
|
||||
let shouldPrefixSecretInMessagePlaceholder: Bool
|
||||
let onClickDonation: (Int, String, Bool) -> Void
|
||||
|
||||
@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 {
|
||||
ZStack {
|
||||
@@ -92,7 +114,7 @@ struct LiveRoomDonationDialogView: View {
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("비밀미션")
|
||||
Text(secretLabel)
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(isSecret ? Color.button : Color.grayee)
|
||||
}
|
||||
@@ -204,7 +226,10 @@ struct LiveRoomDonationDialogView: View {
|
||||
.stroke(Color.graybb, lineWidth: 1)
|
||||
)
|
||||
|
||||
TextField("함께 보낼 \(isSecret ? "비밀 " : "")메시지 입력(최대 1000자)", text: $donationMessage)
|
||||
TextField(
|
||||
"함께 보낼 \((isSecret && shouldPrefixSecretInMessagePlaceholder) ? "비밀 " : "")메시지 입력(최대 \(messageLimit)자)",
|
||||
text: $donationMessage
|
||||
)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(13.3)
|
||||
@@ -243,7 +268,7 @@ struct LiveRoomDonationDialogView: View {
|
||||
.onTapGesture {
|
||||
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, let can = Int(donationCan) {
|
||||
if isSecret && can < 10 {
|
||||
errorMessage = "비밀 미션은 최소 10캔 이상부터 이용이 가능합니다."
|
||||
errorMessage = secretMinimumCanMessage
|
||||
isShowErrorPopup = true
|
||||
} else if can < 1 {
|
||||
errorMessage = "1캔 이상 후원하실 수 있습니다."
|
||||
@@ -266,28 +291,14 @@ struct LiveRoomDonationDialogView: View {
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(20, corners: [.topLeft, .topRight])
|
||||
}
|
||||
.popup(isPresented: $isShowErrorPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $isShowErrorPopup, message: errorMessage, autohideIn: 1.3)
|
||||
.offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func limitText() {
|
||||
if donationMessage.count > 1000 {
|
||||
donationMessage = String(donationMessage.prefix(1000))
|
||||
if donationMessage.count > messageLimit {
|
||||
donationMessage = String(donationMessage.prefix(messageLimit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import SwiftUI
|
||||
struct ContentMainAlarmAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 알람")
|
||||
@@ -81,7 +82,10 @@ struct ContentMainAlarmAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getContentMainAlarmAll()
|
||||
if !isInitialized {
|
||||
viewModel.getContentMainAlarmAll()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
|
||||
@@ -50,21 +50,7 @@ struct ContentMainTabAlarmView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import SwiftUI
|
||||
struct ContentMainAsmrAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentNewAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 ASMR")
|
||||
@@ -72,7 +73,14 @@ struct ContentMainAsmrAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.selectedTheme = "ASMR"
|
||||
if !isInitialized {
|
||||
if viewModel.selectedTheme != "ASMR" {
|
||||
viewModel.selectedTheme = "ASMR"
|
||||
} else if viewModel.newContentList.isEmpty {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
|
||||
@@ -60,21 +60,7 @@ struct ContentMainTabAsmrView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,21 +84,7 @@ struct ContentMainTabContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ struct ContentMainViewV2: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import SwiftUI
|
||||
struct ContentMainIntroduceCreatorAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 13.3) {
|
||||
DetailNavigationBar(title: "크리에이터 소개")
|
||||
@@ -48,25 +49,14 @@ struct ContentMainIntroduceCreatorAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getIntroduceCreatorList()
|
||||
if !isInitialized {
|
||||
viewModel.getIntroduceCreatorList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,21 +78,7 @@ struct ContentMainTabFreeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,21 +291,7 @@ struct ContentMainTabHomeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import SwiftUI
|
||||
struct ContentMainReplayAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentNewAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
||||
@@ -72,7 +73,14 @@ struct ContentMainReplayAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.selectedTheme = "다시듣기"
|
||||
if !isInitialized {
|
||||
if viewModel.selectedTheme != "다시듣기" {
|
||||
viewModel.selectedTheme = "다시듣기"
|
||||
} else if viewModel.newContentList.isEmpty {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
|
||||
@@ -60,21 +60,7 @@ struct ContentMainTabReplayView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,21 +60,7 @@ struct CompletedSeriesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getCompletedSeries()
|
||||
}
|
||||
|
||||
@@ -86,21 +86,7 @@ struct ContentMainTabSeriesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,23 +277,7 @@ struct ContentModifyView: View {
|
||||
}
|
||||
.onTapGesture { hideKeyboard() }
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.contentId = contentId
|
||||
viewModel.getAudioContentDetail {
|
||||
|
||||
@@ -71,21 +71,7 @@ struct ContentPlaylistListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getPlaylistList()
|
||||
}
|
||||
|
||||
@@ -138,21 +138,7 @@ struct ContentPlaylistCreateView: View {
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
|
||||
if isShowAddContentView {
|
||||
PlaylistAddContentView(
|
||||
|
||||
@@ -238,21 +238,7 @@ struct ContentPlaylistDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.playlistId = playlistId
|
||||
}
|
||||
|
||||
@@ -139,21 +139,7 @@ struct ContentPlaylistModifyView: View {
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.playlistId = playlistId
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ struct SeriesDetailView: View {
|
||||
|
||||
@State private var isShowFollowNotifyDialog: Bool = false
|
||||
@State private var creatorId: Int = 0
|
||||
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
||||
@State private var isViewVisible: Bool = false
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
@@ -38,11 +40,7 @@ struct SeriesDetailView: View {
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture {
|
||||
if presentationMode.wrappedValue.isPresented {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} else {
|
||||
AppState.shared.back()
|
||||
}
|
||||
goBack()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -243,9 +241,35 @@ struct SeriesDetailView: View {
|
||||
.navigationBarBackButtonHidden()
|
||||
}
|
||||
.onAppear {
|
||||
isViewVisible = true
|
||||
didTriggerAutoBackOnLoadFailure = false
|
||||
viewModel.seriesId = seriesId
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct SeriesMainByGenreView: View {
|
||||
|
||||
@StateObject var viewModel = SeriesMainByGenreViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -41,39 +42,27 @@ struct SeriesMainByGenreView: View {
|
||||
) {
|
||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||
let item = viewModel.seriesList[index]
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getSeriesListByGenre()
|
||||
}
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getSeriesListByGenre()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getGenreList()
|
||||
if !isInitialized {
|
||||
viewModel.getGenreList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
|
||||
@@ -75,37 +75,20 @@ struct SeriesMainDayOfWeekView: View {
|
||||
) {
|
||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||
let item = viewModel.seriesList[index]
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
||||
}
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
if !isInitialized {
|
||||
dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())]
|
||||
|
||||
@@ -22,11 +22,8 @@ struct SeriesMainHomeBannerView: View {
|
||||
ForEach(0..<bannerList.count, id: \.self) { index in
|
||||
let item = bannerList[index]
|
||||
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
||||
}
|
||||
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct SeriesMainHomeView: View {
|
||||
|
||||
@StateObject var viewModel = SeriesMainHomeViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -43,11 +44,10 @@ struct SeriesMainHomeView: View {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
|
||||
let item = viewModel.completedSeriesList[$0]
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainItemView(item: item)
|
||||
}
|
||||
SeriesMainItemView(item: item)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
@@ -89,11 +89,10 @@ struct SeriesMainHomeView: View {
|
||||
) {
|
||||
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
|
||||
let item = viewModel.recommendSeriesList[$0]
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
}
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
@@ -101,23 +100,12 @@ struct SeriesMainHomeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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()
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.fetchHome()
|
||||
if !isInitialized {
|
||||
viewModel.fetchHome()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
|
||||
@@ -25,7 +25,7 @@ struct SeriesMainView: View {
|
||||
@State private var selectedTab: InnerTab = .home
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "시리즈 전체보기")
|
||||
|
||||
@@ -9,7 +9,8 @@ import SwiftUI
|
||||
|
||||
struct SeriesListAllView: View {
|
||||
|
||||
@ObservedObject var viewModel = SeriesListAllViewModel()
|
||||
@StateObject var viewModel = SeriesListAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var creatorId: Int? = nil
|
||||
var creatorNickname: String? = nil
|
||||
@@ -18,7 +19,7 @@ struct SeriesListAllView: View {
|
||||
var isCompleted = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
if isCompleted {
|
||||
@@ -48,17 +49,16 @@ struct SeriesListAllView: View {
|
||||
) {
|
||||
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
||||
let item = viewModel.seriesList[index]
|
||||
NavigationLink {
|
||||
SeriesDetailView(seriesId: item.seriesId)
|
||||
} label: {
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if index == viewModel.seriesList.count - 1 {
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(horizontalPadding)
|
||||
@@ -67,10 +67,24 @@ struct SeriesListAllView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.creatorId = creatorId
|
||||
viewModel.isOriginal = isOriginal
|
||||
viewModel.isCompleted = isCompleted
|
||||
viewModel.getSeriesList()
|
||||
let hasFilterChanged =
|
||||
viewModel.creatorId != creatorId ||
|
||||
viewModel.isOriginal != isOriginal ||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,300 +15,332 @@ struct ContentView: View {
|
||||
@State private var message = ""
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
if appState.isRestartApp {
|
||||
EmptyView()
|
||||
} else {
|
||||
HomeView()
|
||||
NavigationStack(path: $appState.navigationPath) {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
if appState.isRestartApp {
|
||||
EmptyView()
|
||||
} else {
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch appState.appStep {
|
||||
case .splash:
|
||||
SplashView()
|
||||
|
||||
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)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
||||
if let msg = $0.object as? String {
|
||||
self.message = msg
|
||||
self.isShowDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
if isShowDialog {
|
||||
SodaDialog(
|
||||
title: I18n.Common.pointGrantTitle,
|
||||
desc: message,
|
||||
confirmButtonTitle: I18n.Common.confirm
|
||||
) {
|
||||
isShowDialog = false
|
||||
message = ""
|
||||
.sodaToast(isPresented: $appState.isShowErrorPopup, message: appState.errorMessage, autohideIn: 1)
|
||||
.navigationDestination(for: AppRoute.self) { route in
|
||||
if let step = appState.appStep(for: route) {
|
||||
AppStepLayerView(step: step, canPgPaymentViewModel: canPgPaymentViewModel)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
} else {
|
||||
EmptyView()
|
||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
||||
if let msg = $0.object as? String {
|
||||
self.message = msg
|
||||
self.isShowDialog = true
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(appState.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppStepLayerView: View {
|
||||
let step: AppStep
|
||||
@ObservedObject var canPgPaymentViewModel: CanPgPaymentViewModel
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
switch step {
|
||||
case .splash:
|
||||
SplashView()
|
||||
|
||||
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 .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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,6 @@ let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.g
|
||||
let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33"
|
||||
|
||||
let LINE_CHANNEL_ID = "2008995582"
|
||||
|
||||
let AGORA_CUSTOMER_ID = "de5dd9ea151f4a43ba1ad8411817b169"
|
||||
let AGORA_CUSTOMER_SECRET = "3855da8bc5ae4743af8bf4f87408b515"
|
||||
|
||||
@@ -121,26 +121,7 @@ struct MemberProfileDialog: View {
|
||||
viewModel.getMemberProfile(memberId: memberId)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
|
||||
if viewModel.isShowUesrBlockConfirm {
|
||||
UserBlockConfirmDialogView(
|
||||
|
||||
@@ -13,12 +13,15 @@ enum ExplorerApi {
|
||||
case getExplorer
|
||||
case searchChannel(channel: String)
|
||||
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
|
||||
case getCreatorDetail(userId: Int)
|
||||
case getFollowerList(userId: Int, page: Int, size: Int)
|
||||
case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
|
||||
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
|
||||
case modifyCheers(request: PutModifyCheersRequest)
|
||||
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 {
|
||||
@@ -39,8 +42,11 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
case .getCreatorProfile(let userId, _):
|
||||
return "/explorer/profile/\(userId)"
|
||||
|
||||
case .getCreatorDetail(let userId):
|
||||
return "/explorer/profile/\(userId)/detail"
|
||||
|
||||
case .getCreatorProfileDonationRanking(let userId, _, _):
|
||||
case .getCreatorProfileDonationRanking(let userId, _, _, _):
|
||||
return "/explorer/profile/\(userId)/donation-rank"
|
||||
|
||||
case .getFollowerList(let userId, _, _):
|
||||
@@ -57,15 +63,18 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
case .writeCreatorNotice:
|
||||
return "/explorer/profile/notice"
|
||||
|
||||
case .getChannelDonationList, .postChannelDonation:
|
||||
return "/explorer/profile/channel-donation"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank:
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank, .getChannelDonationList:
|
||||
return .get
|
||||
|
||||
case .writeCheers, .writeCreatorNotice:
|
||||
|
||||
case .writeCheers, .writeCreatorNotice, .postChannelDonation:
|
||||
return .post
|
||||
|
||||
case .modifyCheers:
|
||||
@@ -75,7 +84,7 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
var task: Task {
|
||||
switch self {
|
||||
case .getExplorer, .getCreatorRank:
|
||||
case .getExplorer, .getCreatorRank, .getCreatorDetail:
|
||||
return .requestPlain
|
||||
|
||||
case .searchChannel(let channel):
|
||||
@@ -111,13 +120,23 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
case .writeCreatorNotice(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .getCreatorProfileDonationRanking(_, let page, let size):
|
||||
let parameters = [
|
||||
|
||||
case .getChannelDonationList(let creatorId):
|
||||
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,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
if let period {
|
||||
parameters["period"] = period.rawValue
|
||||
}
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,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> {
|
||||
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)))
|
||||
}
|
||||
|
||||
func getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getCreatorProfileDonationRanking(userId: userId, page: page, size: size))
|
||||
func getCreatorProfileDonationRanking(
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||