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/
|
.kiro/
|
||||||
.junie/
|
.junie/
|
||||||
docs/
|
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
|
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
|
||||||
|
|||||||
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.
|
- 앱 소스: `SodaLive/Sources/**`
|
||||||
2. Use the present tense in the subject line (e.g., "Add feature" not "Added feature").
|
- 프로젝트/스킴: `SodaLive.xcodeproj`, `SodaLive.xcworkspace`
|
||||||
3. Keep the subject line to 50 characters or less.
|
- 의존성 설정: `Podfile`, `Podfile.lock`
|
||||||
4. Add a blank line between the subject and body.
|
- 운영 스크립트: `work/scripts/**`
|
||||||
5. Keep the body to 72 characters or less per line.
|
- 생성/외부 결과물: `Pods/**`, `generated/**`, `build/**`
|
||||||
6. Within a paragraph, only break lines when the text exceeds 72 characters.
|
- 작업 계획 문서: `docs/**`
|
||||||
7. Describe changes to public API features and do not include implementation details such as package-private code.
|
|
||||||
8. Do not mention test code in commit messages.
|
### 수정 우선순위
|
||||||
9. Do not use any prefix (such as "fix:", "update:", "docs:", "feat:", etc.) in the subject line.
|
- 기능 변경은 `SodaLive/Sources/**`에서 해결한다.
|
||||||
10. Do not start the subject line with a lowercase letter unless the first word is a function name or another identifier that is conventionally lowercase and there is a clear, justifiable reason for the exception. Otherwise, always start with an uppercase letter.
|
- 프로젝트 설정 변경은 필요한 경우에만 수행한다.
|
||||||
11. Do not include tool advertisements, branding, or promotional content in commit messages.
|
- `Pods/**`, `generated/**`는 직접 수정하지 않는다.
|
||||||
12. Use separate git commands to stage files before committing.
|
- `build/**`는 빌드 산출물로 간주하며 수정 대상이 아니다.
|
||||||
13. Always validate commits using `work/scripts/check-commit-message-rules.sh` and fix until validation passes.
|
|
||||||
|
## 빌드/테스트/검증 명령
|
||||||
|
아래 명령은 현재 저장소에서 확인된 공식 진입점이다.
|
||||||
|
|
||||||
|
### 1) 의존성 설치
|
||||||
|
- `pod install`
|
||||||
|
- 근거: `Podfile`에 CocoaPods 타깃(`SodaLive`, `SodaLive-dev`) 정의.
|
||||||
|
|
||||||
|
### 2) 스킴/타깃 확인
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -list`
|
||||||
|
- 근거: 공유 스킴 `SodaLive`, `SodaLive-dev` 존재.
|
||||||
|
|
||||||
|
### 3) 빌드
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
|
||||||
|
### 4) 테스트(전체)
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
|
||||||
|
### 5) 단일 테스트 실행
|
||||||
|
- 일반 형식(테스트 타깃이 있는 경우):
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -only-testing:"SodaLiveTests/TestClass/testMethod" test`
|
||||||
|
- **현재 주의사항**:
|
||||||
|
- `SodaLive.xcodeproj/project.pbxproj` 기준으로 앱 타깃 중심 구성이고 테스트 번들 타깃이 확인되지 않는다.
|
||||||
|
- 따라서 현재 상태에서는 단일 테스트 지정이 실질적으로 동작하지 않을 수 있다.
|
||||||
|
|
||||||
|
### 6) 린트/포맷
|
||||||
|
- 저장소에 공식 `swiftlint`/`swiftformat` 실행 스크립트는 확인되지 않았다.
|
||||||
|
- `generated/*.generated.swift`에 `swiftlint:disable all` 주석은 존재하나, 이는 생성 코드 보호 목적이다.
|
||||||
|
- 린트 도구를 도입/추가하면 본 문서 명령 섹션을 즉시 갱신한다.
|
||||||
|
|
||||||
|
## 코드 스타일 가이드
|
||||||
|
|
||||||
|
### 아키텍처/레이어
|
||||||
|
- 기본 흐름은 `View -> ViewModel -> Repository -> Api(TargetType)`를 따른다.
|
||||||
|
- API는 `enum ...Api: TargetType`, 저장소는 `final class ...Repository` 형태를 우선 사용한다.
|
||||||
|
- 상태 모델은 `struct`/`enum` 중심으로 두고, 화면 상태는 `ObservableObject`에서 관리한다.
|
||||||
|
|
||||||
|
### 임포트 규칙
|
||||||
|
- 시스템 프레임워크(`Foundation`, `SwiftUI`, `Combine`)를 먼저 배치한다.
|
||||||
|
- 서드파티(`Moya`, `CombineMoya`, SDK들)는 이후 배치한다.
|
||||||
|
- import 그룹 사이에는 한 줄 공백으로 의미 단위를 분리한다.
|
||||||
|
|
||||||
|
### 포맷/구조
|
||||||
|
- 들여쓰기는 4칸 스페이스를 사용한다.
|
||||||
|
- 프로퍼티 선언, 비즈니스 로직, 헬퍼 메서드는 공백 줄로 구획한다.
|
||||||
|
- 클로저 체인은 줄바꿈해 가독성을 유지한다.
|
||||||
|
|
||||||
|
### 타입/상태 관리
|
||||||
|
- ViewModel은 `final class ...: ObservableObject` 패턴을 우선한다.
|
||||||
|
- View가 소유하는 객체는 `@StateObject`, 외부 주입 객체는 `@ObservedObject`를 사용한다.
|
||||||
|
- 네트워크 반환은 `AnyPublisher<Response, MoyaError>` 패턴을 기본으로 따른다.
|
||||||
|
|
||||||
|
### 네이밍 규칙
|
||||||
|
- 타입명은 PascalCase (`HomeViewModel`, `UserRepository`, `UserApi`).
|
||||||
|
- 변수/함수는 camelCase (`errorMessage`, `getMemberInfo`).
|
||||||
|
- 역할을 이름에 반영한다 (`*View`, `*ViewModel`, `*Repository`, `*Api`, `*Request`, `*Response`).
|
||||||
|
|
||||||
|
### 비동기/Combine 규칙
|
||||||
|
- 비동기 처리는 Combine의 `sink`, `receiveValue`, `.store(in: &subscription)` 패턴을 따른다.
|
||||||
|
- `sink` 완료 블록에서 `.failure`를 반드시 처리한다.
|
||||||
|
- 클로저 캡처는 상황에 맞게 `[weak self]` 또는 `[unowned self]`를 선택한다.
|
||||||
|
|
||||||
|
### 에러 처리 규칙
|
||||||
|
- 사용자 노출 오류는 `errorMessage`와 팝업 플래그(`isShowPopup`)로 일관되게 처리한다.
|
||||||
|
- JSON 파싱은 `do/catch + JSONDecoder` 패턴을 따른다.
|
||||||
|
- **신규 코드에서 빈 `catch`는 금지**하고, 최소한 로깅 또는 명시적 무시 사유를 남긴다.
|
||||||
|
|
||||||
|
### 로깅 규칙
|
||||||
|
- 디버그 로그는 `DEBUG_LOG`, 오류 로그는 `ERROR_LOG`를 사용한다.
|
||||||
|
- `print`는 임시 디버깅 목적 외 신규 코드에서 지양한다.
|
||||||
|
|
||||||
|
### 네트워크/헤더 규칙
|
||||||
|
- 공통 Moya 플러그인(`AuthPlugin`, `AcceptLanguagePlugin`) 흐름을 유지한다.
|
||||||
|
- 언어 헤더는 `LanguageHeaderProvider.current`를 기준으로 사용한다.
|
||||||
|
|
||||||
|
### 문자열/다국어
|
||||||
|
- 신규 사용자 노출 문자열은 가능하면 `I18n` 경로를 우선 사용한다.
|
||||||
|
- 다국어 기능 수정 시 `Settings/Language` 모듈과 `Accept-Language` 헤더 흐름을 함께 점검한다.
|
||||||
|
|
||||||
|
### 주석/문서화
|
||||||
|
- 자명한 코드에는 주석을 남기지 않는다.
|
||||||
|
- 복잡한 분기, 외부 제약, 부작용이 있는 로직에만 주석을 추가한다.
|
||||||
|
|
||||||
|
## Cursor/Copilot 규칙 반영
|
||||||
|
- 아래 파일 존재 여부를 확인해 AGENTS와 함께 유지한다.
|
||||||
|
- `.cursor/rules/**`
|
||||||
|
- `.cursorrules`
|
||||||
|
- `.github/copilot-instructions.md`
|
||||||
|
- 현재 저장소에서는 위 파일들이 확인되지 않았다.
|
||||||
|
- 추후 파일이 추가되면 AGENTS.md에 요약 규칙을 동기화한다.
|
||||||
|
- 충돌 우선순위 기본값:
|
||||||
|
- 범위가 더 구체적인 규칙이 우선한다(경로 특화 > 저장소 전역).
|
||||||
|
- Cursor: `.cursor/rules/**` > `.cursorrules` > `AGENTS.md`
|
||||||
|
- Copilot: `.github/instructions/**`(존재 시) > `.github/copilot-instructions.md` > `AGENTS.md`
|
||||||
|
|
||||||
|
## 커밋 메시지 규칙 (표준 Conventional Commits)
|
||||||
|
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
|
||||||
|
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
|
||||||
|
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
|
||||||
|
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test`)를 사용한다.
|
||||||
|
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
|
||||||
|
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
|
||||||
|
|
||||||
|
### 커밋 메시지 검증 절차
|
||||||
|
- `git commit` 직후 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
|
||||||
|
- 스크립트 결과가 `[FAIL]`이면 커밋 메시지를 수정한 뒤 다시 검증한다.
|
||||||
|
|
||||||
|
## 작업 절차 체크리스트
|
||||||
|
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
||||||
|
- 변경 중: 공개 API 스키마를 임의 변경하지 말고 작은 단위로 안전하게 수정한다.
|
||||||
|
- 변경 후: 영향 범위 파일에 대해 빌드/테스트/로그/다국어 키를 점검한다.
|
||||||
|
- 커밋 직후: `commit-policy` 스킬을 로드하고 메시지 검증 스크립트를 실행한다.
|
||||||
|
|
||||||
|
## 작업 계획 문서 규칙 (docs)
|
||||||
|
- 모든 작업 시작 전에 `docs` 폴더 아래 계획 문서를 먼저 생성하고, 해당 문서를 기준으로 구현한다.
|
||||||
|
- 계획 문서 파일명은 `[날짜]_구현할내용한글.md` 형식을 사용한다.
|
||||||
|
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||||
|
- 구현 항목은 기능/작업 단위 체크박스(`- [ ]`)로 작성하고 완료 즉시 `- [x]`로 갱신한다.
|
||||||
|
- 작업 도중 범위가 변경되면 계획 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||||
|
- 결과 보고 시 문서 하단에 검증 기록(무엇/왜/어떻게, 실행 명령, 결과)을 한국어로 남긴다.
|
||||||
|
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰기 없이 누적한다.
|
||||||
|
|
||||||
|
## 문서 유지보수 규칙
|
||||||
|
- 불확실한 규칙은 추측으로 채우지 말고 근거 파일 경로를 먼저 확인한다.
|
||||||
|
- 에이전트 안내 문구는 한국어 중심으로 유지한다.
|
||||||
|
- 명령/경로/타깃명이 바뀌면 본 문서를 즉시 업데이트한다.
|
||||||
|
|
||||||
|
## 에이전트 동작 원칙
|
||||||
|
- 추측하지 말고 근거 파일을 읽고 결정한다.
|
||||||
|
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
||||||
|
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
||||||
|
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
|
||||||
|
|
||||||
|
## 설정/보안 유의사항
|
||||||
|
- 토큰/키/개인정보를 코드/로그/문서에 하드코딩하지 않는다.
|
||||||
|
- 인증 관련 헤더/토큰 처리 로직(`AuthPlugin`, `UserDefaultsKey.token`) 수정 시 회귀 위험을 함께 점검한다.
|
||||||
|
- 외부 SDK 키 변경 시 빌드 설정과 런타임 초기화 지점을 함께 검토한다.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "9f35428c4c178ca4a8bfa4b72544585a9e4d5b119825b423e1d2166cbe03fe37",
|
"originHash" : "cf552e0db687218f4a2207a39678af43731c56f6f8ea12b111a15ac39574aa38",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "abseil-cpp-binary",
|
"identity" : "abseil-cpp-binary",
|
||||||
@@ -199,15 +199,6 @@
|
|||||||
"revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811"
|
"revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "popupview",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/exyte/PopupView.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "1b99d6e9872ef91fd57aaef657661b5a00069638",
|
|
||||||
"version" : "1.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "promises",
|
"identity" : "promises",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
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" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "ic_message.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -9,7 +10,6 @@
|
|||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "ic_message.png",
|
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
|||||||
|
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" : {
|
"localizations" : {
|
||||||
@@ -2503,6 +2506,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"고정 해제" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"공개 설정" : {
|
"공개 설정" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2967,6 +2973,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"누적" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"눌러서 잠금해제" : {
|
"눌러서 잠금해제" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4120,22 +4129,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"모든 기기에서 로그아웃" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Log out from all devices"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "全端末からログアウト"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"모집완료" : {
|
"모집완료" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -4167,6 +4160,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"모든 기기에서 로그아웃" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Log out from all devices"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "全端末からログアウト"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"모집중" : {
|
"모집중" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5207,6 +5219,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"서비스 알림" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"설정" : {
|
"설정" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5815,6 +5830,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"알림이 없습니다." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"앱 버전 정보" : {
|
"앱 버전 정보" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -6888,54 +6906,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"인기 캐릭터" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Popular"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "人気キャラ"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"인기 캐릭터 채팅" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Top character"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "人気キャラチャット"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"일" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Sun"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "日"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"이메일" : {
|
"이메일" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -7096,6 +7066,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"인기 캐릭터" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Popular"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "人気キャラ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"인기 캐릭터 채팅" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Top character"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "人気キャラチャット"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"인기 콘텐츠" : {
|
"인기 콘텐츠" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -7160,6 +7162,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"일" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sun"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "日"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"일간 랭킹" : {
|
"일간 랭킹" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -7831,6 +7849,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"주간" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"중복확인" : {
|
"중복확인" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -8391,6 +8412,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"최상단에 고정" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"최신 콘텐츠" : {
|
"최신 콘텐츠" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -8632,22 +8656,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"캔" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Cans"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "CAN"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"캐릭터 정보" : {
|
"캐릭터 정보" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -8664,6 +8672,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"캔" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Cans"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "CAN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"캔 충전" : {
|
"캔 충전" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -9608,7 +9632,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"함께 보낼 %@메시지 입력(최대 %lld자)" : {
|
||||||
|
"localizations" : {
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "함께 보낼 %1$@메시지 입력(최대 %2$lld자)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"함께 보낼 %@메시지 입력(최대 1000자)" : {
|
"함께 보낼 %@메시지 입력(최대 1000자)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -266,6 +266,15 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
|
|||||||
Messaging.messaging().appDidReceiveMessage(userInfo)
|
Messaging.messaging().appDidReceiveMessage(userInfo)
|
||||||
Notifly.userNotificationCenter(center, didReceive: response)
|
Notifly.userNotificationCenter(center, didReceive: response)
|
||||||
|
|
||||||
|
let deepLinkString = (userInfo["deep_link"] as? String ?? "")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if !deepLinkString.isEmpty {
|
||||||
|
_ = AppDeepLinkHandler.handle(urlString: deepLinkString)
|
||||||
|
completionHandler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let roomIdString = userInfo["room_id"] as? String
|
let roomIdString = userInfo["room_id"] as? String
|
||||||
let contentIdString = userInfo["content_id"] as? String
|
let contentIdString = userInfo["content_id"] as? String
|
||||||
let channelIdString = userInfo["channel_id"] as? String
|
let channelIdString = userInfo["channel_id"] as? String
|
||||||
|
|||||||
@@ -7,13 +7,31 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
struct AppRoute: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveDetailSheetState {
|
||||||
|
let roomId: Int
|
||||||
|
let onClickParticipant: () -> Void
|
||||||
|
let onClickReservation: () -> Void
|
||||||
|
let onClickStart: () -> Void
|
||||||
|
let onClickCancel: () -> Void
|
||||||
|
}
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
static let shared = AppState()
|
static let shared = AppState()
|
||||||
|
|
||||||
private var appStepBackStack = [AppStep]()
|
private var routeStepMap: [AppRoute: AppStep] = [:]
|
||||||
|
|
||||||
@Published var alreadyUpdatedMarketingInfo = false
|
@Published var alreadyUpdatedMarketingInfo = false
|
||||||
@Published private(set) var appStep: AppStep = .splash
|
@Published private(set) var appStep: AppStep = .splash
|
||||||
|
@Published private(set) var rootStep: AppStep = .splash
|
||||||
|
@Published var navigationPath: [AppRoute] = [] {
|
||||||
|
didSet {
|
||||||
|
syncStepWithNavigationPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var isShowPlayer = false {
|
@Published var isShowPlayer = false {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -31,6 +49,10 @@ class AppState: ObservableObject {
|
|||||||
@Published var pushMessageId = 0
|
@Published var pushMessageId = 0
|
||||||
@Published var pushAudioContentId = 0
|
@Published var pushAudioContentId = 0
|
||||||
@Published var pushSeriesId = 0
|
@Published var pushSeriesId = 0
|
||||||
|
@Published var pendingDeepLinkAction: AppDeepLinkAction? = nil
|
||||||
|
@Published var pendingCommunityCommentCreatorId = 0
|
||||||
|
@Published var pendingCommunityCommentPostId = 0
|
||||||
|
@Published var isPushRoomFromDeepLink = false
|
||||||
@Published var roomId = 0 {
|
@Published var roomId = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
if roomId <= 0 {
|
if roomId <= 0 {
|
||||||
@@ -52,29 +74,113 @@ class AppState: ObservableObject {
|
|||||||
|
|
||||||
@Published var isShowErrorPopup = false
|
@Published var isShowErrorPopup = false
|
||||||
@Published var errorMessage = ""
|
@Published var errorMessage = ""
|
||||||
|
@Published var liveDetailSheet: LiveDetailSheetState? = nil
|
||||||
|
|
||||||
|
private func syncStepWithNavigationPath() {
|
||||||
|
let validRoutes = Set(navigationPath)
|
||||||
|
routeStepMap = routeStepMap.filter { validRoutes.contains($0.key) }
|
||||||
|
|
||||||
|
if let route = navigationPath.last,
|
||||||
|
let step = routeStepMap[route] {
|
||||||
|
appStep = step
|
||||||
|
} else {
|
||||||
|
appStep = rootStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appStep(for route: AppRoute) -> AppStep? {
|
||||||
|
routeStepMap[route]
|
||||||
|
}
|
||||||
|
|
||||||
func setAppStep(step: AppStep) {
|
func setAppStep(step: AppStep) {
|
||||||
switch step {
|
|
||||||
case .splash, .main:
|
|
||||||
appStepBackStack.removeAll()
|
|
||||||
|
|
||||||
default:
|
|
||||||
appStepBackStack.append(appStep)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.appStep = step
|
switch step {
|
||||||
|
case .splash, .main:
|
||||||
|
self.liveDetailSheet = nil
|
||||||
|
self.rootStep = step
|
||||||
|
self.routeStepMap.removeAll()
|
||||||
|
self.navigationPath.removeAll()
|
||||||
|
self.appStep = step
|
||||||
|
|
||||||
|
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
||||||
|
self.liveDetailSheet = LiveDetailSheetState(
|
||||||
|
roomId: roomId,
|
||||||
|
onClickParticipant: onClickParticipant,
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
self.appStep = step
|
||||||
|
|
||||||
|
default:
|
||||||
|
let route = AppRoute()
|
||||||
|
self.routeStepMap[route] = step
|
||||||
|
self.navigationPath.append(route)
|
||||||
|
self.appStep = step
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func back() {
|
func back() {
|
||||||
if let step = appStepBackStack.popLast() {
|
DispatchQueue.main.async {
|
||||||
self.appStep = step
|
if self.liveDetailSheet != nil {
|
||||||
} else {
|
self.liveDetailSheet = nil
|
||||||
self.appStep = .main
|
self.syncStepWithNavigationPath()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.navigationPath.isEmpty {
|
||||||
|
self.rootStep = .main
|
||||||
|
self.appStep = .main
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.navigationPath.popLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hideLiveDetailSheet() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.liveDetailSheet = nil
|
||||||
|
self.syncStepWithNavigationPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumePendingDeepLinkAction() -> AppDeepLinkAction? {
|
||||||
|
let action = pendingDeepLinkAction
|
||||||
|
pendingDeepLinkAction = nil
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPendingCommunityCommentDeepLink(creatorId: Int, postId: Int) {
|
||||||
|
guard creatorId > 0, postId > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCommunityCommentCreatorId = creatorId
|
||||||
|
pendingCommunityCommentPostId = postId
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumePendingCommunityCommentPostId(creatorId: Int) -> Int? {
|
||||||
|
guard creatorId > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard pendingCommunityCommentCreatorId == creatorId,
|
||||||
|
pendingCommunityCommentPostId > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let postId = pendingCommunityCommentPostId
|
||||||
|
clearPendingCommunityCommentDeepLink()
|
||||||
|
return postId
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearPendingCommunityCommentDeepLink() {
|
||||||
|
pendingCommunityCommentCreatorId = 0
|
||||||
|
pendingCommunityCommentPostId = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 언어 적용 직후 앱을 소프트 재시작(스플래시 -> 메인)하여 전역 UI를 새로고침
|
// 언어 적용 직후 앱을 소프트 재시작(스플래시 -> 메인)하여 전역 UI를 새로고침
|
||||||
func softRestart() {
|
func softRestart() {
|
||||||
isRestartApp = true
|
isRestartApp = true
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ enum AppStep {
|
|||||||
|
|
||||||
case notificationSettings
|
case notificationSettings
|
||||||
|
|
||||||
|
case notificationReceiveSettings
|
||||||
|
|
||||||
case contentViewSettings
|
case contentViewSettings
|
||||||
|
|
||||||
case signOut
|
case signOut
|
||||||
@@ -76,6 +78,8 @@ enum AppStep {
|
|||||||
|
|
||||||
case userProfileDonationAll(userId: Int)
|
case userProfileDonationAll(userId: Int)
|
||||||
|
|
||||||
|
case channelDonationAll(creatorId: Int)
|
||||||
|
|
||||||
case userProfileFanTalkAll(userId: Int)
|
case userProfileFanTalkAll(userId: Int)
|
||||||
|
|
||||||
case createLive(
|
case createLive(
|
||||||
@@ -160,6 +164,8 @@ enum AppStep {
|
|||||||
|
|
||||||
case message
|
case message
|
||||||
|
|
||||||
|
case notificationList
|
||||||
|
|
||||||
case pointStatus(refresh: () -> Void)
|
case pointStatus(refresh: () -> Void)
|
||||||
|
|
||||||
case audition
|
case audition
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ struct SodaLiveApp: App {
|
|||||||
comps.path.lowercased() == "/result" {
|
comps.path.lowercased() == "/result" {
|
||||||
canPgPaymentViewModel.handleVerifyOpenURL(url)
|
canPgPaymentViewModel.handleVerifyOpenURL(url)
|
||||||
} else {
|
} else {
|
||||||
|
if AppDeepLinkHandler.handle(url: url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:])
|
_ = LoginManager.shared.application(UIApplication.shared, open: url, options: [:])
|
||||||
ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:])
|
ApplicationDelegate.shared.application(UIApplication.shared, open: url, options: [:])
|
||||||
AppsFlyerLib.shared().handleOpen(url)
|
AppsFlyerLib.shared().handleOpen(url)
|
||||||
|
|||||||
@@ -170,29 +170,7 @@ struct AuditionApplicantRecordingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(soundManager.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.padding(.horizontal, 6.7)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
if soundManager.onClose {
|
|
||||||
isShowing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,23 +130,7 @@ struct AuditionApplyView: View {
|
|||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
.popup(isPresented: $isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $isShowPopup, message: errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isShow = true
|
isShow = true
|
||||||
|
|||||||
@@ -68,21 +68,7 @@ struct AuditionDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getAuditionDetail(auditionId: auditionId) {
|
viewModel.getAuditionDetail(auditionId: auditionId) {
|
||||||
AppState.shared.back()
|
AppState.shared.back()
|
||||||
|
|||||||
@@ -150,36 +150,8 @@ struct AuditionRoleDetailView: View {
|
|||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
.sodaToast(isPresented: $soundManager.isShowPopup, message: soundManager.errorMessage, autohideIn: 2)
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(soundManager.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.onFailure = { AppState.shared.back() }
|
viewModel.onFailure = { AppState.shared.back() }
|
||||||
viewModel.auditionRoleId = roleId
|
viewModel.auditionRoleId = roleId
|
||||||
|
|||||||
@@ -120,23 +120,7 @@ struct CharacterView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,25 +132,7 @@ struct CharacterDetailView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.navigationBarBackButtonHidden()
|
.navigationBarBackButtonHidden()
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(alignment: .center)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.horizontal, 33.3)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.characterId = characterId
|
viewModel.characterId = characterId
|
||||||
|
|||||||
@@ -45,25 +45,7 @@ struct CharacterDetailGalleryView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(alignment: .center)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.horizontal, 33.3)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.characterId = characterId
|
viewModel.characterId = characterId
|
||||||
|
|||||||
@@ -14,104 +14,83 @@ struct NewCharacterListView: View {
|
|||||||
private let gridSpacing: CGFloat = 12
|
private let gridSpacing: CGFloat = 12
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
VStack(spacing: 8) {
|
||||||
VStack(spacing: 8) {
|
// Toolbar
|
||||||
// Toolbar
|
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
||||||
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
// 전체 n개
|
// 전체 n개
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text("전체")
|
Text("전체")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
Text(" \(viewModel.totalCount)")
|
Text(" \(viewModel.totalCount)")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "ff5c49"))
|
.foregroundColor(Color(hex: "ff5c49"))
|
||||||
Text("개")
|
Text("개")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
// Grid 3열
|
// Grid 3열
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(
|
columns: Array(
|
||||||
repeating: GridItem(
|
repeating: GridItem(
|
||||||
.flexible(),
|
.flexible(),
|
||||||
spacing: gridSpacing,
|
spacing: gridSpacing,
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
),
|
||||||
alignment: .leading,
|
count: 2
|
||||||
spacing: gridSpacing
|
),
|
||||||
) {
|
alignment: .leading,
|
||||||
ForEach(viewModel.items.indices, id: \.self) { idx in
|
spacing: gridSpacing
|
||||||
let item = viewModel.items[idx]
|
) {
|
||||||
|
ForEach(viewModel.items.indices, id: \.self) { idx in
|
||||||
|
let item = viewModel.items[idx]
|
||||||
|
|
||||||
NavigationLink(value: item.characterId) {
|
CharacterItemView(
|
||||||
CharacterItemView(
|
character: item,
|
||||||
character: item,
|
size: width,
|
||||||
size: width,
|
rank: 0,
|
||||||
rank: 0,
|
isShowRank: false
|
||||||
isShowRank: false
|
)
|
||||||
)
|
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
||||||
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
}
|
||||||
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
|
||||||
if viewModel.isLoadingMore {
|
if viewModel.isLoadingMore {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 16)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 0, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.onAppear {
|
|
||||||
// 최초 1회만 로드하여 상세 진입 후 복귀 시 스크롤 위치가 유지되도록 함
|
|
||||||
if viewModel.items.isEmpty {
|
|
||||||
viewModel.fetch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: 0, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
.background(Color.black)
|
.padding(.vertical, 12)
|
||||||
}
|
.onAppear {
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
// 최초 1회만 로드하여 상세 진입 후 복귀 시 스크롤 위치가 유지되도록 함
|
||||||
GeometryReader { geo in
|
if viewModel.items.isEmpty {
|
||||||
HStack {
|
viewModel.fetch()
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationDestination(for: Int.self) { characterId in
|
.background(Color.black)
|
||||||
CharacterDetailView(characterId: characterId)
|
}
|
||||||
}
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
import BootpayUI
|
||||||
import PopupView
|
|
||||||
|
|
||||||
struct ChatTabView: View {
|
struct ChatTabView: View {
|
||||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||||
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
||||||
@@ -75,7 +73,7 @@ struct ChatTabView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// 앱 바
|
// 앱 바
|
||||||
HStack(spacing: 24) {
|
HStack(spacing: 20) {
|
||||||
Image("img_text_logo")
|
Image("img_text_logo")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -15,91 +15,87 @@ struct OriginalWorkDetailView: View {
|
|||||||
let originalId: Int
|
let originalId: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
ZStack(alignment: .top) {
|
||||||
ZStack(alignment: .top) {
|
if let imageUrl = viewModel.response?.imageUrl {
|
||||||
if let imageUrl = viewModel.response?.imageUrl {
|
KFImage(URL(string: imageUrl))
|
||||||
KFImage(URL(string: imageUrl))
|
.cancelOnDisappear(true)
|
||||||
.cancelOnDisappear(true)
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
|
||||||
|
.clipped()
|
||||||
|
.blur(radius: 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
Color.black.opacity(0.5).ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_back")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.frame(width: 24, height: 24)
|
||||||
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
|
.onTapGesture {
|
||||||
.clipped()
|
AppState.shared.back()
|
||||||
.blur(radius: 25)
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.frame(height: 56)
|
||||||
|
|
||||||
Color.black.opacity(0.5).ignoresSafeArea()
|
if let response = viewModel.response {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
OriginalWorkDetailHeaderView(item: response)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.bottom, 24)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
SeriesDetailTabView(
|
||||||
Image("ic_back")
|
title: I18n.Tab.character,
|
||||||
.resizable()
|
width: screenSize().width / 2,
|
||||||
.frame(width: 24, height: 24)
|
isSelected: viewModel.currentTab == .character
|
||||||
.onTapGesture {
|
) {
|
||||||
AppState.shared.back()
|
if viewModel.currentTab != .character {
|
||||||
|
viewModel.currentTab = .character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeriesDetailTabView(
|
||||||
|
title: I18n.Tab.workInfo,
|
||||||
|
width: screenSize().width / 2,
|
||||||
|
isSelected: viewModel.currentTab == .info
|
||||||
|
) {
|
||||||
|
if viewModel.currentTab != .info {
|
||||||
|
viewModel.currentTab = .info
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.background(Color.black)
|
||||||
|
|
||||||
Spacer()
|
Rectangle()
|
||||||
}
|
.foregroundColor(Color.gray90.opacity(0.5))
|
||||||
.padding(.horizontal, 24)
|
.frame(height: 1)
|
||||||
.frame(height: 56)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
if let response = viewModel.response {
|
switch(viewModel.currentTab) {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
case .info:
|
||||||
VStack(spacing: 0) {
|
OriginalWorkInfoView(response: response)
|
||||||
OriginalWorkDetailHeaderView(item: response)
|
default:
|
||||||
.padding(.horizontal, 24)
|
OriginalWorkCharacterView(characters: viewModel.characters)
|
||||||
.padding(.bottom, 24)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
SeriesDetailTabView(
|
|
||||||
title: I18n.Tab.character,
|
|
||||||
width: screenSize().width / 2,
|
|
||||||
isSelected: viewModel.currentTab == .character
|
|
||||||
) {
|
|
||||||
if viewModel.currentTab != .character {
|
|
||||||
viewModel.currentTab = .character
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SeriesDetailTabView(
|
|
||||||
title: I18n.Tab.workInfo,
|
|
||||||
width: screenSize().width / 2,
|
|
||||||
isSelected: viewModel.currentTab == .info
|
|
||||||
) {
|
|
||||||
if viewModel.currentTab != .info {
|
|
||||||
viewModel.currentTab = .info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.black)
|
|
||||||
|
|
||||||
Rectangle()
|
|
||||||
.foregroundColor(Color.gray90.opacity(0.5))
|
|
||||||
.frame(height: 1)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
switch(viewModel.currentTab) {
|
|
||||||
case .info:
|
|
||||||
OriginalWorkInfoView(response: response)
|
|
||||||
default:
|
|
||||||
OriginalWorkCharacterView(characters: viewModel.characters)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
}
|
||||||
if viewModel.response == nil {
|
.onAppear {
|
||||||
viewModel.originalId = originalId
|
if viewModel.response == nil {
|
||||||
}
|
viewModel.originalId = originalId
|
||||||
}
|
|
||||||
.navigationDestination(for: Int.self) { characterId in
|
|
||||||
CharacterDetailView(characterId: characterId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +125,13 @@ struct OriginalWorkCharacterView: View {
|
|||||||
ForEach(characters.indices, id: \.self) { idx in
|
ForEach(characters.indices, id: \.self) { idx in
|
||||||
let item = characters[idx]
|
let item = characters[idx]
|
||||||
|
|
||||||
NavigationLink(value: item.characterId) {
|
CharacterItemView(
|
||||||
CharacterItemView(
|
character: item,
|
||||||
character: item,
|
size: width,
|
||||||
size: width,
|
rank: 0,
|
||||||
rank: 0,
|
isShowRank: false
|
||||||
isShowRank: false
|
)
|
||||||
)
|
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
|||||||
@@ -69,23 +69,7 @@ struct OriginalTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -308,23 +308,7 @@ struct ChatRoomView: View {
|
|||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewModel.stopTimer()
|
viewModel.stopTimer()
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,25 +44,7 @@ struct ChatBgSelectionView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(alignment: .center)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.horizontal, 33.3)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.characterId = characterId
|
viewModel.characterId = characterId
|
||||||
|
|||||||
@@ -36,3 +36,99 @@ struct BaseView_Previews: PreviewProvider {
|
|||||||
BaseView(isLoading: .constant(false)) {}
|
BaseView(isLoading: .constant(false)) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct SodaToastModifier: ViewModifier {
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
let message: String
|
||||||
|
let autohideIn: Double
|
||||||
|
|
||||||
|
private let toastBackgroundColor = Color(
|
||||||
|
red: 59.0 / 255.0,
|
||||||
|
green: 185.0 / 255.0,
|
||||||
|
blue: 241.0 / 255.0,
|
||||||
|
opacity: 0.92
|
||||||
|
)
|
||||||
|
|
||||||
|
@State private var dismissWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.overlay(alignment: .top) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
if isPresented, !message.isEmpty {
|
||||||
|
Text(message)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(2)
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(toastBackgroundColor)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.stroke(Color.white.opacity(0.15), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.padding(.top, geo.safeAreaInsets.top + 8)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.transition(.move(edge: .top).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: isPresented)
|
||||||
|
.onAppear {
|
||||||
|
if isPresented {
|
||||||
|
scheduleDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: isPresented) { newValue in
|
||||||
|
if newValue {
|
||||||
|
scheduleDismiss()
|
||||||
|
} else {
|
||||||
|
cancelDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
cancelDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleDismiss() {
|
||||||
|
cancelDismiss()
|
||||||
|
|
||||||
|
guard autohideIn > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem {
|
||||||
|
withAnimation {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissWorkItem = workItem
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelDismiss() {
|
||||||
|
dismissWorkItem?.cancel()
|
||||||
|
dismissWorkItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func sodaToast(isPresented: Binding<Bool>, message: String, autohideIn: Double = 2) -> some View {
|
||||||
|
modifier(
|
||||||
|
SodaToastModifier(
|
||||||
|
isPresented: isPresented,
|
||||||
|
message: message,
|
||||||
|
autohideIn: autohideIn
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum DateParser {
|
|||||||
{ ISO8601.fractional.date(from: $0) },
|
{ ISO8601.fractional.date(from: $0) },
|
||||||
{ ISO8601.basic.date(from: $0) },
|
{ ISO8601.basic.date(from: $0) },
|
||||||
{ DF.rfc3339.date(from: $0) },
|
{ DF.rfc3339.date(from: $0) },
|
||||||
|
{ DF.isoLocalDateTime.date(from: $0) },
|
||||||
{ DF.basic.date(from: $0) }
|
{ DF.basic.date(from: $0) }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -56,5 +57,13 @@ enum DateParser {
|
|||||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
return f
|
return f
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
static let isoLocalDateTime: DateFormatter = {
|
||||||
|
let f = DateFormatter()
|
||||||
|
f.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
|
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
|
||||||
|
return f
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentAllByThemeView: View {
|
struct ContentAllByThemeView: View {
|
||||||
@StateObject var viewModel = ContentAllByThemeViewModel()
|
@StateObject var viewModel = ContentAllByThemeViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let themeId: Int
|
let themeId: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
DetailNavigationBar(title: viewModel.theme)
|
DetailNavigationBar(title: viewModel.theme)
|
||||||
@@ -111,8 +112,11 @@ struct ContentAllByThemeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.themeId = themeId
|
if !isInitialized || viewModel.themeId != themeId {
|
||||||
viewModel.getContentList()
|
viewModel.themeId = themeId
|
||||||
|
viewModel.getContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import SwiftUI
|
|||||||
struct ContentAllView: View {
|
struct ContentAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentAllViewModel()
|
@StateObject var viewModel = ContentAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var isFree: Bool = false
|
var isFree: Bool = false
|
||||||
var isPointAvailableOnly: Bool = false
|
var isPointAvailableOnly: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
|
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
|
||||||
@@ -78,63 +79,63 @@ struct ContentAllView: View {
|
|||||||
ForEach(viewModel.contentList.indices, id: \.self) { idx in
|
ForEach(viewModel.contentList.indices, id: \.self) { idx in
|
||||||
let item = viewModel.contentList[idx]
|
let item = viewModel.contentList[idx]
|
||||||
|
|
||||||
NavigationLink {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
ZStack(alignment: .top) {
|
||||||
} label: {
|
DownsampledKFImage(
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
url: URL(string: item.coverImageUrl),
|
||||||
ZStack(alignment: .top) {
|
size: CGSize(width: itemSize, height: itemSize)
|
||||||
DownsampledKFImage(
|
)
|
||||||
url: URL(string: item.coverImageUrl),
|
.cornerRadius(16)
|
||||||
size: CGSize(width: itemSize, height: itemSize)
|
|
||||||
)
|
|
||||||
.cornerRadius(16)
|
|
||||||
|
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if item.isPointAvailable {
|
if item.isPointAvailable {
|
||||||
Image("ic_point")
|
Image("ic_point")
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
.padding(.trailing, 6)
|
.padding(.trailing, 6)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 18, weight: .regular)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.top, 8)
|
|
||||||
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 14, weight: .regular)
|
|
||||||
.foregroundColor(Color(hex: "78909C"))
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
}
|
||||||
.frame(width: itemSize)
|
|
||||||
.contentShape(Rectangle())
|
Text(item.title)
|
||||||
.onAppear {
|
.appFont(size: 18, weight: .regular)
|
||||||
if idx == viewModel.contentList.count - 1 {
|
.foregroundColor(.white)
|
||||||
viewModel.fetchData()
|
.multilineTextAlignment(.leading)
|
||||||
}
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(Color(hex: "78909C"))
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.frame(width: itemSize)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onAppear {
|
||||||
|
if idx == viewModel.contentList.count - 1 {
|
||||||
|
viewModel.fetchData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontalPadding)
|
.padding(horizontalPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.isFree = isFree
|
if !isInitialized || viewModel.isFree != isFree || viewModel.isPointAvailableOnly != isPointAvailableOnly {
|
||||||
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
viewModel.isFree = isFree
|
||||||
viewModel.getThemeList()
|
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
||||||
viewModel.fetchData()
|
viewModel.getThemeList()
|
||||||
|
viewModel.fetchData()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,95 +14,92 @@ struct ContentNewAllItemView: View {
|
|||||||
let item: GetAudioContentMainItem
|
let item: GetAudioContentMainItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
ZStack(alignment: .bottom) {
|
||||||
} label: {
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
.cancelOnDisappear(true)
|
||||||
ZStack(alignment: .bottom) {
|
.downsampling(
|
||||||
KFImage(URL(string: item.coverImageUrl))
|
size: CGSize(
|
||||||
.cancelOnDisappear(true)
|
width: width,
|
||||||
.downsampling(
|
height: width
|
||||||
size: CGSize(
|
|
||||||
width: width,
|
|
||||||
height: width
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resizable()
|
)
|
||||||
.scaledToFill()
|
.resizable()
|
||||||
.frame(width: width, height: width, alignment: .top)
|
.scaledToFill()
|
||||||
.cornerRadius(2.7)
|
.frame(width: width, height: width, alignment: .top)
|
||||||
|
.cornerRadius(2.7)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
if item.price > 0 {
|
if item.price > 0 {
|
||||||
Image("ic_card_can_gray")
|
Image("ic_card_can_gray")
|
||||||
|
|
||||||
Text("\(item.price)")
|
Text("\(item.price)")
|
||||||
.appFont(size: 8.5, weight: .medium)
|
.appFont(size: 8.5, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
} else {
|
} else {
|
||||||
Text("무료")
|
Text("무료")
|
||||||
.appFont(size: 8.5, weight: .medium)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(3)
|
|
||||||
.background(Color(hex: "333333").opacity(0.7))
|
|
||||||
.cornerRadius(10)
|
|
||||||
.padding(.leading, 2.7)
|
|
||||||
.padding(.bottom, 2.7)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text(item.duration)
|
|
||||||
.appFont(size: 8.5, weight: .medium)
|
.appFont(size: 8.5, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
}
|
}
|
||||||
.padding(3)
|
|
||||||
.background(Color(hex: "333333").opacity(0.7))
|
|
||||||
.cornerRadius(10)
|
|
||||||
.padding(.trailing, 2.7)
|
|
||||||
.padding(.bottom, 2.7)
|
|
||||||
}
|
}
|
||||||
|
.padding(3)
|
||||||
|
.background(Color(hex: "333333").opacity(0.7))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.leading, 2.7)
|
||||||
|
.padding(.bottom, 2.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Text(item.duration)
|
||||||
|
.appFont(size: 8.5, weight: .medium)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
}
|
||||||
|
.padding(3)
|
||||||
|
.background(Color(hex: "333333").opacity(0.7))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.trailing, 2.7)
|
||||||
|
.padding(.bottom, 2.7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: width, height: width)
|
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "d2d2d2"))
|
|
||||||
.frame(width: width, alignment: .leading)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
HStack(spacing: 5.3) {
|
|
||||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
|
||||||
.cancelOnDisappear(true)
|
|
||||||
.downsampling(
|
|
||||||
size: CGSize(
|
|
||||||
width: 21.3,
|
|
||||||
height: 21.3
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 21.3, height: 21.3)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
}
|
||||||
.frame(width: width)
|
.frame(width: width, height: width)
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.appFont(size: 13.3, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
.frame(width: width, alignment: .leading)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
HStack(spacing: 5.3) {
|
||||||
|
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(
|
||||||
|
size: CGSize(
|
||||||
|
width: 21.3,
|
||||||
|
height: 21.3
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 21.3, height: 21.3)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
|
.frame(width: width)
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import SwiftUI
|
|||||||
struct ContentNewAllView: View {
|
struct ContentNewAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let isFree: Bool
|
let isFree: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
|
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
|
||||||
@@ -82,9 +83,12 @@ struct ContentNewAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.isFree = isFree
|
if !isInitialized || viewModel.isFree != isFree {
|
||||||
viewModel.getThemeList()
|
viewModel.isFree = isFree
|
||||||
viewModel.getNewContentList()
|
viewModel.getThemeList()
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import Kingfisher
|
|||||||
struct ContentRankingAllView: View {
|
struct ContentRankingAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentRankingAllViewModel()
|
@StateObject var viewModel = ContentRankingAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: "인기 콘텐츠")
|
DetailNavigationBar(title: "인기 콘텐츠")
|
||||||
@@ -44,97 +45,94 @@ struct ContentRankingAllView: View {
|
|||||||
LazyVStack(spacing: 20) {
|
LazyVStack(spacing: 20) {
|
||||||
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
||||||
let item = viewModel.contentRankingItemList[index]
|
let item = viewModel.contentRankingItemList[index]
|
||||||
NavigationLink {
|
HStack(spacing: 0) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
} label: {
|
.cancelOnDisappear(true)
|
||||||
HStack(spacing: 0) {
|
.downsampling(
|
||||||
KFImage(URL(string: item.coverImageUrl))
|
size: CGSize(
|
||||||
.cancelOnDisappear(true)
|
width: 66.7,
|
||||||
.downsampling(
|
height: 66.7
|
||||||
size: CGSize(
|
|
||||||
width: 66.7,
|
|
||||||
height: 66.7
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resizable()
|
)
|
||||||
.scaledToFill()
|
.resizable()
|
||||||
.frame(width: 66.7, height: 66.7, alignment: .top)
|
.scaledToFill()
|
||||||
.clipped()
|
.frame(width: 66.7, height: 66.7, alignment: .top)
|
||||||
.cornerRadius(5.3)
|
.clipped()
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
Text("\(index + 1)")
|
Text("\(index + 1)")
|
||||||
.appFont(size: 16.7, weight: .bold)
|
.appFont(size: 16.7, weight: .bold)
|
||||||
.foregroundColor(Color(hex: "3bb9f1"))
|
.foregroundColor(Color(hex: "3bb9f1"))
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text(item.themeStr)
|
Text(item.themeStr)
|
||||||
.appFont(size: 8, weight: .medium)
|
.appFont(size: 8, weight: .medium)
|
||||||
.foregroundColor(Color(hex: "3bac6a"))
|
.foregroundColor(Color(hex: "3bac6a"))
|
||||||
.padding(2.6)
|
.padding(2.6)
|
||||||
.background(Color(hex: "28312b"))
|
.background(Color(hex: "28312b"))
|
||||||
.cornerRadius(2.6)
|
|
||||||
|
|
||||||
Text(item.duration)
|
|
||||||
.appFont(size: 8, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.padding(2.6)
|
|
||||||
.background(Color(hex: "222222"))
|
|
||||||
.cornerRadius(2.6)
|
|
||||||
|
|
||||||
if item.isPointAvailable {
|
|
||||||
Text("포인트")
|
|
||||||
.appFont(size: 8, weight: .medium)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.padding(2.6)
|
|
||||||
.background(Color(hex: "7849bc"))
|
|
||||||
.cornerRadius(2.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 10.7, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "d2d2d2"))
|
|
||||||
.lineLimit(2)
|
|
||||||
.padding(.top, 2.7)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if item.price > 0 {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image("ic_can")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 17, height: 17)
|
|
||||||
|
|
||||||
Text("\(item.price)")
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "909090"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("무료")
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "ffffff"))
|
|
||||||
.padding(.horizontal, 5.3)
|
|
||||||
.padding(.vertical, 2.7)
|
|
||||||
.background(Color(hex: "cf5c37"))
|
|
||||||
.cornerRadius(2.6)
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
Text(item.duration)
|
||||||
|
.appFont(size: 8, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
if item.isPointAvailable {
|
||||||
|
Text("포인트")
|
||||||
|
.appFont(size: 8, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "7849bc"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 10.7, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.padding(.top, 2.7)
|
||||||
}
|
}
|
||||||
.frame(height: 66.7)
|
|
||||||
.contentShape(Rectangle())
|
Spacer()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.contentRankingItemList.count - 1 {
|
if item.price > 0 {
|
||||||
viewModel.getContentRanking()
|
HStack(spacing: 8) {
|
||||||
|
Image("ic_can")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 17, height: 17)
|
||||||
|
|
||||||
|
Text("\(item.price)")
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "909090"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Text("무료")
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
.padding(.horizontal, 5.3)
|
||||||
|
.padding(.vertical, 2.7)
|
||||||
|
.background(Color(hex: "cf5c37"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: 66.7)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.contentRankingItemList.count - 1 {
|
||||||
|
viewModel.getContentRanking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,28 +143,13 @@ struct ContentRankingAllView: View {
|
|||||||
LoadingView()
|
LoadingView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.padding(.horizontal, 6.7)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color(hex: "9970ff"))
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getContentRankingSortType()
|
if !isInitialized {
|
||||||
viewModel.getContentRanking()
|
viewModel.getContentRankingSortType()
|
||||||
|
viewModel.getContentRanking()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct ContentBoxView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
NavigationView {
|
Group {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
DetailNavigationBar(title: I18n.ContentBox.title)
|
DetailNavigationBar(title: I18n.ContentBox.title)
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ struct ContentListView: View {
|
|||||||
|
|
||||||
let userId: Int
|
let userId: Int
|
||||||
@StateObject var viewModel = ContentListViewModel()
|
@StateObject var viewModel = ContentListViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@@ -128,17 +129,14 @@ struct ContentListView: View {
|
|||||||
|
|
||||||
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
|
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
|
||||||
let audioContent = viewModel.audioContentList[index]
|
let audioContent = viewModel.audioContentList[index]
|
||||||
NavigationLink {
|
ContentListItemView(item: audioContent)
|
||||||
ContentDetailView(contentId: audioContent.contentId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
ContentListItemView(item: audioContent)
|
if index == viewModel.audioContentList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getAudioContentList()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.audioContentList.count - 1 {
|
|
||||||
viewModel.getAudioContentList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: audioContent.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
@@ -147,25 +145,14 @@ struct ContentListView: View {
|
|||||||
.padding(.top, 13.3)
|
.padding(.top, 13.3)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.userId = userId
|
if !isInitialized || viewModel.userId != userId {
|
||||||
viewModel.getCategoryList()
|
viewModel.userId = userId
|
||||||
viewModel.getAudioContentList()
|
viewModel.getCategoryList()
|
||||||
}
|
viewModel.getAudioContentList()
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
isInitialized = true
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color(hex: "3bb9f1"))
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ struct ContentCreateView: View {
|
|||||||
@StateObject private var viewModel = ContentCreateViewModel()
|
@StateObject private var viewModel = ContentCreateViewModel()
|
||||||
|
|
||||||
@State private var isShowPhotoPicker = false
|
@State private var isShowPhotoPicker = false
|
||||||
|
@State private var selectedPickedImage: UIImage?
|
||||||
|
@State private var cropSourceImage: UIImage?
|
||||||
|
@State private var isShowImageCropper = false
|
||||||
|
@State private var isImageLoading = false
|
||||||
@State private var isShowSelectAudioView = false
|
@State private var isShowSelectAudioView = false
|
||||||
@State private var isShowSelectThemeView = false
|
@State private var isShowSelectThemeView = false
|
||||||
@State private var isShowSelectDateView = false
|
@State private var isShowSelectDateView = false
|
||||||
@@ -630,7 +634,7 @@ struct ContentCreateView: View {
|
|||||||
if isShowPhotoPicker {
|
if isShowPhotoPicker {
|
||||||
ImagePicker(
|
ImagePicker(
|
||||||
isShowing: $isShowPhotoPicker,
|
isShowing: $isShowPhotoPicker,
|
||||||
selectedImage: $viewModel.coverImage,
|
selectedImage: $selectedPickedImage,
|
||||||
sourceType: .photoLibrary
|
sourceType: .photoLibrary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -659,25 +663,56 @@ struct ContentCreateView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
|
||||||
|
if isImageLoading {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.45)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onTapGesture { hideKeyboard() }
|
.onTapGesture { hideKeyboard() }
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
.onChange(of: selectedPickedImage, perform: { newImage in
|
||||||
HStack {
|
guard let newImage else {
|
||||||
Spacer()
|
return
|
||||||
Text(viewModel.errorMessage)
|
}
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
isImageLoading = true
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
.foregroundColor(Color.white)
|
let normalizedImage = newImage.normalizedForCrop()
|
||||||
.multilineTextAlignment(.center)
|
DispatchQueue.main.async {
|
||||||
.cornerRadius(20)
|
isImageLoading = false
|
||||||
.padding(.top, 66.7)
|
selectedPickedImage = nil
|
||||||
Spacer()
|
cropSourceImage = normalizedImage
|
||||||
|
isShowImageCropper = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.onDisappear {
|
||||||
|
isImageLoading = false
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isShowImageCropper, onDismiss: {
|
||||||
|
cropSourceImage = nil
|
||||||
|
}) {
|
||||||
|
if let cropSourceImage {
|
||||||
|
ImageCropEditorView(
|
||||||
|
image: cropSourceImage,
|
||||||
|
aspectPolicy: .square,
|
||||||
|
onCancel: {
|
||||||
|
isShowImageCropper = false
|
||||||
|
},
|
||||||
|
onComplete: { croppedImage in
|
||||||
|
viewModel.coverImage = croppedImage
|
||||||
|
isShowImageCropper = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct ContentCurationView: View {
|
struct ContentCurationView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentCurationViewModel()
|
@StateObject var viewModel = ContentCurationViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let curationId: Int
|
let curationId: Int
|
||||||
@@ -21,7 +22,7 @@ struct ContentCurationView: View {
|
|||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: title)
|
DetailNavigationBar(title: title)
|
||||||
@@ -119,8 +120,11 @@ struct ContentCurationView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.curationId = curationId
|
if !isInitialized || viewModel.curationId != curationId {
|
||||||
viewModel.getContentList()
|
viewModel.curationId = curationId
|
||||||
|
viewModel.getContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct AudioContentCommentListView: View {
|
|||||||
@State private var isShowMemberProfilePopup: Bool = false
|
@State private var isShowMemberProfilePopup: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@@ -188,23 +188,7 @@ struct AudioContentCommentListView: View {
|
|||||||
viewModel.audioContentId = audioContentId
|
viewModel.audioContentId = audioContentId
|
||||||
viewModel.getCommentList()
|
viewModel.getCommentList()
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color(hex: "9970ff"))
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ struct ContentDetailView: View {
|
|||||||
@State private var isShowCommentListView = false
|
@State private var isShowCommentListView = false
|
||||||
@State private var isShowFollowNotifyDialog: Bool = false
|
@State private var isShowFollowNotifyDialog: Bool = false
|
||||||
@State private var creatorId: Int = 0
|
@State private var creatorId: Int = 0
|
||||||
|
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
||||||
|
@State private var isViewVisible: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
@@ -28,11 +30,7 @@ struct ContentDetailView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Button {
|
Button {
|
||||||
if presentationMode.wrappedValue.isPresented {
|
goBack()
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
} else {
|
|
||||||
AppState.shared.back()
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Image("ic_back")
|
Image("ic_back")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -217,9 +215,26 @@ struct ContentDetailView: View {
|
|||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.navigationBarBackButtonHidden()
|
.navigationBarBackButtonHidden()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
isViewVisible = true
|
||||||
|
didTriggerAutoBackOnLoadFailure = false
|
||||||
viewModel.contentId = contentId
|
viewModel.contentId = contentId
|
||||||
AppState.shared.pushAudioContentId = 0
|
AppState.shared.pushAudioContentId = 0
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
isViewVisible = false
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.isShowPopup) { isShowing in
|
||||||
|
guard isShowing else { return }
|
||||||
|
guard viewModel.audioContent == nil else { return }
|
||||||
|
guard !didTriggerAutoBackOnLoadFailure else { return }
|
||||||
|
|
||||||
|
didTriggerAutoBackOnLoadFailure = true
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
guard isViewVisible else { return }
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let audioContent = viewModel.audioContent, isShowOrderView {
|
if let audioContent = viewModel.audioContent, isShowOrderView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -407,31 +422,26 @@ struct ContentDetailView: View {
|
|||||||
.sheet(
|
.sheet(
|
||||||
isPresented: $isShowCommentListView,
|
isPresented: $isShowCommentListView,
|
||||||
content: {
|
content: {
|
||||||
AudioContentCommentListView(
|
NavigationStack {
|
||||||
isPresented: $isShowCommentListView,
|
AudioContentCommentListView(
|
||||||
creatorId: viewModel.audioContent!.creator.creatorId,
|
isPresented: $isShowCommentListView,
|
||||||
audioContentId: viewModel.audioContent!.contentId,
|
creatorId: viewModel.audioContent!.creator.creatorId,
|
||||||
isShowSecret: viewModel.audioContent!.existOrdered
|
audioContentId: viewModel.audioContent!.contentId,
|
||||||
)
|
isShowSecret: viewModel.audioContent!.existOrdered
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
}
|
||||||
HStack {
|
}
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
private func goBack() {
|
||||||
.padding(.vertical, 13.3)
|
if presentationMode.wrappedValue.isPresented {
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
presentationMode.wrappedValue.dismiss()
|
||||||
.appFont(size: 12, weight: .medium)
|
} else {
|
||||||
.background(Color(hex: "9970ff"))
|
AppState.shared.back()
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,32 @@ struct LiveRoomDonationDialogView: View {
|
|||||||
|
|
||||||
@Binding var isShowing: Bool
|
@Binding var isShowing: Bool
|
||||||
let isAudioContentDonation: Bool
|
let isAudioContentDonation: Bool
|
||||||
|
let messageLimit: Int
|
||||||
|
let secretLabel: String
|
||||||
|
let secretMinimumCanMessage: String
|
||||||
|
let shouldPrefixSecretInMessagePlaceholder: Bool
|
||||||
let onClickDonation: (Int, String, Bool) -> Void
|
let onClickDonation: (Int, String, Bool) -> Void
|
||||||
|
|
||||||
@StateObject var keyboardHandler = KeyboardHandler()
|
@StateObject var keyboardHandler = KeyboardHandler()
|
||||||
|
|
||||||
|
init(
|
||||||
|
isShowing: Binding<Bool>,
|
||||||
|
isAudioContentDonation: Bool,
|
||||||
|
messageLimit: Int = 1000,
|
||||||
|
secretLabel: String = I18n.LiveRoom.secretMissionLabel,
|
||||||
|
secretMinimumCanMessage: String = I18n.LiveRoom.secretMissionMinimumCanMessage,
|
||||||
|
shouldPrefixSecretInMessagePlaceholder: Bool = true,
|
||||||
|
onClickDonation: @escaping (Int, String, Bool) -> Void
|
||||||
|
) {
|
||||||
|
self._isShowing = isShowing
|
||||||
|
self.isAudioContentDonation = isAudioContentDonation
|
||||||
|
self.messageLimit = messageLimit
|
||||||
|
self.secretLabel = secretLabel
|
||||||
|
self.secretMinimumCanMessage = secretMinimumCanMessage
|
||||||
|
self.shouldPrefixSecretInMessagePlaceholder = shouldPrefixSecretInMessagePlaceholder
|
||||||
|
self.onClickDonation = onClickDonation
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black
|
Color.black
|
||||||
@@ -92,7 +114,7 @@ struct LiveRoomDonationDialogView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
Text("비밀미션")
|
Text(secretLabel)
|
||||||
.appFont(size: 14.7, weight: .medium)
|
.appFont(size: 14.7, weight: .medium)
|
||||||
.foregroundColor(isSecret ? Color.button : Color.grayee)
|
.foregroundColor(isSecret ? Color.button : Color.grayee)
|
||||||
}
|
}
|
||||||
@@ -204,7 +226,10 @@ struct LiveRoomDonationDialogView: View {
|
|||||||
.stroke(Color.graybb, lineWidth: 1)
|
.stroke(Color.graybb, lineWidth: 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
TextField("함께 보낼 \(isSecret ? "비밀 " : "")메시지 입력(최대 1000자)", text: $donationMessage)
|
TextField(
|
||||||
|
"함께 보낼 \((isSecret && shouldPrefixSecretInMessagePlaceholder) ? "비밀 " : "")메시지 입력(최대 \(messageLimit)자)",
|
||||||
|
text: $donationMessage
|
||||||
|
)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
.padding(13.3)
|
.padding(13.3)
|
||||||
@@ -243,7 +268,7 @@ struct LiveRoomDonationDialogView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, let can = Int(donationCan) {
|
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, let can = Int(donationCan) {
|
||||||
if isSecret && can < 10 {
|
if isSecret && can < 10 {
|
||||||
errorMessage = "비밀 미션은 최소 10캔 이상부터 이용이 가능합니다."
|
errorMessage = secretMinimumCanMessage
|
||||||
isShowErrorPopup = true
|
isShowErrorPopup = true
|
||||||
} else if can < 1 {
|
} else if can < 1 {
|
||||||
errorMessage = "1캔 이상 후원하실 수 있습니다."
|
errorMessage = "1캔 이상 후원하실 수 있습니다."
|
||||||
@@ -266,28 +291,14 @@ struct LiveRoomDonationDialogView: View {
|
|||||||
.background(Color.gray22)
|
.background(Color.gray22)
|
||||||
.cornerRadius(20, corners: [.topLeft, .topRight])
|
.cornerRadius(20, corners: [.topLeft, .topRight])
|
||||||
}
|
}
|
||||||
.popup(isPresented: $isShowErrorPopup, type: .toast, position: .bottom, autohideIn: 1.3) {
|
.sodaToast(isPresented: $isShowErrorPopup, message: errorMessage, autohideIn: 1.3)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight)
|
.offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func limitText() {
|
func limitText() {
|
||||||
if donationMessage.count > 1000 {
|
if donationMessage.count > messageLimit {
|
||||||
donationMessage = String(donationMessage.prefix(1000))
|
donationMessage = String(donationMessage.prefix(messageLimit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainAlarmAllView: View {
|
struct ContentMainAlarmAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 알람")
|
DetailNavigationBar(title: "새로운 알람")
|
||||||
@@ -81,7 +82,10 @@ struct ContentMainAlarmAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getContentMainAlarmAll()
|
if !isInitialized {
|
||||||
|
viewModel.getContentMainAlarmAll()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -50,21 +50,7 @@ struct ContentMainTabAlarmView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainAsmrAllView: View {
|
struct ContentMainAsmrAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 ASMR")
|
DetailNavigationBar(title: "새로운 ASMR")
|
||||||
@@ -72,7 +73,14 @@ struct ContentMainAsmrAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.selectedTheme = "ASMR"
|
if !isInitialized {
|
||||||
|
if viewModel.selectedTheme != "ASMR" {
|
||||||
|
viewModel.selectedTheme = "ASMR"
|
||||||
|
} else if viewModel.newContentList.isEmpty {
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
}
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -60,21 +60,7 @@ struct ContentMainTabAsmrView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,21 +84,7 @@ struct ContentMainTabContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ struct ContentMainViewV2: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainIntroduceCreatorAllView: View {
|
struct ContentMainIntroduceCreatorAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "크리에이터 소개")
|
DetailNavigationBar(title: "크리에이터 소개")
|
||||||
@@ -48,25 +49,14 @@ struct ContentMainIntroduceCreatorAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getIntroduceCreatorList()
|
if !isInitialized {
|
||||||
|
viewModel.getIntroduceCreatorList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,21 +78,7 @@ struct ContentMainTabFreeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,21 +291,7 @@ struct ContentMainTabHomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainReplayAllView: View {
|
struct ContentMainReplayAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
||||||
@@ -72,7 +73,14 @@ struct ContentMainReplayAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.selectedTheme = "다시듣기"
|
if !isInitialized {
|
||||||
|
if viewModel.selectedTheme != "다시듣기" {
|
||||||
|
viewModel.selectedTheme = "다시듣기"
|
||||||
|
} else if viewModel.newContentList.isEmpty {
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
}
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -60,21 +60,7 @@ struct ContentMainTabReplayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,21 +60,7 @@ struct CompletedSeriesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getCompletedSeries()
|
viewModel.getCompletedSeries()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,21 +86,7 @@ struct ContentMainTabSeriesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,23 +277,7 @@ struct ContentModifyView: View {
|
|||||||
}
|
}
|
||||||
.onTapGesture { hideKeyboard() }
|
.onTapGesture { hideKeyboard() }
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
GeometryReader { geo in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.contentId = contentId
|
viewModel.contentId = contentId
|
||||||
viewModel.getAudioContentDetail {
|
viewModel.getAudioContentDetail {
|
||||||
|
|||||||
@@ -71,21 +71,7 @@ struct ContentPlaylistListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getPlaylistList()
|
viewModel.getPlaylistList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,21 +138,7 @@ struct ContentPlaylistCreateView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 13.3)
|
.padding(.vertical, 13.3)
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isShowAddContentView {
|
if isShowAddContentView {
|
||||||
PlaylistAddContentView(
|
PlaylistAddContentView(
|
||||||
|
|||||||
@@ -238,21 +238,7 @@ struct ContentPlaylistDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.playlistId = playlistId
|
viewModel.playlistId = playlistId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,21 +139,7 @@ struct ContentPlaylistModifyView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 13.3)
|
.padding(.vertical, 13.3)
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.playlistId = playlistId
|
viewModel.playlistId = playlistId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct SeriesDetailView: View {
|
|||||||
|
|
||||||
@State private var isShowFollowNotifyDialog: Bool = false
|
@State private var isShowFollowNotifyDialog: Bool = false
|
||||||
@State private var creatorId: Int = 0
|
@State private var creatorId: Int = 0
|
||||||
|
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
||||||
|
@State private var isViewVisible: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
@@ -38,11 +40,7 @@ struct SeriesDetailView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if presentationMode.wrappedValue.isPresented {
|
goBack()
|
||||||
presentationMode.wrappedValue.dismiss()
|
|
||||||
} else {
|
|
||||||
AppState.shared.back()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -243,9 +241,35 @@ struct SeriesDetailView: View {
|
|||||||
.navigationBarBackButtonHidden()
|
.navigationBarBackButtonHidden()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
isViewVisible = true
|
||||||
|
didTriggerAutoBackOnLoadFailure = false
|
||||||
viewModel.seriesId = seriesId
|
viewModel.seriesId = seriesId
|
||||||
viewModel.getSeriesDetail()
|
viewModel.getSeriesDetail()
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
isViewVisible = false
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.isShowPopup) { isShowing in
|
||||||
|
guard isShowing else { return }
|
||||||
|
guard viewModel.seriesDetail == nil else { return }
|
||||||
|
guard !didTriggerAutoBackOnLoadFailure else { return }
|
||||||
|
|
||||||
|
didTriggerAutoBackOnLoadFailure = true
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
guard isViewVisible else { return }
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func goBack() {
|
||||||
|
if presentationMode.wrappedValue.isPresented {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
} else {
|
||||||
|
AppState.shared.back()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct SeriesMainByGenreView: View {
|
struct SeriesMainByGenreView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = SeriesMainByGenreViewModel()
|
@StateObject var viewModel = SeriesMainByGenreViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -41,39 +42,27 @@ struct SeriesMainByGenreView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getSeriesListByGenre()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getSeriesListByGenre()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getGenreList()
|
if !isInitialized {
|
||||||
|
viewModel.getGenreList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
|
|||||||
@@ -75,37 +75,20 @@ struct SeriesMainDayOfWeekView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if !isInitialized {
|
if !isInitialized {
|
||||||
dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())]
|
dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())]
|
||||||
|
|||||||
@@ -22,11 +22,8 @@ struct SeriesMainHomeBannerView: View {
|
|||||||
ForEach(0..<bannerList.count, id: \.self) { index in
|
ForEach(0..<bannerList.count, id: \.self) { index in
|
||||||
let item = bannerList[index]
|
let item = bannerList[index]
|
||||||
|
|
||||||
NavigationLink {
|
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||||
} label: {
|
|
||||||
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct SeriesMainHomeView: View {
|
struct SeriesMainHomeView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = SeriesMainHomeViewModel()
|
@StateObject var viewModel = SeriesMainHomeViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -43,11 +44,10 @@ struct SeriesMainHomeView: View {
|
|||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: 16) {
|
||||||
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
|
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
|
||||||
let item = viewModel.completedSeriesList[$0]
|
let item = viewModel.completedSeriesList[$0]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture {
|
||||||
} label: {
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
SeriesMainItemView(item: item)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
@@ -89,11 +89,10 @@ struct SeriesMainHomeView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
|
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
|
||||||
let item = viewModel.recommendSeriesList[$0]
|
let item = viewModel.recommendSeriesList[$0]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture {
|
||||||
} label: {
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
@@ -101,23 +100,12 @@ struct SeriesMainHomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.fetchHome()
|
if !isInitialized {
|
||||||
|
viewModel.fetchHome()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct SeriesMainView: View {
|
|||||||
@State private var selectedTab: InnerTab = .home
|
@State private var selectedTab: InnerTab = .home
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView {
|
BaseView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: "시리즈 전체보기")
|
DetailNavigationBar(title: "시리즈 전체보기")
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SeriesListAllView: View {
|
struct SeriesListAllView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = SeriesListAllViewModel()
|
@StateObject var viewModel = SeriesListAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var creatorId: Int? = nil
|
var creatorId: Int? = nil
|
||||||
var creatorNickname: String? = nil
|
var creatorNickname: String? = nil
|
||||||
@@ -18,7 +19,7 @@ struct SeriesListAllView: View {
|
|||||||
var isCompleted = false
|
var isCompleted = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if isCompleted {
|
if isCompleted {
|
||||||
@@ -48,17 +49,16 @@ struct SeriesListAllView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getSeriesList()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getSeriesList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontalPadding)
|
.padding(horizontalPadding)
|
||||||
@@ -67,10 +67,24 @@ struct SeriesListAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.creatorId = creatorId
|
let hasFilterChanged =
|
||||||
viewModel.isOriginal = isOriginal
|
viewModel.creatorId != creatorId ||
|
||||||
viewModel.isCompleted = isCompleted
|
viewModel.isOriginal != isOriginal ||
|
||||||
viewModel.getSeriesList()
|
viewModel.isCompleted != isCompleted
|
||||||
|
|
||||||
|
if !isInitialized || hasFilterChanged {
|
||||||
|
if hasFilterChanged {
|
||||||
|
viewModel.page = 1
|
||||||
|
viewModel.isLast = false
|
||||||
|
viewModel.seriesList.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.creatorId = creatorId
|
||||||
|
viewModel.isOriginal = isOriginal
|
||||||
|
viewModel.isCompleted = isCompleted
|
||||||
|
viewModel.getSeriesList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,300 +15,332 @@ struct ContentView: View {
|
|||||||
@State private var message = ""
|
@State private var message = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
NavigationStack(path: $appState.navigationPath) {
|
||||||
Color.black.ignoresSafeArea()
|
ZStack {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
if appState.isRestartApp {
|
if appState.isRestartApp {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
HomeView()
|
HomeView()
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .splash = appState.rootStep {
|
||||||
|
AppStepLayerView(step: .splash, canPgPaymentViewModel: canPgPaymentViewModel)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let liveDetailSheet = appState.liveDetailSheet {
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: liveDetailSheet.roomId,
|
||||||
|
onClickParticipant: liveDetailSheet.onClickParticipant,
|
||||||
|
onClickReservation: liveDetailSheet.onClickReservation,
|
||||||
|
onClickStart: liveDetailSheet.onClickStart,
|
||||||
|
onClickCancel: liveDetailSheet.onClickCancel,
|
||||||
|
onClickClose: {
|
||||||
|
withAnimation {
|
||||||
|
appState.hideLiveDetailSheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowDialog {
|
||||||
|
SodaDialog(
|
||||||
|
title: I18n.Common.pointGrantTitle,
|
||||||
|
desc: message,
|
||||||
|
confirmButtonTitle: I18n.Common.confirm
|
||||||
|
) {
|
||||||
|
isShowDialog = false
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
||||||
switch appState.appStep {
|
if let msg = $0.object as? String {
|
||||||
case .splash:
|
self.message = msg
|
||||||
SplashView()
|
self.isShowDialog = true
|
||||||
|
}
|
||||||
case .login:
|
|
||||||
LoginView()
|
|
||||||
|
|
||||||
case .signUp:
|
|
||||||
SignUpView()
|
|
||||||
|
|
||||||
case .findPassword:
|
|
||||||
FindPasswordView()
|
|
||||||
|
|
||||||
case .textMessageDetail(let messageItem, let messageBox, let refresh):
|
|
||||||
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
|
|
||||||
|
|
||||||
case .writeTextMessage(let userId, let nickname):
|
|
||||||
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
|
|
||||||
|
|
||||||
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
|
|
||||||
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
|
|
||||||
|
|
||||||
case .settings:
|
|
||||||
SettingsView()
|
|
||||||
|
|
||||||
case .languageSettings:
|
|
||||||
LanguageSettingsView()
|
|
||||||
|
|
||||||
case .notices:
|
|
||||||
NoticeListView()
|
|
||||||
|
|
||||||
case .noticeDetail(let notice):
|
|
||||||
NoticeDetailView(notice: notice)
|
|
||||||
|
|
||||||
case .events:
|
|
||||||
EventListView()
|
|
||||||
|
|
||||||
case .eventDetail(let event):
|
|
||||||
EventDetailView(event: event)
|
|
||||||
|
|
||||||
case .terms:
|
|
||||||
TermsView(isPrivacyPolicy: false)
|
|
||||||
|
|
||||||
case .privacy:
|
|
||||||
TermsView(isPrivacyPolicy: true)
|
|
||||||
|
|
||||||
case .notificationSettings:
|
|
||||||
NotificationSettingsView()
|
|
||||||
|
|
||||||
case .contentViewSettings:
|
|
||||||
ContentSettingsView()
|
|
||||||
|
|
||||||
case .signOut:
|
|
||||||
SignOutView()
|
|
||||||
|
|
||||||
case .canStatus(let refresh):
|
|
||||||
CanStatusView(refresh: refresh)
|
|
||||||
|
|
||||||
case .canCharge(let refresh, let afterCompletionToGoBack):
|
|
||||||
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
|
|
||||||
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
|
|
||||||
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
|
|
||||||
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
|
|
||||||
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
.environmentObject(canPgPaymentViewModel)
|
|
||||||
|
|
||||||
case .liveReservation:
|
|
||||||
LiveReservationStatusView()
|
|
||||||
|
|
||||||
case .liveReservationCancel(let reservationId):
|
|
||||||
LiveReservationCancelView(reservationId: reservationId)
|
|
||||||
|
|
||||||
case .serviceCenter:
|
|
||||||
ServiceCenterView()
|
|
||||||
|
|
||||||
case .createContent:
|
|
||||||
ContentCreateView()
|
|
||||||
|
|
||||||
case .liveReservationComplete(let response):
|
|
||||||
LiveReservationCompleteView(reservationCompleteData: response)
|
|
||||||
|
|
||||||
case .creatorDetail(let userId):
|
|
||||||
UserProfileView(userId: userId)
|
|
||||||
|
|
||||||
case .followerList(let userId):
|
|
||||||
FollowerListView(userId: userId)
|
|
||||||
|
|
||||||
case .modifyContent(let contentId):
|
|
||||||
ContentModifyView(contentId: contentId)
|
|
||||||
|
|
||||||
case .contentListAll(let userId):
|
|
||||||
ContentListView(userId: userId)
|
|
||||||
|
|
||||||
case .contentDetail(let contentId):
|
|
||||||
ContentDetailView(contentId: contentId)
|
|
||||||
|
|
||||||
case .createLive(let timeSettingMode, let onSuccess):
|
|
||||||
LiveRoomCreateView(
|
|
||||||
timeSettingMode: timeSettingMode,
|
|
||||||
onSuccess: onSuccess
|
|
||||||
)
|
|
||||||
|
|
||||||
case .liveNowAll(let onClickParticipant):
|
|
||||||
LiveNowAllView(onClickParticipant: onClickParticipant)
|
|
||||||
|
|
||||||
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
|
|
||||||
LiveReservationAllView(
|
|
||||||
onClickReservation: onClickReservation,
|
|
||||||
onClickStart: onClickStart,
|
|
||||||
onClickCancel: onClickCancel,
|
|
||||||
onTapCreateLive: onTapCreateLive
|
|
||||||
)
|
|
||||||
|
|
||||||
case .modifyLive(let room):
|
|
||||||
LiveRoomEditView(room: room)
|
|
||||||
|
|
||||||
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
|
||||||
LiveDetailView(
|
|
||||||
roomId: roomId,
|
|
||||||
onClickParticipant: onClickParticipant,
|
|
||||||
onClickReservation: onClickReservation,
|
|
||||||
onClickStart: onClickStart,
|
|
||||||
onClickCancel: onClickCancel
|
|
||||||
)
|
|
||||||
|
|
||||||
case .modifyPassword:
|
|
||||||
ModifyPasswordView()
|
|
||||||
|
|
||||||
case .changeNickname:
|
|
||||||
NicknameUpdateView()
|
|
||||||
|
|
||||||
case .profileUpdate(let refresh):
|
|
||||||
ProfileUpdateView(refresh: refresh)
|
|
||||||
|
|
||||||
case .followingList:
|
|
||||||
FollowCreatorView()
|
|
||||||
|
|
||||||
case .orderListAll:
|
|
||||||
OrderListAllView()
|
|
||||||
|
|
||||||
case .userProfileDonationAll(let userId):
|
|
||||||
UserProfileDonationAllView(userId: userId)
|
|
||||||
|
|
||||||
case .userProfileFanTalkAll(let userId):
|
|
||||||
UserProfileFanTalkAllView(userId: userId)
|
|
||||||
|
|
||||||
case .newContentAll(let isFree):
|
|
||||||
ContentNewAllView(isFree: isFree)
|
|
||||||
|
|
||||||
case .curationAll(let title, let curationId):
|
|
||||||
ContentCurationView(title: title, curationId: curationId)
|
|
||||||
|
|
||||||
case .contentRankingAll:
|
|
||||||
ContentRankingAllView()
|
|
||||||
|
|
||||||
case .creatorCommunityAll(let creatorId):
|
|
||||||
CreatorCommunityAllView(creatorId: creatorId)
|
|
||||||
|
|
||||||
case .creatorCommunityWrite(let onSuccess):
|
|
||||||
CreatorCommunityWriteView(onSuccess: onSuccess)
|
|
||||||
|
|
||||||
case .creatorCommunityModify(let postId, let onSuccess):
|
|
||||||
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
|
|
||||||
|
|
||||||
case .canCoupon(let refresh):
|
|
||||||
CanCouponView(refresh: refresh)
|
|
||||||
|
|
||||||
case .contentAllByTheme(let themeId):
|
|
||||||
ContentAllByThemeView(themeId: themeId)
|
|
||||||
|
|
||||||
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
|
|
||||||
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
|
|
||||||
|
|
||||||
case .seriesDetail(let seriesId):
|
|
||||||
SeriesDetailView(seriesId: seriesId)
|
|
||||||
|
|
||||||
case .seriesContentAll(let seriesId, let seriesTitle):
|
|
||||||
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
|
|
||||||
|
|
||||||
case .tempCanPayment(let orderType, let contentId, let title, let can):
|
|
||||||
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
|
|
||||||
|
|
||||||
case .blockList:
|
|
||||||
BlockMemberListView()
|
|
||||||
|
|
||||||
case .myBox(let currentTab):
|
|
||||||
ContentBoxView(initCurrentTab: currentTab)
|
|
||||||
|
|
||||||
case .auditionDetail(let auditionId):
|
|
||||||
AuditionDetailView(auditionId: auditionId)
|
|
||||||
|
|
||||||
case .auditionRoleDetail(let roleId, let auditionTitle):
|
|
||||||
AuditionRoleDetailView(
|
|
||||||
roleId: roleId,
|
|
||||||
auditionTitle: auditionTitle
|
|
||||||
)
|
|
||||||
|
|
||||||
case .search:
|
|
||||||
SearchView()
|
|
||||||
|
|
||||||
case .contentMain(let startTab):
|
|
||||||
ContentMainViewV2(selectedTab: startTab)
|
|
||||||
|
|
||||||
case .completedSeriesAll:
|
|
||||||
CompletedSeriesView()
|
|
||||||
|
|
||||||
case .newAlarmContentAll:
|
|
||||||
ContentMainAlarmAllView()
|
|
||||||
|
|
||||||
case .newAsmrContentAll:
|
|
||||||
ContentMainAsmrAllView()
|
|
||||||
|
|
||||||
case .newReplayContentAll:
|
|
||||||
ContentMainReplayAllView()
|
|
||||||
|
|
||||||
case .introduceCreatorAll:
|
|
||||||
ContentMainIntroduceCreatorAllView()
|
|
||||||
|
|
||||||
case .message:
|
|
||||||
MessageView()
|
|
||||||
|
|
||||||
case .pointStatus(let refresh):
|
|
||||||
PointStatusView(refresh: refresh)
|
|
||||||
|
|
||||||
case .audition:
|
|
||||||
AuditionView()
|
|
||||||
|
|
||||||
case .characterDetail(let characterId):
|
|
||||||
CharacterDetailView(characterId: characterId)
|
|
||||||
|
|
||||||
case .chatRoom(let id):
|
|
||||||
ChatRoomView(roomId: id)
|
|
||||||
|
|
||||||
case .newCharacterAll:
|
|
||||||
NewCharacterListView()
|
|
||||||
|
|
||||||
case .originalWorkDetail(let originalId):
|
|
||||||
OriginalWorkDetailView(originalId: originalId)
|
|
||||||
|
|
||||||
case .contentAll(let isFree, let isPointOnly):
|
|
||||||
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
|
|
||||||
|
|
||||||
case .seriesMain:
|
|
||||||
SeriesMainView()
|
|
||||||
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
|
||||||
}
|
}
|
||||||
|
.sodaToast(isPresented: $appState.isShowErrorPopup, message: appState.errorMessage, autohideIn: 1)
|
||||||
if isShowDialog {
|
.navigationDestination(for: AppRoute.self) { route in
|
||||||
SodaDialog(
|
if let step = appState.appStep(for: route) {
|
||||||
title: I18n.Common.pointGrantTitle,
|
AppStepLayerView(step: step, canPgPaymentViewModel: canPgPaymentViewModel)
|
||||||
desc: message,
|
.navigationBarBackButtonHidden(true)
|
||||||
confirmButtonTitle: I18n.Common.confirm
|
} else {
|
||||||
) {
|
EmptyView()
|
||||||
isShowDialog = false
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
message = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
}
|
||||||
if let msg = $0.object as? String {
|
}
|
||||||
self.message = msg
|
|
||||||
self.isShowDialog = true
|
struct AppStepLayerView: View {
|
||||||
}
|
let step: AppStep
|
||||||
}
|
@ObservedObject var canPgPaymentViewModel: CanPgPaymentViewModel
|
||||||
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
|
|
||||||
GeometryReader { geo in
|
@ViewBuilder
|
||||||
HStack {
|
var body: some View {
|
||||||
Spacer()
|
switch step {
|
||||||
Text(appState.errorMessage)
|
case .splash:
|
||||||
.padding(.vertical, 13.3)
|
SplashView()
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
case .login:
|
||||||
.background(Color.button)
|
LoginView()
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
case .signUp:
|
||||||
.cornerRadius(20)
|
SignUpView()
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
case .findPassword:
|
||||||
}
|
FindPasswordView()
|
||||||
}
|
|
||||||
|
case .textMessageDetail(let messageItem, let messageBox, let refresh):
|
||||||
|
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
|
||||||
|
|
||||||
|
case .writeTextMessage(let userId, let nickname):
|
||||||
|
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
|
||||||
|
|
||||||
|
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
|
||||||
|
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
|
||||||
|
|
||||||
|
case .settings:
|
||||||
|
SettingsView()
|
||||||
|
|
||||||
|
case .languageSettings:
|
||||||
|
LanguageSettingsView()
|
||||||
|
|
||||||
|
case .notices:
|
||||||
|
NoticeListView()
|
||||||
|
|
||||||
|
case .noticeDetail(let notice):
|
||||||
|
NoticeDetailView(notice: notice)
|
||||||
|
|
||||||
|
case .events:
|
||||||
|
EventListView()
|
||||||
|
|
||||||
|
case .eventDetail(let event):
|
||||||
|
EventDetailView(event: event)
|
||||||
|
|
||||||
|
case .terms:
|
||||||
|
TermsView(isPrivacyPolicy: false)
|
||||||
|
|
||||||
|
case .privacy:
|
||||||
|
TermsView(isPrivacyPolicy: true)
|
||||||
|
|
||||||
|
case .notificationSettings:
|
||||||
|
NotificationSettingsView()
|
||||||
|
|
||||||
|
case .notificationReceiveSettings:
|
||||||
|
NotificationReceiveSettingsView()
|
||||||
|
|
||||||
|
case .contentViewSettings:
|
||||||
|
ContentSettingsView()
|
||||||
|
|
||||||
|
case .signOut:
|
||||||
|
SignOutView()
|
||||||
|
|
||||||
|
case .canStatus(let refresh):
|
||||||
|
CanStatusView(refresh: refresh)
|
||||||
|
|
||||||
|
case .canCharge(let refresh, let afterCompletionToGoBack):
|
||||||
|
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
|
||||||
|
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
|
||||||
|
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
|
||||||
|
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
|
||||||
|
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
.environmentObject(canPgPaymentViewModel)
|
||||||
|
|
||||||
|
case .liveReservation:
|
||||||
|
LiveReservationStatusView()
|
||||||
|
|
||||||
|
case .liveReservationCancel(let reservationId):
|
||||||
|
LiveReservationCancelView(reservationId: reservationId)
|
||||||
|
|
||||||
|
case .serviceCenter:
|
||||||
|
ServiceCenterView()
|
||||||
|
|
||||||
|
case .createContent:
|
||||||
|
ContentCreateView()
|
||||||
|
|
||||||
|
case .liveReservationComplete(let response):
|
||||||
|
LiveReservationCompleteView(reservationCompleteData: response)
|
||||||
|
|
||||||
|
case .creatorDetail(let userId):
|
||||||
|
UserProfileView(userId: userId)
|
||||||
|
|
||||||
|
case .followerList(let userId):
|
||||||
|
FollowerListView(userId: userId)
|
||||||
|
|
||||||
|
case .modifyContent(let contentId):
|
||||||
|
ContentModifyView(contentId: contentId)
|
||||||
|
|
||||||
|
case .contentListAll(let userId):
|
||||||
|
ContentListView(userId: userId)
|
||||||
|
|
||||||
|
case .contentDetail(let contentId):
|
||||||
|
ContentDetailView(contentId: contentId)
|
||||||
|
|
||||||
|
case .createLive(let timeSettingMode, let onSuccess):
|
||||||
|
LiveRoomCreateView(
|
||||||
|
timeSettingMode: timeSettingMode,
|
||||||
|
onSuccess: onSuccess
|
||||||
|
)
|
||||||
|
|
||||||
|
case .liveNowAll(let onClickParticipant):
|
||||||
|
LiveNowAllView(onClickParticipant: onClickParticipant)
|
||||||
|
|
||||||
|
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
|
||||||
|
LiveReservationAllView(
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel,
|
||||||
|
onTapCreateLive: onTapCreateLive
|
||||||
|
)
|
||||||
|
|
||||||
|
case .modifyLive(let room):
|
||||||
|
LiveRoomEditView(room: room)
|
||||||
|
|
||||||
|
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: roomId,
|
||||||
|
onClickParticipant: onClickParticipant,
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
case .modifyPassword:
|
||||||
|
ModifyPasswordView()
|
||||||
|
|
||||||
|
case .changeNickname:
|
||||||
|
NicknameUpdateView()
|
||||||
|
|
||||||
|
case .profileUpdate(let refresh):
|
||||||
|
ProfileUpdateView(refresh: refresh)
|
||||||
|
|
||||||
|
case .followingList:
|
||||||
|
FollowCreatorView()
|
||||||
|
|
||||||
|
case .orderListAll:
|
||||||
|
OrderListAllView()
|
||||||
|
|
||||||
|
case .userProfileDonationAll(let userId):
|
||||||
|
UserProfileDonationAllView(userId: userId)
|
||||||
|
|
||||||
|
case .channelDonationAll(let creatorId):
|
||||||
|
ChannelDonationAllView(creatorId: creatorId)
|
||||||
|
|
||||||
|
case .userProfileFanTalkAll(let userId):
|
||||||
|
UserProfileFanTalkAllView(userId: userId)
|
||||||
|
|
||||||
|
case .newContentAll(let isFree):
|
||||||
|
ContentNewAllView(isFree: isFree)
|
||||||
|
|
||||||
|
case .curationAll(let title, let curationId):
|
||||||
|
ContentCurationView(title: title, curationId: curationId)
|
||||||
|
|
||||||
|
case .contentRankingAll:
|
||||||
|
ContentRankingAllView()
|
||||||
|
|
||||||
|
case .creatorCommunityAll(let creatorId):
|
||||||
|
CreatorCommunityAllView(creatorId: creatorId)
|
||||||
|
|
||||||
|
case .creatorCommunityWrite(let onSuccess):
|
||||||
|
CreatorCommunityWriteView(onSuccess: onSuccess)
|
||||||
|
|
||||||
|
case .creatorCommunityModify(let postId, let onSuccess):
|
||||||
|
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
|
||||||
|
|
||||||
|
case .canCoupon(let refresh):
|
||||||
|
CanCouponView(refresh: refresh)
|
||||||
|
|
||||||
|
case .contentAllByTheme(let themeId):
|
||||||
|
ContentAllByThemeView(themeId: themeId)
|
||||||
|
|
||||||
|
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
|
||||||
|
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
|
||||||
|
|
||||||
|
case .seriesDetail(let seriesId):
|
||||||
|
SeriesDetailView(seriesId: seriesId)
|
||||||
|
|
||||||
|
case .seriesContentAll(let seriesId, let seriesTitle):
|
||||||
|
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
|
||||||
|
|
||||||
|
case .tempCanPayment(let orderType, let contentId, let title, let can):
|
||||||
|
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
|
||||||
|
|
||||||
|
case .blockList:
|
||||||
|
BlockMemberListView()
|
||||||
|
|
||||||
|
case .myBox(let currentTab):
|
||||||
|
ContentBoxView(initCurrentTab: currentTab)
|
||||||
|
|
||||||
|
case .auditionDetail(let auditionId):
|
||||||
|
AuditionDetailView(auditionId: auditionId)
|
||||||
|
|
||||||
|
case .auditionRoleDetail(let roleId, let auditionTitle):
|
||||||
|
AuditionRoleDetailView(
|
||||||
|
roleId: roleId,
|
||||||
|
auditionTitle: auditionTitle
|
||||||
|
)
|
||||||
|
|
||||||
|
case .search:
|
||||||
|
SearchView()
|
||||||
|
|
||||||
|
case .contentMain(let startTab):
|
||||||
|
ContentMainViewV2(selectedTab: startTab)
|
||||||
|
|
||||||
|
case .completedSeriesAll:
|
||||||
|
CompletedSeriesView()
|
||||||
|
|
||||||
|
case .newAlarmContentAll:
|
||||||
|
ContentMainAlarmAllView()
|
||||||
|
|
||||||
|
case .newAsmrContentAll:
|
||||||
|
ContentMainAsmrAllView()
|
||||||
|
|
||||||
|
case .newReplayContentAll:
|
||||||
|
ContentMainReplayAllView()
|
||||||
|
|
||||||
|
case .introduceCreatorAll:
|
||||||
|
ContentMainIntroduceCreatorAllView()
|
||||||
|
|
||||||
|
case .message:
|
||||||
|
MessageView()
|
||||||
|
|
||||||
|
case .notificationList:
|
||||||
|
PushNotificationListView()
|
||||||
|
|
||||||
|
case .pointStatus(let refresh):
|
||||||
|
PointStatusView(refresh: refresh)
|
||||||
|
|
||||||
|
case .audition:
|
||||||
|
AuditionView()
|
||||||
|
|
||||||
|
case .characterDetail(let characterId):
|
||||||
|
CharacterDetailView(characterId: characterId)
|
||||||
|
|
||||||
|
case .chatRoom(let id):
|
||||||
|
ChatRoomView(roomId: id)
|
||||||
|
|
||||||
|
case .newCharacterAll:
|
||||||
|
NewCharacterListView()
|
||||||
|
|
||||||
|
case .originalWorkDetail(let originalId):
|
||||||
|
OriginalWorkDetailView(originalId: originalId)
|
||||||
|
|
||||||
|
case .contentAll(let isFree, let isPointOnly):
|
||||||
|
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
|
||||||
|
|
||||||
|
case .seriesMain:
|
||||||
|
SeriesMainView()
|
||||||
|
|
||||||
|
case .main:
|
||||||
|
EmptyView()
|
||||||
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,6 @@ let GID_SERVER_CLIENT_ID = "758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.g
|
|||||||
let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33"
|
let KAKAO_APP_KEY = "20cf19413d63bfdfd30e8e6dff933d33"
|
||||||
|
|
||||||
let LINE_CHANNEL_ID = "2008995582"
|
let LINE_CHANNEL_ID = "2008995582"
|
||||||
|
|
||||||
|
let AGORA_CUSTOMER_ID = "de5dd9ea151f4a43ba1ad8411817b169"
|
||||||
|
let AGORA_CUSTOMER_SECRET = "3855da8bc5ae4743af8bf4f87408b515"
|
||||||
|
|||||||
@@ -121,26 +121,7 @@ struct MemberProfileDialog: View {
|
|||||||
viewModel.getMemberProfile(memberId: memberId)
|
viewModel.getMemberProfile(memberId: memberId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.bottom, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
if viewModel.dismissDialog {
|
|
||||||
isShowing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.isShowUesrBlockConfirm {
|
if viewModel.isShowUesrBlockConfirm {
|
||||||
UserBlockConfirmDialogView(
|
UserBlockConfirmDialogView(
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ enum ExplorerApi {
|
|||||||
case getExplorer
|
case getExplorer
|
||||||
case searchChannel(channel: String)
|
case searchChannel(channel: String)
|
||||||
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
|
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
|
||||||
|
case getCreatorDetail(userId: Int)
|
||||||
case getFollowerList(userId: Int, page: Int, size: Int)
|
case getFollowerList(userId: Int, page: Int, size: Int)
|
||||||
case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
|
case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
|
||||||
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
|
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
|
||||||
case modifyCheers(request: PutModifyCheersRequest)
|
case modifyCheers(request: PutModifyCheersRequest)
|
||||||
case writeCreatorNotice(request: PostCreatorNoticeRequest)
|
case writeCreatorNotice(request: PostCreatorNoticeRequest)
|
||||||
case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int)
|
case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int, period: DonationRankingPeriod?)
|
||||||
|
case getChannelDonationList(creatorId: Int)
|
||||||
|
case postChannelDonation(request: PostChannelDonationRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExplorerApi: TargetType {
|
extension ExplorerApi: TargetType {
|
||||||
@@ -40,7 +43,10 @@ extension ExplorerApi: TargetType {
|
|||||||
case .getCreatorProfile(let userId, _):
|
case .getCreatorProfile(let userId, _):
|
||||||
return "/explorer/profile/\(userId)"
|
return "/explorer/profile/\(userId)"
|
||||||
|
|
||||||
case .getCreatorProfileDonationRanking(let userId, _, _):
|
case .getCreatorDetail(let userId):
|
||||||
|
return "/explorer/profile/\(userId)/detail"
|
||||||
|
|
||||||
|
case .getCreatorProfileDonationRanking(let userId, _, _, _):
|
||||||
return "/explorer/profile/\(userId)/donation-rank"
|
return "/explorer/profile/\(userId)/donation-rank"
|
||||||
|
|
||||||
case .getFollowerList(let userId, _, _):
|
case .getFollowerList(let userId, _, _):
|
||||||
@@ -57,15 +63,18 @@ extension ExplorerApi: TargetType {
|
|||||||
|
|
||||||
case .writeCreatorNotice:
|
case .writeCreatorNotice:
|
||||||
return "/explorer/profile/notice"
|
return "/explorer/profile/notice"
|
||||||
|
|
||||||
|
case .getChannelDonationList, .postChannelDonation:
|
||||||
|
return "/explorer/profile/channel-donation"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var method: Moya.Method {
|
var method: Moya.Method {
|
||||||
switch self {
|
switch self {
|
||||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank:
|
case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank, .getChannelDonationList:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
case .writeCheers, .writeCreatorNotice:
|
case .writeCheers, .writeCreatorNotice, .postChannelDonation:
|
||||||
return .post
|
return .post
|
||||||
|
|
||||||
case .modifyCheers:
|
case .modifyCheers:
|
||||||
@@ -75,7 +84,7 @@ extension ExplorerApi: TargetType {
|
|||||||
|
|
||||||
var task: Task {
|
var task: Task {
|
||||||
switch self {
|
switch self {
|
||||||
case .getExplorer, .getCreatorRank:
|
case .getExplorer, .getCreatorRank, .getCreatorDetail:
|
||||||
return .requestPlain
|
return .requestPlain
|
||||||
|
|
||||||
case .searchChannel(let channel):
|
case .searchChannel(let channel):
|
||||||
@@ -112,12 +121,22 @@ extension ExplorerApi: TargetType {
|
|||||||
case .writeCreatorNotice(let request):
|
case .writeCreatorNotice(let request):
|
||||||
return .requestJSONEncodable(request)
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
case .getCreatorProfileDonationRanking(_, let page, let size):
|
case .getChannelDonationList(let creatorId):
|
||||||
let parameters = [
|
return .requestParameters(parameters: ["creatorId": creatorId], encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
|
case .postChannelDonation(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .getCreatorProfileDonationRanking(_, let page, let size, let period):
|
||||||
|
var parameters = [
|
||||||
"page": page - 1,
|
"page": page - 1,
|
||||||
"size": size
|
"size": size
|
||||||
] as [String : Any]
|
] as [String : Any]
|
||||||
|
|
||||||
|
if let period {
|
||||||
|
parameters["period"] = period.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ final class ExplorerRepository {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getCreatorDetail(userId: id))
|
||||||
|
}
|
||||||
|
|
||||||
func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
func getFollowerList(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size))
|
return api.requestPublisher(.getFollowerList(userId: userId, page: page, size: size))
|
||||||
}
|
}
|
||||||
@@ -51,7 +55,27 @@ final class ExplorerRepository {
|
|||||||
return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice)))
|
return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
func getCreatorProfileDonationRanking(
|
||||||
return api.requestPublisher(.getCreatorProfileDonationRanking(userId: userId, page: page, size: size))
|
userId: Int,
|
||||||
|
page: Int,
|
||||||
|
size: Int,
|
||||||
|
period: DonationRankingPeriod?
|
||||||
|
) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(
|
||||||
|
.getCreatorProfileDonationRanking(
|
||||||
|
userId: userId,
|
||||||
|
page: page,
|
||||||
|
size: size,
|
||||||
|
period: period
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannelDonationList(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getChannelDonationList(creatorId: creatorId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postChannelDonation(request: PostChannelDonationRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.postChannelDonation(request: request))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||