공개: 플랫폼 결론과 릴리즈 노트 규칙 반영
54
.github/workflows/release-portable.yml
vendored
|
|
@ -31,7 +31,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -42,21 +42,30 @@ jobs:
|
||||||
with:
|
with:
|
||||||
global-json-file: global.json
|
global-json-file: global.json
|
||||||
|
|
||||||
- name: Restore desktop project
|
- name: Install NSIS
|
||||||
run: dotnet restore src/PhysOn.Desktop/PhysOn.Desktop.csproj
|
run: sudo apt-get update && sudo apt-get install -y nsis
|
||||||
|
|
||||||
- name: Publish portable desktop build
|
- name: Build Windows release assets
|
||||||
run: dotnet publish src/PhysOn.Desktop/PhysOn.Desktop.csproj -c Release -r win-x64 --self-contained true -o out/win-x64
|
env:
|
||||||
|
VERSION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/release/build-windows-distributions.sh
|
||||||
|
./scripts/release/build-windows-distributions.sh \
|
||||||
|
--version "$VERSION_INPUT" \
|
||||||
|
--output out
|
||||||
|
|
||||||
- name: Create portable ZIP
|
cp out/KoTalk-windows-x64-"$VERSION_INPUT".zip out/KoTalk-windows-x64.zip
|
||||||
shell: pwsh
|
cp out/KoTalk-windows-x64-onefile-"$VERSION_INPUT".exe out/KoTalk-windows-x64-onefile.exe
|
||||||
run: Compress-Archive -Path out/win-x64/* -DestinationPath out/KoTalk-windows-x64.zip
|
cp out/KoTalk-windows-x64-installer-"$VERSION_INPUT".exe out/KoTalk-windows-x64-installer.exe
|
||||||
|
|
||||||
- name: Upload Windows artifact
|
- name: Upload Windows artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-portable
|
name: windows-release
|
||||||
path: out/KoTalk-windows-x64.zip
|
path: |
|
||||||
|
out/KoTalk-windows-x64.zip
|
||||||
|
out/KoTalk-windows-x64-onefile.exe
|
||||||
|
out/KoTalk-windows-x64-installer.exe
|
||||||
|
|
||||||
build-android:
|
build-android:
|
||||||
if: ${{ hashFiles('src/PhysOn.Mobile.Android/*.csproj') != '' }}
|
if: ${{ hashFiles('src/PhysOn.Mobile.Android/*.csproj') != '' }}
|
||||||
|
|
@ -80,23 +89,16 @@ jobs:
|
||||||
- name: Install Android workload
|
- name: Install Android workload
|
||||||
run: dotnet workload install android
|
run: dotnet workload install android
|
||||||
|
|
||||||
- name: Restore Android project
|
|
||||||
run: dotnet restore src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj
|
|
||||||
|
|
||||||
- name: Publish Android APK
|
- name: Publish Android APK
|
||||||
|
env:
|
||||||
|
VERSION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \
|
chmod +x scripts/release/build-android-apk.sh
|
||||||
-c Release \
|
./scripts/release/build-android-apk.sh \
|
||||||
-f net8.0-android \
|
--version "$VERSION_INPUT" \
|
||||||
-p:AndroidPackageFormat=apk \
|
--output out
|
||||||
-p:AndroidKeyStore=false \
|
|
||||||
-o out/android
|
|
||||||
|
|
||||||
- name: Collect Android artifact
|
cp out/KoTalk-android-universal-"$VERSION_INPUT".apk out/KoTalk-android-universal.apk
|
||||||
run: |
|
|
||||||
apk_path="$(find out/android -type f -name '*.apk' | head -n 1)"
|
|
||||||
test -n "$apk_path"
|
|
||||||
cp "$apk_path" out/KoTalk-android-universal.apk
|
|
||||||
|
|
||||||
- name: Upload Android artifact
|
- name: Upload Android artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -118,7 +120,7 @@ jobs:
|
||||||
- name: Download Windows artifact
|
- name: Download Windows artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-portable
|
name: windows-release
|
||||||
path: incoming/windows
|
path: incoming/windows
|
||||||
|
|
||||||
- name: Download Android artifact
|
- name: Download Android artifact
|
||||||
|
|
@ -149,6 +151,8 @@ jobs:
|
||||||
--version "$VERSION_INPUT"
|
--version "$VERSION_INPUT"
|
||||||
--channel "$channel"
|
--channel "$channel"
|
||||||
--windows-zip incoming/windows/KoTalk-windows-x64.zip
|
--windows-zip incoming/windows/KoTalk-windows-x64.zip
|
||||||
|
--windows-portable-exe incoming/windows/KoTalk-windows-x64-onefile.exe
|
||||||
|
--windows-installer-exe incoming/windows/KoTalk-windows-x64-installer.exe
|
||||||
--force
|
--force
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Android WebView 기반 첫 APK 프로젝트와 `KoTalk-android-universal-*.apk` 산출물 경로 추가
|
||||||
|
- Windows `installer exe + onefile exe + zip` 3종 배포 체인 추가
|
||||||
|
- 모바일/iOS/Linux 프레임워크 결론 문서 `CLIENT_PLATFORM_DECISION.md` 추가
|
||||||
- 공개 원격용 버전 태그 생성 스크립트 `scripts/release/release-create-tag.sh`
|
- 공개 원격용 버전 태그 생성 스크립트 `scripts/release/release-create-tag.sh`
|
||||||
- GitHub 릴리즈 게시 스크립트 `scripts/release/release-publish-github.sh`
|
- GitHub 릴리즈 게시 스크립트 `scripts/release/release-publish-github.sh`
|
||||||
- 제2·제3 공개 원격 순차 게시 스크립트 `scripts/release/release-publish-public.sh`
|
- 제2·제3 공개 원격 순차 게시 스크립트 `scripts/release/release-publish-public.sh`
|
||||||
|
|
@ -53,6 +56,8 @@
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- 공개 릴리즈 Assets에서 스크린샷과 릴리즈 노트 첨부를 제거하고, 최신 화면은 변경 노트 본문에서 직접 확인하도록 조정
|
||||||
|
- Android 상태를 `계획`에서 `첫 APK 기준선 확보` 단계로 상향하고, iOS/Linux 계획을 공개 문서에 명시
|
||||||
- 데스크톱/웹 스크린샷 캡처를 앱 창·앱 셸 기준으로 정리하고, README·SHOWCASE 이미지를 실제 종횡비에 맞춘 `width/height` 기준으로 재배치
|
- 데스크톱/웹 스크린샷 캡처를 앱 창·앱 셸 기준으로 정리하고, README·SHOWCASE 이미지를 실제 종횡비에 맞춘 `width/height` 기준으로 재배치
|
||||||
- 공개 원격 배포 정책을 `public/* 브랜치 + 버전 태그 + 릴리즈 페이지 + 자산` 기준으로 고정
|
- 공개 원격 배포 정책을 `public/* 브랜치 + 버전 태그 + 릴리즈 페이지 + 자산` 기준으로 고정
|
||||||
- Gitea/GitHub 릴리즈 게시 스크립트가 지정 원격 기준으로 동작하고 최신 스크린샷 자산도 함께 첨부하도록 확장
|
- Gitea/GitHub 릴리즈 게시 스크립트가 지정 원격 기준으로 동작하고 최신 스크린샷 자산도 함께 첨부하도록 확장
|
||||||
|
|
|
||||||
160
CLIENT_PLATFORM_DECISION.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Client Platform Decision
|
||||||
|
|
||||||
|
마지막 정리일: `2026-04-16`
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
KoTalk의 장기적인 네이티브 클라이언트 기준선은 **Avalonia 중심**으로 유지하는 편이 맞습니다.
|
||||||
|
현재 Android의 WebView 셸은 `alpha` 단계의 빠른 APK 배포를 위한 전술적 경로로 유지하되, iOS와 Linux까지 포함하는 장기 클라이언트 전략은 Avalonia를 축으로 정리합니다.
|
||||||
|
|
||||||
|
## 왜 다시 판단했는가
|
||||||
|
|
||||||
|
초기에는 `Windows + mobile web + Android APK`만 빨리 닫는 것이 우선이었습니다. 그런데 이제 조건이 바뀌었습니다.
|
||||||
|
|
||||||
|
- Android만이 아니라 iOS도 예정돼 있습니다.
|
||||||
|
- Linux 공식 지원도 예정돼 있습니다.
|
||||||
|
- Windows는 이미 네이티브 데스크톱 축으로 유지해야 합니다.
|
||||||
|
- 저장소와 공식 서비스 모두에서 UI/UX 일관성을 더 강하게 요구받고 있습니다.
|
||||||
|
|
||||||
|
즉, Android만 빨리 붙이는 기준이 아니라 `Windows + Android + iOS + Linux`를 함께 수용하는 클라이언트 구조가 필요해졌습니다.
|
||||||
|
|
||||||
|
## 현재 기준선
|
||||||
|
|
||||||
|
### 지금 이미 있는 것
|
||||||
|
|
||||||
|
- Windows 네이티브 데스크톱 앱: Avalonia 기반
|
||||||
|
- Mobile web: 실제 운영 중
|
||||||
|
- Android: WebView 기반 첫 APK 셸 생성 가능
|
||||||
|
|
||||||
|
### 지금 없는 것
|
||||||
|
|
||||||
|
- iOS 클라이언트
|
||||||
|
- Linux 네이티브 패키징
|
||||||
|
- Android/iOS 전용 네이티브 경험을 공용 UI 축으로 가져가는 체계
|
||||||
|
|
||||||
|
## 검토한 선택지
|
||||||
|
|
||||||
|
### 1. Android/iOS를 계속 각각 별도 네이티브 셸로 유지
|
||||||
|
|
||||||
|
장점:
|
||||||
|
|
||||||
|
- 지금 당장 APK를 빠르게 배포하기 쉽다
|
||||||
|
- 모바일 웹을 감싸는 방식이라 초기 유지 비용이 낮다
|
||||||
|
|
||||||
|
한계:
|
||||||
|
|
||||||
|
- Windows와 Linux 쪽 UI 자산을 재사용하기 어렵다
|
||||||
|
- iOS까지 같은 방식으로 늘어나면 표면은 빠르게 늘지만 제품 일관성은 약해진다
|
||||||
|
- 장기적으로는 플랫폼마다 다른 껍데기를 유지하게 된다
|
||||||
|
|
||||||
|
판단:
|
||||||
|
|
||||||
|
- **전술적으로는 유효**
|
||||||
|
- **장기 기준선으로는 부적합**
|
||||||
|
|
||||||
|
### 2. .NET MAUI로 장기 축 전환
|
||||||
|
|
||||||
|
장점:
|
||||||
|
|
||||||
|
- Android와 iOS, Windows까지 한 프레임워크로 묶기 좋다
|
||||||
|
- 모바일 장치 API 접근이 익숙한 생태계다
|
||||||
|
|
||||||
|
한계:
|
||||||
|
|
||||||
|
- Microsoft 공식 문서 기준 `.NET MAUI`의 기본 지원 플랫폼은 Android, iOS, macOS, Windows다. Linux는 기본 축에 없다.
|
||||||
|
- 현재 저장소의 Windows 클라이언트는 이미 Avalonia 기반이라, MAUI로 옮기면 데스크톱 축을 다시 크게 재구성해야 한다.
|
||||||
|
|
||||||
|
판단:
|
||||||
|
|
||||||
|
- **Linux 공식 지원 예정 조건과 현재 코드베이스 기준을 함께 놓고 보면, 주 프레임워크로 선택하기 어렵다**
|
||||||
|
|
||||||
|
참고:
|
||||||
|
|
||||||
|
- Microsoft Learn, “What is .NET MAUI?”: <https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui>
|
||||||
|
- Microsoft Learn, “Supported platforms for .NET MAUI apps”: <https://learn.microsoft.com/en-us/dotnet/maui/supported-platforms?view=net-maui-9.0>
|
||||||
|
|
||||||
|
### 3. Uno Platform으로 장기 축 전환
|
||||||
|
|
||||||
|
장점:
|
||||||
|
|
||||||
|
- 공식 문서 기준 Android, iOS, Web, macOS, Linux, Windows를 포괄한다
|
||||||
|
- 광범위한 플랫폼 표면 자체는 매력적이다
|
||||||
|
|
||||||
|
한계:
|
||||||
|
|
||||||
|
- 현재 Windows 클라이언트가 Avalonia 기반이라, Uno로 가면 데스크톱 UI 자산과 운영 경험을 다시 맞춰야 한다
|
||||||
|
- 지금 시점의 저장소 기준으로는 전환 비용이 작지 않다
|
||||||
|
|
||||||
|
판단:
|
||||||
|
|
||||||
|
- **대안으로는 충분히 검토 가능**
|
||||||
|
- **하지만 현재 코드베이스를 고려하면 1순위는 아님**
|
||||||
|
|
||||||
|
참고:
|
||||||
|
|
||||||
|
- Uno Platform, “Supported platforms”: <https://platform.uno/docs/articles/getting-started/requirements.html>
|
||||||
|
|
||||||
|
### 4. Avalonia를 장기 축으로 유지
|
||||||
|
|
||||||
|
장점:
|
||||||
|
|
||||||
|
- 현재 Windows 클라이언트가 이미 Avalonia 기반이다
|
||||||
|
- 공식 문서 기준 Windows, Linux, iOS, Android, WebAssembly까지 포괄한다
|
||||||
|
- 데스크톱 밀도, 멀티 윈도우, 플랫한 커스텀 UI를 유지하기 유리하다
|
||||||
|
- Linux 공식 지원 예정이라는 조건과 가장 자연스럽게 맞는다
|
||||||
|
|
||||||
|
전제 조건:
|
||||||
|
|
||||||
|
- Avalonia 공식 문서 기준 데스크톱은 `.NET 8` 이상이지만, 모바일인 iOS/Android는 `.NET 10` 기준을 따른다
|
||||||
|
- 따라서 현재 저장소의 `.NET 8` 기준선을 유지한 채 즉시 Android/iOS를 Avalonia로 통합하는 것은 현실적이지 않다
|
||||||
|
|
||||||
|
주의:
|
||||||
|
|
||||||
|
- 모바일은 데스크톱과 같은 방식으로 밀어붙이면 안 된다
|
||||||
|
- 작은 화면, 제스처, OS 관례를 반영한 모바일 전용 UX 조정은 별도로 필요하다
|
||||||
|
- 현재 Android alpha 셸을 곧바로 폐기할 필요는 없지만, 장기적으로는 공용 UI 구조와의 관계를 분명히 해야 한다
|
||||||
|
|
||||||
|
판단:
|
||||||
|
|
||||||
|
- **현 시점 KoTalk의 가장 현실적인 장기 기준선**
|
||||||
|
|
||||||
|
참고:
|
||||||
|
|
||||||
|
- Avalonia Docs, “Supported platforms”: <https://docs.avaloniaui.net/docs/supported-platforms>
|
||||||
|
- Avalonia Docs, “Welcome / What is Avalonia?”: <https://docs.avaloniaui.net/docs/welcome>
|
||||||
|
|
||||||
|
## 최종 결정
|
||||||
|
|
||||||
|
### 전술
|
||||||
|
|
||||||
|
- Android는 당분간 현재 WebView 셸을 유지합니다.
|
||||||
|
- 이유는 APK 배포, 설치 동선 검증, 알파 피드백 수집을 빠르게 계속하기 위해서입니다.
|
||||||
|
- 현재 저장소가 `.NET 8` 기준선이므로, 모바일 공용 UI로 바로 넘어가기 전까지는 이 경로가 가장 안전합니다.
|
||||||
|
|
||||||
|
### 전략
|
||||||
|
|
||||||
|
- 장기적인 네이티브 클라이언트 축은 **Avalonia 기반 공유 구조**로 정리합니다.
|
||||||
|
- 다만 이 단계는 `.NET 10` 모바일 기준선을 감당할 준비가 됐을 때 들어갑니다.
|
||||||
|
- Windows와 Linux는 같은 데스크톱 계열 기준으로 묶습니다.
|
||||||
|
- Android와 iOS는 같은 제품 경험 원칙 아래로 수렴시키되, 모바일 전용 UX 조정은 별도 계층으로 둡니다.
|
||||||
|
|
||||||
|
## 배포 원칙
|
||||||
|
|
||||||
|
- 자체 미러와 공개 저장소 Assets:
|
||||||
|
- Windows installer / onefile / zip
|
||||||
|
- Android APK
|
||||||
|
- Apple 배포 채널:
|
||||||
|
- iOS는 저장소 Assets나 자체 미러 직접 배포가 아니라 Apple 채널을 전제로 준비합니다
|
||||||
|
|
||||||
|
## 지금 바로 바뀌는 문장
|
||||||
|
|
||||||
|
- `Android는 계획 단계`라고 쓰지 않습니다.
|
||||||
|
- `Android APK 기준선은 확보됐고, 장기 모바일 전략은 재정의 중`이라고 씁니다.
|
||||||
|
- `iOS와 Linux는 예정`이라고 공개적으로 적되, 아직 완성된 것처럼 쓰지 않습니다.
|
||||||
|
|
||||||
|
## 다음 실행 항목
|
||||||
|
|
||||||
|
1. Android alpha 셸을 유지하면서 배포와 설치 루프를 먼저 안정화
|
||||||
|
2. `.NET 10` 전환 부담과 시점을 포함해 Avalonia 기반 공유 클라이언트 구조를 설계
|
||||||
|
3. Linux 패키징 기준선 정리
|
||||||
|
4. iOS 배포 준비 문서와 빌드 전제 정리
|
||||||
|
|
@ -80,22 +80,21 @@ dotnet test PhysOn.sln -c Debug
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet publish src/PhysOn.Desktop/PhysOn.Desktop.csproj \
|
./scripts/release/build-windows-distributions.sh \
|
||||||
-c Release \
|
--version 2026.04.16-alpha.6
|
||||||
-r win-x64 \
|
|
||||||
--self-contained true \
|
|
||||||
-o artifacts/release/v0.1.0-alpha.1-win-x64
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
생성물:
|
||||||
|
|
||||||
|
- `KoTalk-windows-x64-<version>.zip`
|
||||||
|
- `KoTalk-windows-x64-onefile-<version>.exe`
|
||||||
|
- `KoTalk-windows-x64-installer-<version>.exe`
|
||||||
|
|
||||||
Android:
|
Android:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet workload install android
|
./scripts/release/build-android-apk.sh \
|
||||||
dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \
|
--version 2026.04.16-alpha.6
|
||||||
-c Release \
|
|
||||||
-f net8.0-android \
|
|
||||||
-p:AndroidPackageFormat=apk \
|
|
||||||
-o artifacts/release/android
|
|
||||||
```
|
```
|
||||||
|
|
||||||
공개 산출물 네이밍은 `KoTalk-*` 기준으로 정리하는 방향이고, 현재 내부 스크립트와 프로젝트명은 별도 정렬 단계에 있습니다.
|
공개 산출물 네이밍은 `KoTalk-*` 기준으로 정리하는 방향이고, 현재 내부 스크립트와 프로젝트명은 별도 정렬 단계에 있습니다.
|
||||||
|
|
@ -104,10 +103,12 @@ dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/release/release-prepare-assets.sh \
|
./scripts/release/release-prepare-assets.sh \
|
||||||
--version v0.1.0-alpha.1 \
|
--version 2026.04.16-alpha.6 \
|
||||||
--channel alpha \
|
--channel alpha \
|
||||||
--windows-zip artifacts/release/PhysOn-win-x64-v0.1.0-alpha.1.zip \
|
--windows-zip artifacts/builds/2026.04.16-alpha.6/KoTalk-windows-x64-2026.04.16-alpha.6.zip \
|
||||||
--android-apk artifacts/release/PhysOn-android-universal-v0.1.0-alpha.1.apk \
|
--windows-portable-exe artifacts/builds/2026.04.16-alpha.6/KoTalk-windows-x64-onefile-2026.04.16-alpha.6.exe \
|
||||||
|
--windows-installer-exe artifacts/builds/2026.04.16-alpha.6/KoTalk-windows-x64-installer-2026.04.16-alpha.6.exe \
|
||||||
|
--android-apk artifacts/builds/2026.04.16-alpha.6/KoTalk-android-universal-2026.04.16-alpha.6.apk \
|
||||||
--screenshots artifacts/screenshots \
|
--screenshots artifacts/screenshots \
|
||||||
--force
|
--force
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
| Public brand | `KoTalk` |
|
| Public brand | `KoTalk` |
|
||||||
| Stage | `Alpha` |
|
| Stage | `Alpha` |
|
||||||
| Most usable surface | Mobile web live + Windows build |
|
| Most usable surface | Mobile web live + Windows build |
|
||||||
| Biggest current gap | Android 실빌드와 데스크톱 멀티윈도우 완성도 |
|
| Biggest current gap | 네이티브 모바일 공용 UI 수렴과 데스크톱 멀티윈도우 완성도 |
|
||||||
| Signup direction | 공개형 1회성 인증 중심으로 재설계 중 |
|
| Signup direction | 공개형 1회성 인증 중심으로 재설계 중 |
|
||||||
| Tone of this repo | 현재 동작 범위와 남은 갭을 함께 적는 제품형 저장소 |
|
| Tone of this repo | 현재 동작 범위와 남은 갭을 함께 적는 제품형 저장소 |
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
|
|
||||||
- Windows 데스크톱 클라이언트 빌드
|
- Windows 데스크톱 클라이언트 빌드
|
||||||
- 모바일 웹 실서비스 채널
|
- 모바일 웹 실서비스 채널
|
||||||
|
- Android APK 빌드 기준선
|
||||||
- 기본 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 복구 루프
|
- 기본 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 복구 루프
|
||||||
- 최신 기준 스크린샷 세트
|
- 최신 기준 스크린샷 세트
|
||||||
- 릴리즈 경로와 다운로드 경로 문서
|
- 릴리즈 경로와 다운로드 경로 문서
|
||||||
|
|
@ -29,8 +30,10 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Windows desktop | 저장소 빌드 / 릴리즈 산출물 | Buildable | 핵심 메시징 루프 검증 가능 |
|
| Windows desktop | 저장소 빌드 / 릴리즈 산출물 | Buildable | 핵심 메시징 루프 검증 가능 |
|
||||||
| Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 제공 |
|
| Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 제공 |
|
||||||
| Android | 저장소 릴리즈 예정 | In progress | 문서와 배포 구조 우선 정리 중 |
|
| Android | 저장소 APK / 릴리즈 파이프라인 | Buildable | alpha WebView 셸과 APK 산출물 생성 가능 |
|
||||||
| Official mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest와 version manifest를 HTTPS로 제공 |
|
| iOS | Apple 배포 채널 예정 | Planned | 자체 미러가 아닌 Apple 채널 전제 |
|
||||||
|
| Linux | 저장소 빌드 예정 | Planned | Windows와 같은 공유 UI 축으로 준비 |
|
||||||
|
| Official mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest, Android latest, version manifest를 HTTPS로 제공 |
|
||||||
|
|
||||||
## Verified Now
|
## Verified Now
|
||||||
|
|
||||||
|
|
@ -38,6 +41,7 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
|
|
||||||
- Windows 클라이언트는 저장소 기준으로 빌드 가능한 상태입니다.
|
- Windows 클라이언트는 저장소 기준으로 빌드 가능한 상태입니다.
|
||||||
- 모바일 웹은 [vstalk.phy.kr](https://vstalk.phy.kr)에서 공개 중입니다.
|
- 모바일 웹은 [vstalk.phy.kr](https://vstalk.phy.kr)에서 공개 중입니다.
|
||||||
|
- Android APK는 저장소 기준으로 생성 가능한 상태입니다.
|
||||||
- 기본 메시징 루프와 세션 복구 흐름은 구현돼 있습니다.
|
- 기본 메시징 루프와 세션 복구 흐름은 구현돼 있습니다.
|
||||||
- 검색, 보관, 빈 상태 UX는 1차 개편이 반영돼 있습니다.
|
- 검색, 보관, 빈 상태 UX는 1차 개편이 반영돼 있습니다.
|
||||||
- 최신 스크린샷은 저장소에 함께 보관됩니다.
|
- 최신 스크린샷은 저장소에 함께 보관됩니다.
|
||||||
|
|
@ -66,7 +70,9 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
|
||||||
- Android 첫 실사용 빌드
|
- Android APK 서명과 배포 품질 정리
|
||||||
|
- iOS 채널 준비와 Apple 배포 절차 정리
|
||||||
|
- Linux 네이티브 패키징 기준선
|
||||||
- 릴리즈 페이지와 미러 간 latest 라우트 통합
|
- 릴리즈 페이지와 미러 간 latest 라우트 통합
|
||||||
- 검색 범위 확장
|
- 검색 범위 확장
|
||||||
- 파일 전송
|
- 파일 전송
|
||||||
|
|
@ -76,10 +82,12 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
|
|
||||||
아직 부족한 부분도 그대로 남깁니다.
|
아직 부족한 부분도 그대로 남깁니다.
|
||||||
|
|
||||||
- Android 실사용 빌드는 아직 제공되지 않습니다.
|
- Android는 현재 WebView 셸 중심의 첫 APK 기준선으로, 네이티브 모바일 경험은 더 다듬어야 합니다.
|
||||||
|
- iOS 클라이언트는 아직 시작되지 않았습니다.
|
||||||
|
- Linux 클라이언트는 장기 목표로만 정리돼 있습니다.
|
||||||
- 파일 전송은 미구현입니다.
|
- 파일 전송은 미구현입니다.
|
||||||
- 검색은 전역 파일/링크/사람 범위까지 확장되지 않았습니다.
|
- 검색은 전역 파일/링크/사람 범위까지 확장되지 않았습니다.
|
||||||
- 공식 다운로드 미러는 현재 Windows latest와 version manifest 기준으로 동작합니다.
|
- 공식 다운로드 미러는 현재 Windows latest, Android latest, version manifest 기준으로 동작합니다.
|
||||||
- 데스크톱 멀티 윈도우는 방향은 잡혀 있지만, 실제 생산성 흐름은 더 다듬어야 합니다.
|
- 데스크톱 멀티 윈도우는 방향은 잡혀 있지만, 실제 생산성 흐름은 더 다듬어야 합니다.
|
||||||
|
|
||||||
## Download And Release Paths
|
## Download And Release Paths
|
||||||
|
|
@ -111,3 +119,4 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서
|
||||||
- 라이브 우선순위: [문서/35-live-user-review-and-priority-backlog.md](문서/35-live-user-review-and-priority-backlog.md)
|
- 라이브 우선순위: [문서/35-live-user-review-and-priority-backlog.md](문서/35-live-user-review-and-priority-backlog.md)
|
||||||
- 가입/온보딩 정책: [문서/10-signup-onboarding-and-auth-policy.md](문서/10-signup-onboarding-and-auth-policy.md)
|
- 가입/온보딩 정책: [문서/10-signup-onboarding-and-auth-policy.md](문서/10-signup-onboarding-and-auth-policy.md)
|
||||||
- 신뢰와 보안 표면: [TRUST_CENTER.md](TRUST_CENTER.md), [SECURITY.md](SECURITY.md)
|
- 신뢰와 보안 표면: [TRUST_CENTER.md](TRUST_CENTER.md), [SECURITY.md](SECURITY.md)
|
||||||
|
- 클라이언트 플랫폼 결론: [CLIENT_PLATFORM_DECISION.md](CLIENT_PLATFORM_DECISION.md)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{04EBE1E2
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Api.IntegrationTests", "tests\PhysOn.Api.IntegrationTests\PhysOn.Api.IntegrationTests.csproj", "{38821CD9-915B-4C96-8A35-11BDE991C16E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Api.IntegrationTests", "tests\PhysOn.Api.IntegrationTests\PhysOn.Api.IntegrationTests.csproj", "{38821CD9-915B-4C96-8A35-11BDE991C16E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Mobile.Android", "src\PhysOn.Mobile.Android\PhysOn.Mobile.Android.csproj", "{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -64,6 +66,10 @@ Global
|
||||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
{DD8A5885-9647-4248-A0B2-C8695BBFB54E} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||||
|
|
@ -74,5 +80,6 @@ Global
|
||||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
{925D92EC-965D-44D9-A3FF-011E1815C9EE} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
{90EF510F-E338-45C4-9EF4-0A4916725EF0} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||||
{38821CD9-915B-4C96-8A35-11BDE991C16E} = {04EBE1E2-D374-4F58-A0D5-5062BB4674FA}
|
{38821CD9-915B-4C96-8A35-11BDE991C16E} = {04EBE1E2-D374-4F58-A0D5-5062BB4674FA}
|
||||||
|
{FD4774AB-47F7-4C47-BCC7-FD97BCBF4101} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
18
README.md
|
|
@ -16,6 +16,10 @@
|
||||||
짧은 답장, 빠른 복귀, 설명 가능한 제품 표면, 그리고 한국어 사용 습관에 맞는 조용한 UI를 핵심 기준으로 삼습니다.
|
짧은 답장, 빠른 복귀, 설명 가능한 제품 표면, 그리고 한국어 사용 습관에 맞는 조용한 UI를 핵심 기준으로 삼습니다.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
현재는 Windows, 모바일 웹, Android APK 기준선을 운영하고 있으며, iOS와 Linux 네이티브 채널은 같은 제품 경험 안으로 수렴시키는 방향을 공개적으로 준비 중입니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="PROJECT_STATUS.md"><img alt="status" src="https://img.shields.io/badge/status-active%20alpha-166534"></a>
|
<a href="PROJECT_STATUS.md"><img alt="status" src="https://img.shields.io/badge/status-active%20alpha-166534"></a>
|
||||||
<a href="PROJECT_STATUS.md"><img alt="platforms" src="https://img.shields.io/badge/platforms-windows%20%7C%20web%20%7C%20android-1D4ED8"></a>
|
<a href="PROJECT_STATUS.md"><img alt="platforms" src="https://img.shields.io/badge/platforms-windows%20%7C%20web%20%7C%20android-1D4ED8"></a>
|
||||||
|
|
@ -31,6 +35,7 @@
|
||||||
<a href="SHOWCASE.md">Showcase</a> ·
|
<a href="SHOWCASE.md">Showcase</a> ·
|
||||||
<a href="BACKGROUND.md">Background</a> ·
|
<a href="BACKGROUND.md">Background</a> ·
|
||||||
<a href="ALTERNATIVE_GAP.md">Alternative Gap</a> ·
|
<a href="ALTERNATIVE_GAP.md">Alternative Gap</a> ·
|
||||||
|
<a href="CLIENT_PLATFORM_DECISION.md">Platform Strategy</a> ·
|
||||||
<a href="branding/BRAND_GUIDE.md">Brand Guide</a> ·
|
<a href="branding/BRAND_GUIDE.md">Brand Guide</a> ·
|
||||||
<a href="FAQ.md">FAQ</a> ·
|
<a href="FAQ.md">FAQ</a> ·
|
||||||
<a href="RELEASING.md">Releases</a> ·
|
<a href="RELEASING.md">Releases</a> ·
|
||||||
|
|
@ -61,8 +66,8 @@
|
||||||
| Layer | Current read |
|
| Layer | Current read |
|
||||||
|---|---|
|
|---|---|
|
||||||
| What this is | 한국어 UI, 낮은 피로도, 업무형 메시징 복귀 흐름을 중심에 둔 Windows-first 오픈소스 메신저 |
|
| What this is | 한국어 UI, 낮은 피로도, 업무형 메시징 복귀 흐름을 중심에 둔 Windows-first 오픈소스 메신저 |
|
||||||
| What works now | Windows 빌드, 모바일 웹 라이브, 기본 인증/대화/세션 루프 |
|
| What works now | Windows 빌드, 모바일 웹 라이브, Android APK 기준선, 기본 인증/대화/세션 루프 |
|
||||||
| What is still moving | Android 첫 실빌드, 파일 전송, 검색 확장, 공개 다운로드 미러 정합성 |
|
| What is still moving | 파일 전송, 검색 확장, 네이티브 모바일 공용 UI 수렴, 공개 다운로드 미러 정합성 |
|
||||||
| What this repo tries to show | 화면, 빌드 산출물, 릴리즈 경로, 상태 문서, 배경 문서를 한 눈에 읽히게 정리한 제품형 저장소 |
|
| What this repo tries to show | 화면, 빌드 산출물, 릴리즈 경로, 상태 문서, 배경 문서를 한 눈에 읽히게 정리한 제품형 저장소 |
|
||||||
|
|
||||||
## Why Now
|
## Why Now
|
||||||
|
|
@ -156,14 +161,16 @@ KoTalk는 바로 그 비어 있는 결합점을 목표로 둡니다. 이 항목
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Windows desktop | 저장소 빌드와 버전별 산출물 | Buildable | 핵심 메시징 루프와 데스크톱 레이아웃 실험 진행 중 |
|
| Windows desktop | 저장소 빌드와 버전별 산출물 | Buildable | 핵심 메시징 루프와 데스크톱 레이아웃 실험 진행 중 |
|
||||||
| Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 검증 |
|
| Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 검증 |
|
||||||
| Android | 저장소 릴리즈 예정 | In progress | 문서와 배포 구조 우선 정리 중 |
|
| Android | 저장소 APK / 릴리즈 파이프라인 | Buildable | WebView 기반 alpha 셸과 APK 산출물 기준선 확보 |
|
||||||
| Official download mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest와 version manifest를 HTTPS로 제공 |
|
| iOS | Apple 배포 채널 예정 | Planned | 자체 미러 대신 App Store/TestFlight 전제 |
|
||||||
|
| Linux | 저장소 빌드 예정 | Planned | 장기적으로 Windows와 같은 네이티브 클라이언트 축 |
|
||||||
|
| Official download mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest, Android latest, version manifest를 HTTPS로 제공 |
|
||||||
|
|
||||||
## Architecture Snapshot
|
## Architecture Snapshot
|
||||||
|
|
||||||
KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각을 조합해 실서비스와 로컬 빌드를 함께 검증하는 쪽에 가깝습니다.
|
KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각을 조합해 실서비스와 로컬 빌드를 함께 검증하는 쪽에 가깝습니다.
|
||||||
|
|
||||||
- 클라이언트: Windows 데스크톱 + 모바일 웹 + Android 예정
|
- 클라이언트: Windows 데스크톱 + 모바일 웹 + Android APK 기준선 + iOS/Linux 예정
|
||||||
- API: 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 루프
|
- API: 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 루프
|
||||||
- 배포: VPS 기반 same-origin 웹앱과 API 운영
|
- 배포: VPS 기반 same-origin 웹앱과 API 운영
|
||||||
- 공개 증거: 저장소 스크린샷, 빌드 산출물, 상태 문서, 릴리즈 경로
|
- 공개 증거: 저장소 스크린샷, 빌드 산출물, 상태 문서, 릴리즈 경로
|
||||||
|
|
@ -211,6 +218,7 @@ KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각
|
||||||
- [ROADMAP.md](ROADMAP.md)
|
- [ROADMAP.md](ROADMAP.md)
|
||||||
- [BUSINESS_MODEL.md](BUSINESS_MODEL.md)
|
- [BUSINESS_MODEL.md](BUSINESS_MODEL.md)
|
||||||
- [ALTERNATIVE_GAP.md](ALTERNATIVE_GAP.md)
|
- [ALTERNATIVE_GAP.md](ALTERNATIVE_GAP.md)
|
||||||
|
- [CLIENT_PLATFORM_DECISION.md](CLIENT_PLATFORM_DECISION.md)
|
||||||
- [문서/01-product-strategy-and-mvp.md](문서/01-product-strategy-and-mvp.md)
|
- [문서/01-product-strategy-and-mvp.md](문서/01-product-strategy-and-mvp.md)
|
||||||
- [문서/18-white-material-compact-ui-system.md](문서/18-white-material-compact-ui-system.md)
|
- [문서/18-white-material-compact-ui-system.md](문서/18-white-material-compact-ui-system.md)
|
||||||
- [문서/22-work-communication-ux-playbook.md](문서/22-work-communication-ux-playbook.md)
|
- [문서/22-work-communication-ux-playbook.md](문서/22-work-communication-ux-playbook.md)
|
||||||
|
|
|
||||||
12
RELEASING.md
|
|
@ -15,7 +15,7 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공
|
||||||
## Current Note
|
## Current Note
|
||||||
|
|
||||||
2026-04-16 기준 [download-vstalk.phy.kr](https://download-vstalk.phy.kr)는 DNS와 HTTPS가 정상입니다.
|
2026-04-16 기준 [download-vstalk.phy.kr](https://download-vstalk.phy.kr)는 DNS와 HTTPS가 정상입니다.
|
||||||
현재는 Windows latest와 version manifest를 제공하고, 저장소 릴리즈 경로를 함께 유지합니다.
|
현재 기준선은 Windows installer / onefile / zip, Android latest APK, version manifest를 함께 제공하는 구조입니다.
|
||||||
|
|
||||||
## Minimum Release Contract
|
## Minimum Release Contract
|
||||||
|
|
||||||
|
|
@ -25,13 +25,16 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공
|
||||||
4. 최신 스크린샷이 현재 UI를 대표해야 합니다.
|
4. 최신 스크린샷이 현재 UI를 대표해야 합니다.
|
||||||
5. 다운로드 경로와 릴리즈 링크가 함께 갱신돼야 합니다.
|
5. 다운로드 경로와 릴리즈 링크가 함께 갱신돼야 합니다.
|
||||||
6. 공개 원격은 `브랜치 + 태그 + 릴리즈 페이지 + 자산`을 한 세트로 맞춥니다.
|
6. 공개 원격은 `브랜치 + 태그 + 릴리즈 페이지 + 자산`을 한 세트로 맞춥니다.
|
||||||
7. 공개 릴리즈 페이지에는 산출물과 최신 스크린샷을 함께 게시합니다.
|
7. 공개 릴리즈 페이지의 Assets는 실행 파일, 체크섬, 메타데이터처럼 실제 배포에 필요한 파일만 남깁니다.
|
||||||
|
8. 최신 스크린샷은 Assets 첨부 대신 변경 노트 안에 담고, 다운로드 미러의 정적 경로를 이미지 소스로 사용합니다.
|
||||||
|
|
||||||
## Platform Policy
|
## Platform Policy
|
||||||
|
|
||||||
- Windows: 빌드 산출물, 스크린샷, 체크섬을 함께 남깁니다.
|
- Windows: installer, onefile portable, zip, 체크섬을 기본 산출물로 유지합니다.
|
||||||
- Mobile web: 라이브 반영이 있으면 스크린샷과 상태 문서를 함께 갱신합니다.
|
- Mobile web: 라이브 반영이 있으면 스크린샷과 상태 문서를 함께 갱신합니다.
|
||||||
- Android: APK 공개 시 공식 미러와 저장소 릴리즈를 함께 맞춥니다.
|
- Android: APK 공개 시 공식 미러와 저장소 릴리즈를 함께 맞춥니다.
|
||||||
|
- iOS: 자체 미러나 저장소 Assets로 직접 배포하지 않고, Apple 배포 채널을 전제로 준비합니다.
|
||||||
|
- Linux: Windows와 함께 장기적인 네이티브 클라이언트 축으로 다루며, 공유 UI 프레임워크 기준선을 유지합니다.
|
||||||
|
|
||||||
## Public Release Sequence
|
## Public Release Sequence
|
||||||
|
|
||||||
|
|
@ -41,7 +44,8 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공
|
||||||
4. 제2 공개 레포에 브랜치와 태그를 푸시합니다.
|
4. 제2 공개 레포에 브랜치와 태그를 푸시합니다.
|
||||||
5. 제2 공개 레포 릴리즈 페이지에 자산과 노트를 게시합니다.
|
5. 제2 공개 레포 릴리즈 페이지에 자산과 노트를 게시합니다.
|
||||||
6. 명시적 요청이 있을 때만 같은 태그와 자산을 제3 공개 레포에 게시합니다.
|
6. 명시적 요청이 있을 때만 같은 태그와 자산을 제3 공개 레포에 게시합니다.
|
||||||
7. `download-vstalk.phy.kr`는 최신 포인터만 유지합니다.
|
7. `download-vstalk.phy.kr`는 `Windows latest landing`, `Android latest landing`, `latest/version.json` 포인터를 유지합니다.
|
||||||
|
8. 공개 릴리즈 노트에는 최신 스크린샷을 포함하고, 공개 릴리즈 Assets에는 스크린샷을 첨부하지 않습니다.
|
||||||
|
|
||||||
## Release Scripts
|
## Release Scripts
|
||||||
|
|
||||||
|
|
|
||||||
18
ROADMAP.md
|
|
@ -3,7 +3,8 @@
|
||||||
## 현재 상태
|
## 현재 상태
|
||||||
|
|
||||||
- Alpha 가입, 대화 목록, 대화창, 텍스트 전송이 동작하는 첫 사용 가능 프로토타입 확보
|
- Alpha 가입, 대화 목록, 대화창, 텍스트 전송이 동작하는 첫 사용 가능 프로토타입 확보
|
||||||
- Windows x64 portable build 생성 가능
|
- Windows x64 installer / onefile / zip 생성 가능
|
||||||
|
- Android APK alpha 기준선 생성 가능
|
||||||
- `vstalk.phy.kr` 모바일 웹앱과 API를 VPS에 실제 배포
|
- `vstalk.phy.kr` 모바일 웹앱과 API를 VPS에 실제 배포
|
||||||
- 원격 저장소에 최신 기준 스크린샷 포함 시작
|
- 원격 저장소에 최신 기준 스크린샷 포함 시작
|
||||||
- `vstalk.phy.kr` 모바일 웹앱 MVP 빌드 및 same-origin API 검증 완료
|
- `vstalk.phy.kr` 모바일 웹앱 MVP 빌드 및 same-origin API 검증 완료
|
||||||
|
|
@ -15,9 +16,11 @@
|
||||||
- [x] 메시지 전송
|
- [x] 메시지 전송
|
||||||
- [x] 읽기 커서 갱신
|
- [x] 읽기 커서 갱신
|
||||||
- [x] Windows portable zip 생성
|
- [x] Windows portable zip 생성
|
||||||
|
- [x] Windows installer / onefile / zip 생성
|
||||||
- [x] 모바일 웹앱 PWA 셸/가입/대화/전송
|
- [x] 모바일 웹앱 PWA 셸/가입/대화/전송
|
||||||
- [x] VPS 공개 API 상시 구동
|
- [x] VPS 공개 API 상시 구동
|
||||||
- [x] `vstalk.phy.kr` same-origin 웹앱 배포
|
- [x] `vstalk.phy.kr` same-origin 웹앱 배포
|
||||||
|
- [x] Android WebView 셸과 첫 APK 생성
|
||||||
- [ ] 데스크톱 WebSocket 실시간 반영
|
- [ ] 데스크톱 WebSocket 실시간 반영
|
||||||
|
|
||||||
## v0.2 Collaboration Basics
|
## v0.2 Collaboration Basics
|
||||||
|
|
@ -30,12 +33,19 @@
|
||||||
|
|
||||||
## v0.2 Android First-class
|
## v0.2 Android First-class
|
||||||
|
|
||||||
- [ ] Android 셸/네비게이션 골격
|
- [x] Android 셸/네비게이션 골격
|
||||||
- [ ] Android 로그인/대화 목록/대화 진입 MVP
|
- [x] Android 로그인/대화 목록/대화 진입 MVP 기준선
|
||||||
- [ ] APK 서명 및 산출물 규칙 확정
|
- [ ] APK 서명 및 산출물 규칙 확정
|
||||||
- [ ] Windows/Android 동시 릴리즈 메타데이터 검증
|
- [ ] Windows/Android 동시 릴리즈 메타데이터 검증
|
||||||
- [ ] Forge Releases + VPS 미러 동시 게시
|
- [ ] Forge Releases + VPS 미러 동시 게시
|
||||||
|
|
||||||
|
## v0.3 Shared Client Expansion
|
||||||
|
|
||||||
|
- [ ] Avalonia 기반 공유 클라이언트 구조를 Android/iOS/Linux 관점으로 재정리
|
||||||
|
- [ ] iOS 배포 채널 준비 문서와 빌드 체인 검증
|
||||||
|
- [ ] Linux 패키징 기준선 확보
|
||||||
|
- [ ] Android WebView 셸에서 네이티브 공용 UI 단계로 이행할 범위 정의
|
||||||
|
|
||||||
## v0.2 Mobile Web Entry
|
## v0.2 Mobile Web Entry
|
||||||
|
|
||||||
- [x] `vstalk.phy.kr` 모바일 웹 IA와 핵심 사용자 흐름 확정
|
- [x] `vstalk.phy.kr` 모바일 웹 IA와 핵심 사용자 흐름 확정
|
||||||
|
|
@ -89,6 +99,6 @@
|
||||||
|
|
||||||
- 하나의 태그는 하나의 릴리즈 레코드를 뜻합니다.
|
- 하나의 태그는 하나의 릴리즈 레코드를 뜻합니다.
|
||||||
- 같은 버전 번호 아래에 Windows와 Android 자산을 함께 게시합니다.
|
- 같은 버전 번호 아래에 Windows와 Android 자산을 함께 게시합니다.
|
||||||
- 원격 저장소에는 최신 기준 스크린샷도 함께 포함합니다.
|
- 원격 저장소 릴리즈 Assets에는 실행 파일과 체크섬만 두고, 최신 스크린샷은 변경 노트 안에서 참조합니다.
|
||||||
- 다운로드 미러와 원격 Releases는 같은 자산 이름과 같은 노트를 기준으로 맞춥니다.
|
- 다운로드 미러와 원격 Releases는 같은 자산 이름과 같은 노트를 기준으로 맞춥니다.
|
||||||
- 모바일 웹은 설치형 산출물 대신 `https://vstalk.phy.kr`를 기준 진입점으로 관리합니다.
|
- 모바일 웹은 설치형 산출물 대신 `https://vstalk.phy.kr`를 기준 진입점으로 관리합니다.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
root * /srv/download
|
root * /srv/download
|
||||||
redir /windows /windows/latest 302
|
redir /windows /windows/latest 302
|
||||||
redir /android /android/latest 302
|
redir /android /android/latest 302
|
||||||
redir /windows/latest /latest/KoTalk-windows-x64.zip 302
|
redir /windows/latest /latest/windows/index.html 302
|
||||||
redir /android/latest /latest/KoTalk-android-universal.apk 302
|
redir /android/latest /latest/android/index.html 302
|
||||||
redir /latest /latest/version.json 302
|
redir /latest /latest/version.json 302
|
||||||
header {
|
header {
|
||||||
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
|
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@
|
||||||
- 현재 포함:
|
- 현재 포함:
|
||||||
- Windows 데스크톱 셸
|
- Windows 데스크톱 셸
|
||||||
- Windows 온보딩/대화 화면
|
- Windows 온보딩/대화 화면
|
||||||
|
- Android APK shell 목업
|
||||||
- `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면
|
- `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면
|
||||||
|
|
|
||||||
BIN
docs/assets/latest/kotalk-android-mockup.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
85
packaging/windows/KoTalkInstaller.nsi
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
Unicode True
|
||||||
|
ManifestDPIAware True
|
||||||
|
RequestExecutionLevel user
|
||||||
|
SetCompressor /SOLID lzma
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
|
||||||
|
!ifndef APP_NAME
|
||||||
|
!define APP_NAME "KoTalk"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef APP_COMPANY
|
||||||
|
!define APP_COMPANY "PHYSIA"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef APP_VERSION
|
||||||
|
!define APP_VERSION "0.1.0"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef SOURCE_DIR
|
||||||
|
!error "SOURCE_DIR define is required"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef OUTPUT_FILE
|
||||||
|
!error "OUTPUT_FILE define is required"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef APP_ICON
|
||||||
|
!define APP_ICON "branding/ico/kotalk.ico"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
!define MUI_ICON "${APP_ICON}"
|
||||||
|
!define MUI_UNICON "${APP_ICON}"
|
||||||
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\KoTalk.exe"
|
||||||
|
!define MUI_FINISHPAGE_RUN_TEXT "KoTalk 실행"
|
||||||
|
|
||||||
|
Name "${APP_NAME}"
|
||||||
|
OutFile "${OUTPUT_FILE}"
|
||||||
|
InstallDir "$LOCALAPPDATA\KoTalk"
|
||||||
|
InstallDirRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir"
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "Korean"
|
||||||
|
|
||||||
|
Section "Install"
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
File /r "${SOURCE_DIR}/*"
|
||||||
|
WriteUninstaller "$INSTDIR\Uninstall KoTalk.exe"
|
||||||
|
|
||||||
|
WriteRegStr HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir" "$INSTDIR"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayName" "${APP_NAME}"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "Publisher" "${APP_COMPANY}"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayVersion" "${APP_VERSION}"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "InstallLocation" "$INSTDIR"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayIcon" "$INSTDIR\KoTalk.exe"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "UninstallString" "$INSTDIR\Uninstall KoTalk.exe"
|
||||||
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoModify" 1
|
||||||
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoRepair" 1
|
||||||
|
|
||||||
|
CreateDirectory "$SMPROGRAMS\KoTalk"
|
||||||
|
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
|
||||||
|
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk" "$INSTDIR\Uninstall KoTalk.exe"
|
||||||
|
CreateShortcut "$DESKTOP\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
Delete "$DESKTOP\KoTalk.lnk"
|
||||||
|
Delete "$SMPROGRAMS\KoTalk\KoTalk.lnk"
|
||||||
|
Delete "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk"
|
||||||
|
RMDir "$SMPROGRAMS\KoTalk"
|
||||||
|
|
||||||
|
Delete "$INSTDIR\Uninstall KoTalk.exe"
|
||||||
|
RMDir /r "$INSTDIR"
|
||||||
|
|
||||||
|
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
|
||||||
|
DeleteRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}"
|
||||||
|
SectionEnd
|
||||||
|
|
@ -19,10 +19,14 @@ release-assets/
|
||||||
SHA256SUMS.txt
|
SHA256SUMS.txt
|
||||||
screenshots/
|
screenshots/
|
||||||
windows/
|
windows/
|
||||||
|
index.html
|
||||||
KoTalk-windows-x64.zip
|
KoTalk-windows-x64.zip
|
||||||
|
KoTalk-windows-x64-onefile.exe
|
||||||
|
KoTalk-windows-x64-installer.exe
|
||||||
SHA256SUMS.txt
|
SHA256SUMS.txt
|
||||||
version.json
|
version.json
|
||||||
android/
|
android/
|
||||||
|
index.html
|
||||||
KoTalk-android-universal.apk
|
KoTalk-android-universal.apk
|
||||||
SHA256SUMS.txt
|
SHA256SUMS.txt
|
||||||
version.json
|
version.json
|
||||||
|
|
@ -35,6 +39,8 @@ release-assets/
|
||||||
windows/
|
windows/
|
||||||
x64/
|
x64/
|
||||||
KoTalk-windows-x64-v0.2.0-alpha.1.zip
|
KoTalk-windows-x64-v0.2.0-alpha.1.zip
|
||||||
|
KoTalk-windows-x64-onefile-v0.2.0-alpha.1.exe
|
||||||
|
KoTalk-windows-x64-installer-v0.2.0-alpha.1.exe
|
||||||
SHA256SUMS.txt
|
SHA256SUMS.txt
|
||||||
android/
|
android/
|
||||||
universal/
|
universal/
|
||||||
|
|
@ -46,14 +52,16 @@ release-assets/
|
||||||
|
|
||||||
- 같은 버전은 같은 서버 API 계약과 같은 릴리즈 노트를 공유합니다.
|
- 같은 버전은 같은 서버 API 계약과 같은 릴리즈 노트를 공유합니다.
|
||||||
- Windows와 Android는 같은 태그 아래 병렬 산출물로 게시합니다.
|
- Windows와 Android는 같은 태그 아래 병렬 산출물로 게시합니다.
|
||||||
- Windows 기본 공개 형식은 `zip`, Android 기본 공개 형식은 `apk`입니다.
|
- Windows 기본 공개 형식은 `installer exe + onefile exe + zip`, Android 기본 공개 형식은 `apk`입니다.
|
||||||
- APK는 공개 채널에 올릴 때 반드시 서명본을 사용합니다.
|
- APK는 공개 채널에 올릴 때 반드시 서명본을 사용합니다.
|
||||||
- `latest/version.json`은 전체 플랫폼 상태를 담고, `latest/windows/version.json`, `latest/android/version.json`은 플랫폼별 상세 포인터를 담습니다.
|
- `latest/version.json`은 전체 플랫폼 상태를 담고, `latest/windows/version.json`, `latest/android/version.json`은 플랫폼별 상세 포인터를 담습니다.
|
||||||
|
- `screenshots/`는 다운로드 미러와 변경 노트에서 참조하는 정적 자산입니다. 공개 릴리즈 페이지의 Assets 첨부에는 넣지 않습니다.
|
||||||
|
- `RELEASE_NOTES.ko.md`도 자산 첨부 대신 릴리즈 본문으로 사용합니다.
|
||||||
|
|
||||||
## 다운로드 경로 규칙
|
## 다운로드 경로 규칙
|
||||||
|
|
||||||
- 최신 Windows: `https://download-vstalk.phy.kr/windows/latest`
|
- 최신 Windows landing: `https://download-vstalk.phy.kr/windows/latest`
|
||||||
- 최신 Android: `https://download-vstalk.phy.kr/android/latest`
|
- 최신 Android landing: `https://download-vstalk.phy.kr/android/latest`
|
||||||
- 전체 최신 메타데이터: `https://download-vstalk.phy.kr/latest/version.json`
|
- 전체 최신 메타데이터: `https://download-vstalk.phy.kr/latest/version.json`
|
||||||
- 버전별 Windows: `https://download-vstalk.phy.kr/releases/<version>/windows/x64/...`
|
- 버전별 Windows: `https://download-vstalk.phy.kr/releases/<version>/windows/x64/...`
|
||||||
- 버전별 Android: `https://download-vstalk.phy.kr/releases/<version>/android/universal/...`
|
- 버전별 Android: `https://download-vstalk.phy.kr/releases/<version>/android/universal/...`
|
||||||
|
|
@ -69,6 +77,8 @@ release-assets/
|
||||||
--version v0.2.0-alpha.1 \
|
--version v0.2.0-alpha.1 \
|
||||||
--channel alpha \
|
--channel alpha \
|
||||||
--windows-zip artifacts/release/KoTalk-windows-x64-v0.2.0-alpha.1.zip \
|
--windows-zip artifacts/release/KoTalk-windows-x64-v0.2.0-alpha.1.zip \
|
||||||
|
--windows-portable-exe artifacts/release/KoTalk-windows-x64-onefile-v0.2.0-alpha.1.exe \
|
||||||
|
--windows-installer-exe artifacts/release/KoTalk-windows-x64-installer-v0.2.0-alpha.1.exe \
|
||||||
--android-apk artifacts/release/KoTalk-android-universal-v0.2.0-alpha.1.apk \
|
--android-apk artifacts/release/KoTalk-android-universal-v0.2.0-alpha.1.apk \
|
||||||
--screenshots artifacts/screenshots \
|
--screenshots artifacts/screenshots \
|
||||||
--force
|
--force
|
||||||
|
|
@ -87,11 +97,14 @@ release-assets/
|
||||||
- Forge Releases: 버전별 원본 보관
|
- Forge Releases: 버전별 원본 보관
|
||||||
- 다운로드 미러: 최신 포인터와 빠른 정적 다운로드
|
- 다운로드 미러: 최신 포인터와 빠른 정적 다운로드
|
||||||
- 모바일 웹앱: `release-assets/`가 아니라 `vstalk.phy.kr` 배포 트랙에서 별도 운영
|
- 모바일 웹앱: `release-assets/`가 아니라 `vstalk.phy.kr` 배포 트랙에서 별도 운영
|
||||||
- 공개 원격 릴리즈 페이지에는 ZIP/APK뿐 아니라 `screenshots/` 아래 최신 캡처도 함께 게시합니다.
|
- 공개 원격 릴리즈 페이지 Assets는 EXE/ZIP/APK, 최상위 `SHA256SUMS.txt`, 최상위 `version.json`만 유지합니다.
|
||||||
|
- 최신 스크린샷은 `release-assets/releases/<version>/screenshots/`에 보관하고, 릴리즈 노트 본문에서 직접 참조합니다.
|
||||||
|
|
||||||
## 운영 메모
|
## 운영 메모
|
||||||
|
|
||||||
- 생성된 버전별 산출물은 워크트리에 유지하며, 최신 로컬 검수와 서버 업로드 기준으로 사용합니다.
|
- 생성된 버전별 산출물은 워크트리에 유지하며, 최신 로컬 검수와 서버 업로드 기준으로 사용합니다.
|
||||||
- 공개 릴리즈마다 `RELEASE_NOTES.ko.md`, `SHA256SUMS.txt`, `version.json`을 함께 갱신합니다.
|
- 공개 릴리즈마다 `RELEASE_NOTES.ko.md`, `SHA256SUMS.txt`, `version.json`을 함께 갱신합니다.
|
||||||
- 같은 버전에서 Windows만 있고 Android가 아직 없을 수는 있지만, 장기 원칙은 `같은 버전 아래 두 플랫폼 병렬 게시`입니다.
|
- 같은 버전에서 Windows만 있고 Android가 아직 없을 수는 있지만, 장기 원칙은 `같은 버전 아래 두 플랫폼 병렬 게시`입니다.
|
||||||
|
- Windows latest landing은 설치형, onefile, 압축본을 동시에 노출합니다.
|
||||||
|
- Android latest landing은 APK 하나만 직접 노출하고, iOS는 저장소 Assets에 포함하지 않습니다.
|
||||||
- 모바일 웹앱 정적 산출물은 `release-assets/`가 아니라 `/srv/vs-messanger/webapp/releases/<version>`에 배포합니다.
|
- 모바일 웹앱 정적 산출물은 `release-assets/`가 아니라 `/srv/vs-messanger/webapp/releases/<version>`에 배포합니다.
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
## 이번 빌드에 포함된 것
|
## 이번 빌드에 포함된 것
|
||||||
|
|
||||||
|
- Windows x64 installer exe
|
||||||
|
- Windows x64 onefile portable exe
|
||||||
- Windows x64 portable zip
|
- Windows x64 portable zip
|
||||||
- Android universal apk
|
- Android universal apk
|
||||||
- 무결성 체크용 `SHA256SUMS.txt`
|
- 무결성 체크용 `SHA256SUMS.txt`
|
||||||
- 버전 메타데이터 `version.json`, `latest.json`
|
- 버전 메타데이터 `version.json`, `latest.json`
|
||||||
- 필요 시 한국어 스크린샷
|
- 최신 스크린샷은 이 변경 노트 하단에서 직접 확인할 수 있습니다.
|
||||||
|
|
||||||
## 확인할 것
|
## 확인할 것
|
||||||
|
|
||||||
|
|
|
||||||
50
scripts/branding/generate_android_icon_assets.py
Executable file
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SOURCE = ROOT / "branding" / "png" / "kotalk-transparent-1024.png"
|
||||||
|
ANDROID_RES = ROOT / "src" / "PhysOn.Mobile.Android" / "Resources"
|
||||||
|
|
||||||
|
BACKGROUND = (247, 243, 238, 255)
|
||||||
|
ICON_SIZES = {
|
||||||
|
"mipmap-mdpi": 48,
|
||||||
|
"mipmap-hdpi": 72,
|
||||||
|
"mipmap-xhdpi": 96,
|
||||||
|
"mipmap-xxhdpi": 144,
|
||||||
|
"mipmap-xxxhdpi": 192,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resize_logo(size: int, scale: float) -> Image.Image:
|
||||||
|
source = Image.open(SOURCE).convert("RGBA")
|
||||||
|
logo_size = max(8, round(size * scale))
|
||||||
|
logo = source.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
offset = ((size - logo_size) // 2, (size - logo_size) // 2)
|
||||||
|
canvas.alpha_composite(logo, offset)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def render_assets() -> None:
|
||||||
|
for folder, size in ICON_SIZES.items():
|
||||||
|
directory = ANDROID_RES / folder
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
background = Image.new("RGBA", (size, size), BACKGROUND)
|
||||||
|
foreground = resize_logo(size, 0.74)
|
||||||
|
full_icon = background.copy()
|
||||||
|
full_icon.alpha_composite(resize_logo(size, 0.64))
|
||||||
|
|
||||||
|
background.save(directory / "appicon_background.png")
|
||||||
|
foreground.save(directory / "appicon_foreground.png")
|
||||||
|
full_icon.save(directory / "appicon.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
render_assets()
|
||||||
119
scripts/release/build-android-apk.sh
Executable file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/release/build-android-apk.sh --version 2026.04.16-alpha.6 [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--configuration <name> Build configuration. Default: Release
|
||||||
|
--output <dir> Output directory. Default: artifacts/builds/<version>
|
||||||
|
--dotnet <path> Explicit dotnet binary path
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
version=""
|
||||||
|
configuration="Release"
|
||||||
|
output_dir=""
|
||||||
|
dotnet_bin="${DOTNET_BIN:-}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
version="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--configuration)
|
||||||
|
configuration="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
output_dir="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dotnet)
|
||||||
|
dotnet_bin="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
if [[ -z "$dotnet_bin" ]]; then
|
||||||
|
if command -v dotnet >/dev/null 2>&1; then
|
||||||
|
dotnet_bin="$(command -v dotnet)"
|
||||||
|
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||||
|
dotnet_bin="$HOME/.dotnet/dotnet"
|
||||||
|
else
|
||||||
|
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${JAVA_HOME:-}" && -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then
|
||||||
|
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||||
|
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$output_dir" ]]; then
|
||||||
|
output_dir="$repo_root/artifacts/builds/$version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish_dir="$output_dir/android/publish"
|
||||||
|
apk_path="$output_dir/KoTalk-android-universal-$version.apk"
|
||||||
|
|
||||||
|
rm -rf "$publish_dir"
|
||||||
|
mkdir -p "$publish_dir" "$output_dir"
|
||||||
|
|
||||||
|
if [[ ! -d "$ANDROID_SDK_ROOT/platforms" ]]; then
|
||||||
|
mkdir -p "$ANDROID_SDK_ROOT"
|
||||||
|
"$dotnet_bin" build "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
|
||||||
|
-t:InstallAndroidDependencies \
|
||||||
|
-f net8.0-android \
|
||||||
|
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
|
||||||
|
-p:JavaSdkDirectory="${JAVA_HOME:-}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$dotnet_bin" publish "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
|
||||||
|
-c "$configuration" \
|
||||||
|
-f net8.0-android \
|
||||||
|
-p:AndroidPackageFormat=apk \
|
||||||
|
-p:AndroidKeyStore=false \
|
||||||
|
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
|
||||||
|
-p:JavaSdkDirectory="${JAVA_HOME:-}" \
|
||||||
|
-o "$publish_dir"
|
||||||
|
|
||||||
|
apk_source="$(find "$publish_dir" -type f -name '*.apk' | head -n 1)"
|
||||||
|
if [[ -z "$apk_source" ]]; then
|
||||||
|
echo "Android publish did not produce an APK." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$apk_source" "$apk_path"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$output_dir"
|
||||||
|
sha256sum "$(basename "$apk_path")" > KoTalk-android-universal-$version.apk.sha256
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Built Android APK in $output_dir"
|
||||||
130
scripts/release/build-windows-distributions.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/release/build-windows-distributions.sh --version 2026.04.16-alpha.6 [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--configuration <name> Build configuration. Default: Release
|
||||||
|
--runtime <rid> Runtime identifier. Default: win-x64
|
||||||
|
--output <dir> Output directory. Default: artifacts/builds/<version>
|
||||||
|
--dotnet <path> Explicit dotnet binary path
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
version=""
|
||||||
|
configuration="Release"
|
||||||
|
runtime="win-x64"
|
||||||
|
output_dir=""
|
||||||
|
dotnet_bin="${DOTNET_BIN:-}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
version="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--configuration)
|
||||||
|
configuration="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--runtime)
|
||||||
|
runtime="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
output_dir="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dotnet)
|
||||||
|
dotnet_bin="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
if [[ -z "$dotnet_bin" ]]; then
|
||||||
|
if command -v dotnet >/dev/null 2>&1; then
|
||||||
|
dotnet_bin="$(command -v dotnet)"
|
||||||
|
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||||
|
dotnet_bin="$HOME/.dotnet/dotnet"
|
||||||
|
else
|
||||||
|
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$output_dir" ]]; then
|
||||||
|
output_dir="$repo_root/artifacts/builds/$version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish_dir="$output_dir/publish/$runtime"
|
||||||
|
onefile_dir="$output_dir/onefile/$runtime"
|
||||||
|
zip_path="$output_dir/KoTalk-windows-x64-$version.zip"
|
||||||
|
portable_exe_path="$output_dir/KoTalk-windows-x64-onefile-$version.exe"
|
||||||
|
installer_path="$output_dir/KoTalk-windows-x64-installer-$version.exe"
|
||||||
|
installer_script="$repo_root/packaging/windows/KoTalkInstaller.nsi"
|
||||||
|
app_icon="$repo_root/branding/ico/kotalk.ico"
|
||||||
|
|
||||||
|
rm -rf "$publish_dir" "$onefile_dir"
|
||||||
|
mkdir -p "$publish_dir" "$onefile_dir" "$output_dir"
|
||||||
|
|
||||||
|
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
|
||||||
|
-c "$configuration" \
|
||||||
|
-r "$runtime" \
|
||||||
|
--self-contained true \
|
||||||
|
-p:DebugSymbols=false \
|
||||||
|
-p:DebugType=None \
|
||||||
|
-o "$publish_dir"
|
||||||
|
|
||||||
|
rm -f "$zip_path"
|
||||||
|
(cd "$publish_dir" && zip -rq "$zip_path" .)
|
||||||
|
|
||||||
|
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
|
||||||
|
-c "$configuration" \
|
||||||
|
-r "$runtime" \
|
||||||
|
--self-contained true \
|
||||||
|
-p:PublishSingleFile=true \
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||||
|
-p:EnableCompressionInSingleFile=true \
|
||||||
|
-p:DebugSymbols=false \
|
||||||
|
-p:DebugType=None \
|
||||||
|
-o "$onefile_dir"
|
||||||
|
|
||||||
|
cp "$onefile_dir/KoTalk.exe" "$portable_exe_path"
|
||||||
|
|
||||||
|
makensis \
|
||||||
|
-DAPP_VERSION="$version" \
|
||||||
|
-DAPP_ICON="$app_icon" \
|
||||||
|
-DSOURCE_DIR="$publish_dir" \
|
||||||
|
-DOUTPUT_FILE="$installer_path" \
|
||||||
|
"$installer_script" >/dev/null
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$output_dir"
|
||||||
|
sha256sum \
|
||||||
|
"$(basename "$zip_path")" \
|
||||||
|
"$(basename "$portable_exe_path")" \
|
||||||
|
"$(basename "$installer_path")" \
|
||||||
|
> SHA256SUMS.txt
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Built Windows release assets in $output_dir"
|
||||||
|
|
@ -8,6 +8,10 @@ Usage:
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--windows-zip <path> Windows x64 ZIP artifact path
|
--windows-zip <path> Windows x64 ZIP artifact path
|
||||||
|
--windows-portable-exe <path>
|
||||||
|
Windows onefile portable EXE artifact path
|
||||||
|
--windows-installer-exe <path>
|
||||||
|
Windows installer EXE artifact path
|
||||||
--android-apk <path> Android universal APK artifact path
|
--android-apk <path> Android universal APK artifact path
|
||||||
--zip <path> Backward-compatible alias for --windows-zip
|
--zip <path> Backward-compatible alias for --windows-zip
|
||||||
--channel <name> Release channel. Default: alpha
|
--channel <name> Release channel. Default: alpha
|
||||||
|
|
@ -23,6 +27,8 @@ EOF
|
||||||
version=""
|
version=""
|
||||||
channel="alpha"
|
channel="alpha"
|
||||||
windows_zip=""
|
windows_zip=""
|
||||||
|
windows_portable_exe=""
|
||||||
|
windows_installer_exe=""
|
||||||
android_apk=""
|
android_apk=""
|
||||||
notes_path=""
|
notes_path=""
|
||||||
screenshots_dir=""
|
screenshots_dir=""
|
||||||
|
|
@ -42,6 +48,14 @@ while [[ $# -gt 0 ]]; do
|
||||||
windows_zip="${2:-}"
|
windows_zip="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--windows-portable-exe)
|
||||||
|
windows_portable_exe="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--windows-installer-exe)
|
||||||
|
windows_installer_exe="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--android-apk)
|
--android-apk)
|
||||||
android_apk="${2:-}"
|
android_apk="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -75,8 +89,8 @@ if [[ -z "$version" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$windows_zip" && -z "$android_apk" ]]; then
|
if [[ -z "$windows_zip" && -z "$windows_portable_exe" && -z "$windows_installer_exe" && -z "$android_apk" ]]; then
|
||||||
echo "At least one artifact must be provided: --windows-zip or --android-apk" >&2
|
echo "At least one artifact must be provided for Windows or Android." >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -86,6 +100,16 @@ if [[ -n "$windows_zip" && ! -f "$windows_zip" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$windows_portable_exe" && ! -f "$windows_portable_exe" ]]; then
|
||||||
|
echo "Windows portable EXE artifact not found: $windows_portable_exe" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$windows_installer_exe" && ! -f "$windows_installer_exe" ]]; then
|
||||||
|
echo "Windows installer EXE artifact not found: $windows_installer_exe" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$android_apk" && ! -f "$android_apk" ]]; then
|
if [[ -n "$android_apk" && ! -f "$android_apk" ]]; then
|
||||||
echo "Android APK artifact not found: $android_apk" >&2
|
echo "Android APK artifact not found: $android_apk" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -146,6 +170,31 @@ else
|
||||||
fi
|
fi
|
||||||
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
|
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
|
||||||
|
|
||||||
|
append_screenshot_gallery() {
|
||||||
|
local notes_file="$1"
|
||||||
|
local screenshot_root_url="$2"
|
||||||
|
|
||||||
|
if [[ -z "$screenshots_dir" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t note_screenshots < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
|
||||||
|
if [[ ${#note_screenshots[@]} -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '\n## 최신 화면\n\n'
|
||||||
|
printf '스크린샷은 릴리즈 Assets 대신 변경 노트 안에서 직접 확인할 수 있도록 정리합니다.\n\n'
|
||||||
|
for screenshot in "${note_screenshots[@]}"; do
|
||||||
|
name="$(basename "$screenshot")"
|
||||||
|
label="${name%.*}"
|
||||||
|
printf '### %s\n\n' "$label"
|
||||||
|
printf '\n\n' "$label" "$screenshot_root_url" "$name"
|
||||||
|
done
|
||||||
|
} >> "$notes_file"
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -n "$screenshots_dir" ]]; then
|
if [[ -n "$screenshots_dir" ]]; then
|
||||||
while IFS= read -r screenshot; do
|
while IFS= read -r screenshot; do
|
||||||
cp "$screenshot" "$release_root/screenshots/$(basename "$screenshot")"
|
cp "$screenshot" "$release_root/screenshots/$(basename "$screenshot")"
|
||||||
|
|
@ -153,6 +202,9 @@ if [[ -n "$screenshots_dir" ]]; then
|
||||||
done < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
|
done < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
append_screenshot_gallery "$release_root/RELEASE_NOTES.ko.md" "$download_base_url/releases/$version/screenshots"
|
||||||
|
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
|
||||||
|
|
||||||
platform_count=0
|
platform_count=0
|
||||||
platforms_json=""
|
platforms_json=""
|
||||||
top_level_windows_alias=""
|
top_level_windows_alias=""
|
||||||
|
|
@ -168,6 +220,23 @@ append_platform_json() {
|
||||||
platform_count=$((platform_count + 1))
|
platform_count=$((platform_count + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
join_json_lines() {
|
||||||
|
local result=""
|
||||||
|
local total=$#
|
||||||
|
local index=0
|
||||||
|
local line=""
|
||||||
|
|
||||||
|
for line in "$@"; do
|
||||||
|
index=$((index + 1))
|
||||||
|
result+="$line"
|
||||||
|
if (( index < total )); then
|
||||||
|
result+=$',\n'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s' "$result"
|
||||||
|
}
|
||||||
|
|
||||||
write_platform_version_json() {
|
write_platform_version_json() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
local body="$2"
|
local body="$2"
|
||||||
|
|
@ -187,38 +256,69 @@ $body
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ -n "$windows_zip" ]]; then
|
if [[ -n "$windows_zip" || -n "$windows_portable_exe" || -n "$windows_installer_exe" ]]; then
|
||||||
windows_release_name="KoTalk-windows-x64-$version.zip"
|
|
||||||
windows_latest_name="KoTalk-windows-x64.zip"
|
|
||||||
windows_release_dir="$release_root/windows/x64"
|
windows_release_dir="$release_root/windows/x64"
|
||||||
windows_latest_dir="$latest_root/windows"
|
windows_latest_dir="$latest_root/windows"
|
||||||
mkdir -p "$windows_release_dir" "$windows_latest_dir"
|
mkdir -p "$windows_release_dir" "$windows_latest_dir"
|
||||||
|
|
||||||
|
windows_release_hash_items=()
|
||||||
|
windows_latest_hash_items=()
|
||||||
|
windows_platform_lines=(
|
||||||
|
' "name": "KoTalk for Windows"'
|
||||||
|
' "kind": "desktop"'
|
||||||
|
' "arch": "x64"'
|
||||||
|
" \"landingUrl\": \"$download_base_url/windows/latest\""
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$windows_zip" ]]; then
|
||||||
|
windows_release_name="KoTalk-windows-x64-$version.zip"
|
||||||
|
windows_latest_name="KoTalk-windows-x64.zip"
|
||||||
cp "$windows_zip" "$windows_release_dir/$windows_release_name"
|
cp "$windows_zip" "$windows_release_dir/$windows_release_name"
|
||||||
cp "$windows_zip" "$windows_latest_dir/$windows_latest_name"
|
cp "$windows_zip" "$windows_latest_dir/$windows_latest_name"
|
||||||
|
release_hash_paths+=("windows/x64/$windows_release_name")
|
||||||
|
latest_hash_paths+=("windows/$windows_latest_name")
|
||||||
|
windows_release_hash_items+=("$windows_release_name")
|
||||||
|
windows_latest_hash_items+=("$windows_latest_name")
|
||||||
|
windows_platform_lines+=(" \"portableZipUrl\": \"$download_base_url/windows/latest/$windows_latest_name\"")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$windows_portable_exe" ]]; then
|
||||||
|
windows_onefile_release_name="KoTalk-windows-x64-onefile-$version.exe"
|
||||||
|
windows_onefile_latest_name="KoTalk-windows-x64-onefile.exe"
|
||||||
|
cp "$windows_portable_exe" "$windows_release_dir/$windows_onefile_release_name"
|
||||||
|
cp "$windows_portable_exe" "$windows_latest_dir/$windows_onefile_latest_name"
|
||||||
|
release_hash_paths+=("windows/x64/$windows_onefile_release_name")
|
||||||
|
latest_hash_paths+=("windows/$windows_onefile_latest_name")
|
||||||
|
windows_release_hash_items+=("$windows_onefile_release_name")
|
||||||
|
windows_latest_hash_items+=("$windows_onefile_latest_name")
|
||||||
|
windows_platform_lines+=(" \"portableExeUrl\": \"$download_base_url/windows/latest/$windows_onefile_latest_name\"")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$windows_installer_exe" ]]; then
|
||||||
|
windows_installer_release_name="KoTalk-windows-x64-installer-$version.exe"
|
||||||
|
windows_installer_latest_name="KoTalk-windows-x64-installer.exe"
|
||||||
|
cp "$windows_installer_exe" "$windows_release_dir/$windows_installer_release_name"
|
||||||
|
cp "$windows_installer_exe" "$windows_latest_dir/$windows_installer_latest_name"
|
||||||
|
release_hash_paths+=("windows/x64/$windows_installer_release_name")
|
||||||
|
latest_hash_paths+=("windows/$windows_installer_latest_name")
|
||||||
|
windows_release_hash_items+=("$windows_installer_release_name")
|
||||||
|
windows_latest_hash_items+=("$windows_installer_latest_name")
|
||||||
|
windows_platform_lines+=(" \"installerUrl\": \"$download_base_url/windows/latest/$windows_installer_latest_name\"")
|
||||||
|
fi
|
||||||
|
|
||||||
|
windows_platform_lines+=(" \"sha256Url\": \"$download_base_url/windows/latest/SHA256SUMS.txt\"")
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "$windows_release_dir"
|
cd "$windows_release_dir"
|
||||||
sha256sum "$windows_release_name" > SHA256SUMS.txt
|
sha256sum "${windows_release_hash_items[@]}" > SHA256SUMS.txt
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "$windows_latest_dir"
|
cd "$windows_latest_dir"
|
||||||
sha256sum "$windows_latest_name" > SHA256SUMS.txt
|
sha256sum "${windows_latest_hash_items[@]}" > SHA256SUMS.txt
|
||||||
)
|
)
|
||||||
|
|
||||||
release_hash_paths+=("windows/x64/$windows_release_name")
|
windows_platform_body="$(join_json_lines "${windows_platform_lines[@]}")"
|
||||||
latest_hash_paths+=("windows/$windows_latest_name")
|
|
||||||
|
|
||||||
windows_platform_body="$(cat <<EOF
|
|
||||||
"name": "KoTalk for Windows",
|
|
||||||
"kind": "desktop",
|
|
||||||
"arch": "x64",
|
|
||||||
"latestUrl": "$download_base_url/windows/latest",
|
|
||||||
"portableZipUrl": "$download_base_url/windows/latest/$windows_latest_name",
|
|
||||||
"sha256Url": "$download_base_url/windows/latest/SHA256SUMS.txt"
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
|
|
||||||
append_platform_json "$(cat <<EOF
|
append_platform_json "$(cat <<EOF
|
||||||
"windows": {
|
"windows": {
|
||||||
|
|
@ -298,12 +398,12 @@ if (( ${#latest_hash_paths[@]} > 0 )); then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
windows_landing_card=""
|
windows_landing_card=""
|
||||||
if [[ -n "$windows_zip" ]]; then
|
if [[ -n "$windows_zip" || -n "$windows_portable_exe" || -n "$windows_installer_exe" ]]; then
|
||||||
windows_landing_card="$(cat <<EOF
|
windows_landing_card="$(cat <<EOF
|
||||||
<a class="card" href="/windows/latest">
|
<a class="card" href="/windows/latest">
|
||||||
<span class="eyebrow">Windows</span>
|
<span class="eyebrow">Windows</span>
|
||||||
<strong>Latest Windows build</strong>
|
<strong>Latest Windows build</strong>
|
||||||
<span>ZIP package and SHA256 checksum</span>
|
<span>Installer, onefile portable, ZIP, SHA256</span>
|
||||||
</a>
|
</a>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -321,6 +421,156 @@ EOF
|
||||||
)"
|
)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
windows_installer_card=""
|
||||||
|
if [[ -n "$windows_installer_exe" ]]; then
|
||||||
|
windows_installer_card="$(cat <<EOF
|
||||||
|
<a class="card" href="/windows/latest/KoTalk-windows-x64-installer.exe">
|
||||||
|
<span class="eyebrow">Installer</span>
|
||||||
|
<strong>Windows installer</strong>
|
||||||
|
<span>설치형 EXE</span>
|
||||||
|
</a>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
windows_portable_card=""
|
||||||
|
if [[ -n "$windows_portable_exe" ]]; then
|
||||||
|
windows_portable_card="$(cat <<EOF
|
||||||
|
<a class="card" href="/windows/latest/KoTalk-windows-x64-onefile.exe">
|
||||||
|
<span class="eyebrow">Portable</span>
|
||||||
|
<strong>Onefile executable</strong>
|
||||||
|
<span>압축 해제 없이 바로 실행</span>
|
||||||
|
</a>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
windows_zip_card=""
|
||||||
|
if [[ -n "$windows_zip" ]]; then
|
||||||
|
windows_zip_card="$(cat <<EOF
|
||||||
|
<a class="card" href="/windows/latest/KoTalk-windows-x64.zip">
|
||||||
|
<span class="eyebrow">Archive</span>
|
||||||
|
<strong>Extracted bundle ZIP</strong>
|
||||||
|
<span>폴더 배포본 압축 파일</span>
|
||||||
|
</a>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${latest_root}/windows" ]]; then
|
||||||
|
cat > "${latest_root}/windows/index.html" <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>KoTalk for Windows</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f7f3ee;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--border: #ddd1c4;
|
||||||
|
--text: #20242b;
|
||||||
|
--soft: #5f5a54;
|
||||||
|
--accent: #f05b2b;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
|
||||||
|
main { max-width: 920px; margin: 0 auto; padding: 40px 20px 56px; }
|
||||||
|
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
|
||||||
|
.hero { padding: 24px; }
|
||||||
|
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
|
||||||
|
p { margin: 0; line-height: 1.6; color: var(--soft); }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
|
||||||
|
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
|
||||||
|
.card:hover { border-color: var(--accent); }
|
||||||
|
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
|
||||||
|
.meta { margin-top: 16px; color: var(--soft); }
|
||||||
|
.meta a { color: inherit; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section class="hero">
|
||||||
|
<h1>KoTalk for Windows</h1>
|
||||||
|
<p>설치형과 onefile portable, 압축본을 같은 기준선으로 제공합니다.</p>
|
||||||
|
<div class="grid">
|
||||||
|
$windows_installer_card
|
||||||
|
$windows_portable_card
|
||||||
|
$windows_zip_card
|
||||||
|
<a class="card" href="/windows/latest/SHA256SUMS.txt">
|
||||||
|
<span class="eyebrow">Integrity</span>
|
||||||
|
<strong>SHA256SUMS</strong>
|
||||||
|
<span>체크섬 확인</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="meta"><a href="/latest/version.json">version.json</a></p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${latest_root}/android" ]]; then
|
||||||
|
cat > "${latest_root}/android/index.html" <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>KoTalk for Android</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f7f3ee;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--border: #ddd1c4;
|
||||||
|
--text: #20242b;
|
||||||
|
--soft: #5f5a54;
|
||||||
|
--accent: #f05b2b;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
|
||||||
|
main { max-width: 760px; margin: 0 auto; padding: 40px 20px 56px; }
|
||||||
|
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
|
||||||
|
.hero { padding: 24px; }
|
||||||
|
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
|
||||||
|
p { margin: 0; line-height: 1.6; color: var(--soft); }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
|
||||||
|
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
|
||||||
|
.card:hover { border-color: var(--accent); }
|
||||||
|
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
|
||||||
|
.meta { margin-top: 16px; color: var(--soft); }
|
||||||
|
.meta a { color: inherit; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section class="hero">
|
||||||
|
<h1>KoTalk for Android</h1>
|
||||||
|
<p>KoTalk Android shell APK와 버전 메타데이터를 같은 기준선으로 제공합니다.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<a class="card" href="/android/latest/KoTalk-android-universal.apk">
|
||||||
|
<span class="eyebrow">APK</span>
|
||||||
|
<strong>Universal APK</strong>
|
||||||
|
<span>직접 설치용 패키지</span>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="/android/latest/SHA256SUMS.txt">
|
||||||
|
<span class="eyebrow">Integrity</span>
|
||||||
|
<strong>SHA256SUMS</strong>
|
||||||
|
<span>체크섬 확인</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="meta"><a href="/latest/version.json">version.json</a></p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
cat > "$download_root/index.html" <<EOF
|
cat > "$download_root/index.html" <<EOF
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,11 @@ case "$version" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
mapfile -t asset_files < <(
|
mapfile -t asset_files < <(
|
||||||
find "$release_root" -type f \
|
{
|
||||||
\( -name '*.zip' -o -name '*.apk' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
|
find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \)
|
||||||
| sort
|
[[ -f "$release_root/SHA256SUMS.txt" ]] && printf '%s\n' "$release_root/SHA256SUMS.txt"
|
||||||
|
[[ -f "$release_root/version.json" ]] && printf '%s\n' "$release_root/version.json"
|
||||||
|
} | sort
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ ${#asset_files[@]} -eq 0 ]]; then
|
if [[ ${#asset_files[@]} -eq 0 ]]; then
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,11 @@ case "$version" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
mapfile -t asset_files < <(
|
mapfile -t asset_files < <(
|
||||||
find "$release_root" -type f \
|
{
|
||||||
\( -name '*.zip' -o -name '*.apk' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
|
find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \)
|
||||||
| sort
|
[[ -f "$release_root/SHA256SUMS.txt" ]] && printf '%s\n' "$release_root/SHA256SUMS.txt"
|
||||||
|
[[ -f "$release_root/version.json" ]] && printf '%s\n' "$release_root/version.json"
|
||||||
|
} | sort
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ ${#asset_files[@]} -eq 0 ]]; then
|
if [[ ${#asset_files[@]} -eq 0 ]]; then
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
<Product>KoTalk</Product>
|
<Product>KoTalk</Product>
|
||||||
<Description>한국어 중심의 차분한 메시징 경험을 다시 설계하는 Windows-first 메신저</Description>
|
<Description>한국어 중심의 차분한 메시징 경험을 다시 설계하는 Windows-first 메신저</Description>
|
||||||
<AssemblyTitle>KoTalk</AssemblyTitle>
|
<AssemblyTitle>KoTalk</AssemblyTitle>
|
||||||
<AssemblyVersion>0.1.0.5</AssemblyVersion>
|
<AssemblyVersion>0.1.0.6</AssemblyVersion>
|
||||||
<FileVersion>0.1.0.5</FileVersion>
|
<FileVersion>0.1.0.6</FileVersion>
|
||||||
<Version>0.1.0-alpha.5</Version>
|
<Version>0.1.0-alpha.6</Version>
|
||||||
<InformationalVersion>0.1.0-alpha.5</InformationalVersion>
|
<InformationalVersion>0.1.0-alpha.6</InformationalVersion>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable
|
||||||
$"desktop-{Environment.MachineName.ToLowerInvariant()}",
|
$"desktop-{Environment.MachineName.ToLowerInvariant()}",
|
||||||
"windows",
|
"windows",
|
||||||
Environment.MachineName,
|
Environment.MachineName,
|
||||||
"0.1.0-alpha.5"));
|
"0.1.0-alpha.6"));
|
||||||
|
|
||||||
var response = await _apiClient.RegisterAlphaQuickAsync(apiBaseUrl, request, CancellationToken.None);
|
var response = await _apiClient.RegisterAlphaQuickAsync(apiBaseUrl, request, CancellationToken.None);
|
||||||
ApiBaseUrl = apiBaseUrl;
|
ApiBaseUrl = apiBaseUrl;
|
||||||
|
|
|
||||||
14
src/PhysOn.Mobile.Android/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="false"
|
||||||
|
android:icon="@mipmap/appicon"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:roundIcon="@mipmap/appicon_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="false">
|
||||||
|
</application>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
||||||
268
src/PhysOn.Mobile.Android/MainActivity.cs
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.Graphics;
|
||||||
|
using Android.Net;
|
||||||
|
using Android.Net.Http;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Views;
|
||||||
|
using Android.Webkit;
|
||||||
|
using Android.Widget;
|
||||||
|
|
||||||
|
namespace PhysOn.Mobile.Android;
|
||||||
|
|
||||||
|
[Activity(
|
||||||
|
Label = "@string/app_name",
|
||||||
|
MainLauncher = true,
|
||||||
|
Exported = true,
|
||||||
|
LaunchMode = LaunchMode.SingleTask,
|
||||||
|
Theme = "@style/KoTalkTheme",
|
||||||
|
ConfigurationChanges =
|
||||||
|
ConfigChanges.Orientation |
|
||||||
|
ConfigChanges.ScreenSize |
|
||||||
|
ConfigChanges.SmallestScreenSize |
|
||||||
|
ConfigChanges.ScreenLayout |
|
||||||
|
ConfigChanges.UiMode |
|
||||||
|
ConfigChanges.KeyboardHidden |
|
||||||
|
ConfigChanges.Density)]
|
||||||
|
public class MainActivity : Activity
|
||||||
|
{
|
||||||
|
private const string AppVersion = "0.1.0-alpha.6";
|
||||||
|
private const string HomeUrl = "https://vstalk.phy.kr";
|
||||||
|
|
||||||
|
private static readonly HashSet<string> AllowedHosts = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"vstalk.phy.kr",
|
||||||
|
"download-vstalk.phy.kr"
|
||||||
|
};
|
||||||
|
|
||||||
|
private WebView? _webView;
|
||||||
|
private ProgressBar? _loadingBar;
|
||||||
|
private View? _offlineOverlay;
|
||||||
|
private ImageButton? _retryButton;
|
||||||
|
|
||||||
|
protected override void OnCreate(Bundle? savedInstanceState)
|
||||||
|
{
|
||||||
|
base.OnCreate(savedInstanceState);
|
||||||
|
ConfigureWindowChrome();
|
||||||
|
SetContentView(Resource.Layout.activity_main);
|
||||||
|
|
||||||
|
_webView = FindViewById<WebView>(Resource.Id.app_webview);
|
||||||
|
_loadingBar = FindViewById<ProgressBar>(Resource.Id.loading_bar);
|
||||||
|
_offlineOverlay = FindViewById<View>(Resource.Id.offline_overlay);
|
||||||
|
_retryButton = FindViewById<ImageButton>(Resource.Id.retry_button);
|
||||||
|
|
||||||
|
if (_webView is null || _loadingBar is null || _offlineOverlay is null || _retryButton is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("KoTalk Android shell layout failed to load.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_retryButton.Click += HandleRetryClick;
|
||||||
|
ConfigureWebView(_webView, _loadingBar);
|
||||||
|
|
||||||
|
if (savedInstanceState is not null)
|
||||||
|
{
|
||||||
|
_webView.RestoreState(savedInstanceState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_webView.LoadUrl(HomeUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSaveInstanceState(Bundle outState)
|
||||||
|
{
|
||||||
|
base.OnSaveInstanceState(outState);
|
||||||
|
_webView?.SaveState(outState);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_retryButton is not null)
|
||||||
|
{
|
||||||
|
_retryButton.Click -= HandleRetryClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_webView is not null)
|
||||||
|
{
|
||||||
|
_webView.StopLoading();
|
||||||
|
_webView.Destroy();
|
||||||
|
_webView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnBackPressed()
|
||||||
|
{
|
||||||
|
if (_webView?.CanGoBack() == true)
|
||||||
|
{
|
||||||
|
_webView.GoBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1422
|
||||||
|
base.OnBackPressed();
|
||||||
|
#pragma warning restore CA1422
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRetryClick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_webView?.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureWindowChrome()
|
||||||
|
{
|
||||||
|
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
|
||||||
|
|
||||||
|
if (Window is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window.SetStatusBarColor(Color.ParseColor("#F7F3EE"));
|
||||||
|
Window.SetNavigationBarColor(Color.ParseColor("#F7F3EE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureWebView(WebView webView, ProgressBar loadingBar)
|
||||||
|
{
|
||||||
|
WebView.SetWebContentsDebuggingEnabled(System.Diagnostics.Debugger.IsAttached);
|
||||||
|
|
||||||
|
var settings = webView.Settings!;
|
||||||
|
settings.JavaScriptEnabled = true;
|
||||||
|
settings.DomStorageEnabled = true;
|
||||||
|
settings.DatabaseEnabled = true;
|
||||||
|
settings.AllowFileAccess = false;
|
||||||
|
settings.AllowContentAccess = false;
|
||||||
|
settings.SetSupportZoom(false);
|
||||||
|
settings.BuiltInZoomControls = false;
|
||||||
|
settings.DisplayZoomControls = false;
|
||||||
|
settings.LoadWithOverviewMode = true;
|
||||||
|
settings.UseWideViewPort = true;
|
||||||
|
settings.MixedContentMode = MixedContentHandling.NeverAllow;
|
||||||
|
settings.CacheMode = CacheModes.Default;
|
||||||
|
settings.MediaPlaybackRequiresUserGesture = true;
|
||||||
|
settings.UserAgentString = $"{settings.UserAgentString} KoTalkAndroid/{AppVersion}";
|
||||||
|
|
||||||
|
var cookies = CookieManager.Instance;
|
||||||
|
cookies?.SetAcceptCookie(true);
|
||||||
|
cookies?.SetAcceptThirdPartyCookies(webView, false);
|
||||||
|
|
||||||
|
webView.SetBackgroundColor(Color.ParseColor("#F7F3EE"));
|
||||||
|
webView.SetWebChromeClient(new KoTalkWebChromeClient(loadingBar));
|
||||||
|
webView.SetWebViewClient(
|
||||||
|
new KoTalkWebViewClient(
|
||||||
|
AllowedHosts,
|
||||||
|
ShowOfflineOverlay,
|
||||||
|
HideOfflineOverlay));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowOfflineOverlay()
|
||||||
|
{
|
||||||
|
RunOnUiThread(() =>
|
||||||
|
{
|
||||||
|
if (_offlineOverlay is not null)
|
||||||
|
{
|
||||||
|
_offlineOverlay.Visibility = ViewStates.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_loadingBar is not null)
|
||||||
|
{
|
||||||
|
_loadingBar.Visibility = ViewStates.Invisible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideOfflineOverlay()
|
||||||
|
{
|
||||||
|
RunOnUiThread(() =>
|
||||||
|
{
|
||||||
|
if (_offlineOverlay is not null)
|
||||||
|
{
|
||||||
|
_offlineOverlay.Visibility = ViewStates.Gone;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class KoTalkWebChromeClient(ProgressBar loadingBar) : WebChromeClient
|
||||||
|
{
|
||||||
|
public override void OnProgressChanged(WebView? view, int newProgress)
|
||||||
|
{
|
||||||
|
base.OnProgressChanged(view, newProgress);
|
||||||
|
loadingBar.Progress = newProgress;
|
||||||
|
loadingBar.Visibility = newProgress >= 100 ? ViewStates.Invisible : ViewStates.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class KoTalkWebViewClient(
|
||||||
|
IReadOnlySet<string> allowedHosts,
|
||||||
|
Action showOfflineOverlay,
|
||||||
|
Action hideOfflineOverlay) : WebViewClient
|
||||||
|
{
|
||||||
|
public override bool ShouldOverrideUrlLoading(WebView? view, IWebResourceRequest? request)
|
||||||
|
{
|
||||||
|
if (request?.Url is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = request.Url;
|
||||||
|
var scheme = url.Scheme?.ToLowerInvariant();
|
||||||
|
var host = url.Host?.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (scheme is "http" or "https" && host is not null && allowedHosts.Contains(host))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view?.Context is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(url.ToString()));
|
||||||
|
intent.AddFlags(ActivityFlags.NewTask);
|
||||||
|
view.Context.StartActivity(intent);
|
||||||
|
}
|
||||||
|
catch (ActivityNotFoundException)
|
||||||
|
{
|
||||||
|
showOfflineOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
|
||||||
|
{
|
||||||
|
base.OnPageStarted(view, url, favicon);
|
||||||
|
hideOfflineOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnPageFinished(WebView? view, string? url)
|
||||||
|
{
|
||||||
|
base.OnPageFinished(view, url);
|
||||||
|
hideOfflineOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnReceivedError(
|
||||||
|
WebView? view,
|
||||||
|
IWebResourceRequest? request,
|
||||||
|
WebResourceError? error)
|
||||||
|
{
|
||||||
|
base.OnReceivedError(view, request, error);
|
||||||
|
|
||||||
|
if (request?.IsForMainFrame ?? true)
|
||||||
|
{
|
||||||
|
showOfflineOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnReceivedSslError(WebView? view, SslErrorHandler? handler, SslError? error)
|
||||||
|
{
|
||||||
|
handler?.Cancel();
|
||||||
|
showOfflineOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-android</TargetFramework>
|
||||||
|
<SupportedOSPlatformVersion>26</SupportedOSPlatformVersion>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>PhysOn.Mobile.Android</RootNamespace>
|
||||||
|
<AssemblyName>KoTalk.Mobile.Android</AssemblyName>
|
||||||
|
<ApplicationId>kr.physia.kotalk</ApplicationId>
|
||||||
|
<ApplicationVersion>6</ApplicationVersion>
|
||||||
|
<ApplicationDisplayVersion>0.1.0-alpha.6</ApplicationDisplayVersion>
|
||||||
|
<Company>PHYSIA</Company>
|
||||||
|
<Authors>PHYSIA</Authors>
|
||||||
|
<Product>KoTalk</Product>
|
||||||
|
<Description>KoTalk Android shell for the live Korean messaging experience</Description>
|
||||||
|
<ApplicationTitle>KoTalk</ApplicationTitle>
|
||||||
|
<AndroidPackageFormat>apk</AndroidPackageFormat>
|
||||||
|
<AndroidUseDesignerAssembly>false</AndroidUseDesignerAssembly>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and '$(JAVA_HOME)' != ''">
|
||||||
|
<JavaSdkDirectory>$(JAVA_HOME)</JavaSdkDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and Exists('/usr/lib/jvm/java-17-openjdk-amd64')">
|
||||||
|
<JavaSdkDirectory>/usr/lib/jvm/java-17-openjdk-amd64</JavaSdkDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and '$(ANDROID_SDK_ROOT)' != ''">
|
||||||
|
<AndroidSdkDirectory>$(ANDROID_SDK_ROOT)</AndroidSdkDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and Exists('$([System.Environment]::GetEnvironmentVariable(`HOME`))/Android/Sdk')">
|
||||||
|
<AndroidSdkDirectory>$([System.Environment]::GetEnvironmentVariable('HOME'))/Android/Sdk</AndroidSdkDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
44
src/PhysOn.Mobile.Android/Resources/AboutResources.txt
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
Images, layout descriptions, binary blobs and string dictionaries can be included
|
||||||
|
in your application as resource files. Various Android APIs are designed to
|
||||||
|
operate on the resource IDs instead of dealing with images, strings or binary blobs
|
||||||
|
directly.
|
||||||
|
|
||||||
|
For example, a sample Android app that contains a user interface layout (main.xml),
|
||||||
|
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
|
||||||
|
would keep its resources in the "Resources" directory of the application:
|
||||||
|
|
||||||
|
Resources/
|
||||||
|
drawable/
|
||||||
|
icon.png
|
||||||
|
|
||||||
|
layout/
|
||||||
|
main.xml
|
||||||
|
|
||||||
|
values/
|
||||||
|
strings.xml
|
||||||
|
|
||||||
|
In order to get the build system to recognize Android resources, set the build action to
|
||||||
|
"AndroidResource". The native Android APIs do not operate directly with filenames, but
|
||||||
|
instead operate on resource IDs. When you compile an Android application that uses resources,
|
||||||
|
the build system will package the resources for distribution and generate a class called "Resource"
|
||||||
|
(this is an Android convention) that contains the tokens for each one of the resources
|
||||||
|
included. For example, for the above Resources layout, this is what the Resource class would expose:
|
||||||
|
|
||||||
|
public class Resource {
|
||||||
|
public class Drawable {
|
||||||
|
public const int icon = 0x123;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Layout {
|
||||||
|
public const int main = 0x456;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Strings {
|
||||||
|
public const int first_string = 0xabc;
|
||||||
|
public const int second_string = 0xbcd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or
|
||||||
|
Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string
|
||||||
|
to reference the first string in the dictionary file values/strings.xml.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
|
<solid android:color="@color/surface_card" />
|
||||||
|
<stroke android:width="1dp" android:color="@color/surface_border" />
|
||||||
|
<corners android:radius="2dp" />
|
||||||
|
</shape>
|
||||||
52
src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/surface_canvas">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/app_webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:importantForAutofill="noExcludeDescendants"
|
||||||
|
android:overScrollMode="never" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading_bar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="3dp"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="100"
|
||||||
|
android:progressBackgroundTint="@color/surface_border"
|
||||||
|
android:progressTint="@color/accent_primary"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/offline_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="28dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:contentDescription="@string/app_name"
|
||||||
|
android:src="@mipmap/appicon" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/retry_button"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:background="@drawable/retry_button_background"
|
||||||
|
android:contentDescription="@string/reload"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@android:drawable/ic_popup_sync" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/appicon_background" />
|
||||||
|
<foreground android:drawable="@mipmap/appicon_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/appicon_background" />
|
||||||
|
<foreground android:drawable="@mipmap/appicon_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 940 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 1,010 B |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 3.3 KiB |
8
src/PhysOn.Mobile.Android/Resources/values/colors.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="surface_canvas">#F7F3EE</color>
|
||||||
|
<color name="surface_card">#FFFFFF</color>
|
||||||
|
<color name="surface_border">#DDD1C4</color>
|
||||||
|
<color name="ink_primary">#20242B</color>
|
||||||
|
<color name="accent_primary">#F05B2B</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#2C3E50</color>
|
||||||
|
</resources>
|
||||||
4
src/PhysOn.Mobile.Android/Resources/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">KoTalk</string>
|
||||||
|
<string name="reload">다시 불러오기</string>
|
||||||
|
</resources>
|
||||||
8
src/PhysOn.Mobile.Android/Resources/values/styles.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="KoTalkTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
|
||||||
|
<item name="android:windowBackground">@color/surface_canvas</item>
|
||||||
|
<item name="android:statusBarColor">@color/surface_canvas</item>
|
||||||
|
<item name="android:navigationBarColor">@color/surface_canvas</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="false" />
|
||||||
|
</network-security-config>
|
||||||
4
src/PhysOn.Web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "physon-web",
|
"name": "physon-web",
|
||||||
"version": "0.1.0-alpha.5",
|
"version": "0.1.0-alpha.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "physon-web",
|
"name": "physon-web",
|
||||||
"version": "0.1.0-alpha.5",
|
"version": "0.1.0-alpha.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "physon-web",
|
"name": "physon-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0-alpha.5",
|
"version": "0.1.0-alpha.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ type IconName =
|
||||||
| 'group'
|
| 'group'
|
||||||
|
|
||||||
const DEFAULT_API_BASE_URL = import.meta.env.VITE_API_BASE_URL?.trim() ?? ''
|
const DEFAULT_API_BASE_URL = import.meta.env.VITE_API_BASE_URL?.trim() ?? ''
|
||||||
const APP_VERSION = 'web-0.1.0-alpha.5'
|
const APP_VERSION = 'web-0.1.0-alpha.6'
|
||||||
|
|
||||||
const CONNECTION_LABEL: Record<ConnectionState, string> = {
|
const CONNECTION_LABEL: Record<ConnectionState, string> = {
|
||||||
idle: '준비 중',
|
idle: '준비 중',
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,22 @@ Windows, Mobile Web, Android를 병렬로 운영하면 언제든지 `문서상
|
||||||
|
|
||||||
| 기능 | Windows | Mobile Web | Android | 비고 |
|
| 기능 | Windows | Mobile Web | Android | 비고 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 초간단 가입 | Buildable | Live | Planned | Alpha 기준 |
|
| 초간단 가입 | Buildable | Live | Buildable | Android는 WebView 셸 기준 |
|
||||||
| 자기 자신과의 대화 | Buildable | Live | Planned | 첫 진입 루프 |
|
| 자기 자신과의 대화 | Buildable | Live | Buildable | 첫 진입 루프 |
|
||||||
| 텍스트 메시지 전송 | Buildable | Live | Planned | 기본 루프 |
|
| 텍스트 메시지 전송 | Buildable | Live | Buildable | 기본 루프 |
|
||||||
| 읽음 커서 갱신 | Partial | Partial | Planned | 복귀 정확도 보강 필요 |
|
| 읽음 커서 갱신 | Partial | Partial | Partial | 복귀 정확도 보강 필요 |
|
||||||
| 실시간 수신 | Partial | Partial | Planned | 경계 조건 검증 부족 |
|
| 실시간 수신 | Partial | Partial | Partial | 경계 조건 검증 부족 |
|
||||||
| 로컬 검색 | Partial | Partial | Planned | 제목/기본 검색 수준 |
|
| 로컬 검색 | Partial | Partial | Partial | 제목/기본 검색 수준 |
|
||||||
| 전역 검색 | Planned | Planned | Planned | 핵심 우선순위 |
|
| 전역 검색 | Planned | Planned | Planned | 핵심 우선순위 |
|
||||||
| 드래프트 보존 | Partial | Partial | Planned | 실제 복원 체감 부족 |
|
| 드래프트 보존 | Partial | Partial | Partial | 실제 복원 체감 부족 |
|
||||||
| 세션 자동 갱신 | Planned | Planned | Planned | 현 시점 취약점 |
|
| 세션 자동 갱신 | Planned | Planned | Partial | WebView 셸 기준 |
|
||||||
| 파일 첨부 | Planned | Planned | Planned | 다음 대규모 과제 |
|
| 파일 첨부 | Planned | Planned | Planned | 다음 대규모 과제 |
|
||||||
| 링크 프리뷰 | Planned | Planned | Planned | 제품성 핵심 |
|
| 링크 프리뷰 | Planned | Planned | Planned | 제품성 핵심 |
|
||||||
| 알림 묶음 정책 | Planned | Planned | Planned | 정책 문서화 완료 |
|
| 알림 묶음 정책 | Planned | Planned | Planned | 정책 문서화 완료 |
|
||||||
| 팝아웃 창 | Planned | N/A | N/A | 데스크톱 핵심 |
|
| 팝아웃 창 | Planned | N/A | N/A | 데스크톱 핵심 |
|
||||||
| 다중 창 | Planned | N/A | N/A | 데스크톱 핵심 |
|
| 다중 창 | Planned | N/A | N/A | 데스크톱 핵심 |
|
||||||
| Android APK | N/A | N/A | Planned | 병렬 도입 예정 |
|
| Android APK | N/A | N/A | Buildable | alpha 기준선 확보 |
|
||||||
| Releases 배포 | Partial | Partial | Planned | 표면 정리 필요 |
|
| Releases 배포 | Partial | Partial | Buildable | 표면 정리 진행 중 |
|
||||||
|
|
||||||
## 제품 메시지 규칙
|
## 제품 메시지 규칙
|
||||||
|
|
||||||
|
|
@ -58,7 +58,13 @@ Windows, Mobile Web, Android를 병렬로 운영하면 언제든지 `문서상
|
||||||
|
|
||||||
- 장기적으로 일상 주사용 모바일 채널
|
- 장기적으로 일상 주사용 모바일 채널
|
||||||
- 푸시/미디어/백그라운드 안정성으로 모바일 웹을 보완
|
- 푸시/미디어/백그라운드 안정성으로 모바일 웹을 보완
|
||||||
- 아직 설계 우선 단계다
|
- 현재는 WebView 기반 APK 셸을 확보했고, 장기적으로는 공용 네이티브 UI 축으로 옮길지 판단 중이다
|
||||||
|
|
||||||
|
## iOS / Linux
|
||||||
|
|
||||||
|
- iOS는 저장소 Assets 직접 배포가 아니라 Apple 채널 기준으로 준비한다
|
||||||
|
- Linux는 Windows와 같은 장기 네이티브 데스크톱 축으로 본다
|
||||||
|
- 두 채널 모두 공용 UI 프레임워크 선택에 직접 연결되므로, Android 단독 최적화와 분리해 판단하면 안 된다
|
||||||
|
|
||||||
## 기능 우선순위 매핑
|
## 기능 우선순위 매핑
|
||||||
|
|
||||||
|
|
|
||||||