From 799b975406c33e8ecfdacb8052c30bba00f802fd Mon Sep 17 00:00:00 2001 From: ian Date: Thu, 16 Apr 2026 13:54:11 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EA=B0=9C:=20=ED=94=8C=EB=9E=AB?= =?UTF-8?q?=ED=8F=BC=20=EA=B2=B0=EB=A1=A0=EA=B3=BC=20=EB=A6=B4=EB=A6=AC?= =?UTF-8?q?=EC=A6=88=20=EB=85=B8=ED=8A=B8=20=EA=B7=9C=EC=B9=99=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-portable.yml | 54 ++-- CHANGELOG.md | 5 + CLIENT_PLATFORM_DECISION.md | 160 ++++++++++ DEVELOPMENT.md | 29 +- PROJECT_STATUS.md | 21 +- PhysOn.sln | 7 + README.md | 18 +- RELEASING.md | 12 +- ROADMAP.md | 18 +- deploy/Caddyfile | 4 +- docs/assets/latest/README.md | 1 + docs/assets/latest/kotalk-android-mockup.png | Bin 0 -> 80622 bytes packaging/windows/KoTalkInstaller.nsi | 85 +++++ release-assets/README.md | 21 +- release-assets/templates/RELEASE_NOTES.ko.md | 4 +- .../branding/generate_android_icon_assets.py | 50 +++ scripts/release/build-android-apk.sh | 119 +++++++ .../release/build-windows-distributions.sh | 130 ++++++++ scripts/release/release-prepare-assets.sh | 296 ++++++++++++++++-- scripts/release/release-publish-forge.sh | 8 +- scripts/release/release-publish-github.sh | 8 +- src/PhysOn.Desktop/PhysOn.Desktop.csproj | 8 +- .../ViewModels/MainWindowViewModel.cs | 2 +- src/PhysOn.Mobile.Android/AndroidManifest.xml | 14 + src/PhysOn.Mobile.Android/MainActivity.cs | 268 ++++++++++++++++ .../PhysOn.Mobile.Android.csproj | 37 +++ .../Resources/AboutResources.txt | 44 +++ .../drawable/retry_button_background.xml | 6 + .../Resources/layout/activity_main.xml | 52 +++ .../Resources/mipmap-anydpi-v26/appicon.xml | 4 + .../mipmap-anydpi-v26/appicon_round.xml | 4 + .../Resources/mipmap-hdpi/appicon.png | Bin 0 -> 1328 bytes .../mipmap-hdpi/appicon_background.png | Bin 0 -> 229 bytes .../mipmap-hdpi/appicon_foreground.png | Bin 0 -> 1374 bytes .../Resources/mipmap-mdpi/appicon.png | Bin 0 -> 940 bytes .../mipmap-mdpi/appicon_background.png | Bin 0 -> 142 bytes .../mipmap-mdpi/appicon_foreground.png | Bin 0 -> 1010 bytes .../Resources/mipmap-xhdpi/appicon.png | Bin 0 -> 1721 bytes .../mipmap-xhdpi/appicon_background.png | Bin 0 -> 295 bytes .../mipmap-xhdpi/appicon_foreground.png | Bin 0 -> 1859 bytes .../Resources/mipmap-xxhdpi/appicon.png | Bin 0 -> 2528 bytes .../mipmap-xxhdpi/appicon_background.png | Bin 0 -> 436 bytes .../mipmap-xxhdpi/appicon_foreground.png | Bin 0 -> 2749 bytes .../Resources/mipmap-xxxhdpi/appicon.png | Bin 0 -> 3267 bytes .../mipmap-xxxhdpi/appicon_background.png | Bin 0 -> 594 bytes .../mipmap-xxxhdpi/appicon_foreground.png | Bin 0 -> 3387 bytes .../Resources/values/colors.xml | 8 + .../values/ic_launcher_background.xml | 4 + .../Resources/values/strings.xml | 4 + .../Resources/values/styles.xml | 8 + .../Resources/xml/network_security_config.xml | 4 + src/PhysOn.Web/package-lock.json | 4 +- src/PhysOn.Web/package.json | 2 +- src/PhysOn.Web/src/App.tsx | 2 +- 문서/33-platform-capability-matrix.md | 28 +- 55 files changed, 1439 insertions(+), 114 deletions(-) create mode 100644 CLIENT_PLATFORM_DECISION.md create mode 100644 docs/assets/latest/kotalk-android-mockup.png create mode 100644 packaging/windows/KoTalkInstaller.nsi create mode 100755 scripts/branding/generate_android_icon_assets.py create mode 100755 scripts/release/build-android-apk.sh create mode 100755 scripts/release/build-windows-distributions.sh create mode 100644 src/PhysOn.Mobile.Android/AndroidManifest.xml create mode 100644 src/PhysOn.Mobile.Android/MainActivity.cs create mode 100644 src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj create mode 100644 src/PhysOn.Mobile.Android/Resources/AboutResources.txt create mode 100644 src/PhysOn.Mobile.Android/Resources/drawable/retry_button_background.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon_round.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_background.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_foreground.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_background.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_foreground.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_background.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_foreground.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_background.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_foreground.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_background.png create mode 100644 src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_foreground.png create mode 100644 src/PhysOn.Mobile.Android/Resources/values/colors.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/values/ic_launcher_background.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/values/strings.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/values/styles.xml create mode 100644 src/PhysOn.Mobile.Android/Resources/xml/network_security_config.xml diff --git a/.github/workflows/release-portable.yml b/.github/workflows/release-portable.yml index d12184b..9ad929d 100644 --- a/.github/workflows/release-portable.yml +++ b/.github/workflows/release-portable.yml @@ -31,7 +31,7 @@ permissions: jobs: build-windows: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout @@ -42,21 +42,30 @@ jobs: with: global-json-file: global.json - - name: Restore desktop project - run: dotnet restore src/PhysOn.Desktop/PhysOn.Desktop.csproj + - name: Install NSIS + run: sudo apt-get update && sudo apt-get install -y nsis - - name: Publish portable desktop build - run: dotnet publish src/PhysOn.Desktop/PhysOn.Desktop.csproj -c Release -r win-x64 --self-contained true -o out/win-x64 + - name: Build Windows release assets + 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 - shell: pwsh - run: Compress-Archive -Path out/win-x64/* -DestinationPath out/KoTalk-windows-x64.zip + cp out/KoTalk-windows-x64-"$VERSION_INPUT".zip out/KoTalk-windows-x64.zip + cp out/KoTalk-windows-x64-onefile-"$VERSION_INPUT".exe out/KoTalk-windows-x64-onefile.exe + cp out/KoTalk-windows-x64-installer-"$VERSION_INPUT".exe out/KoTalk-windows-x64-installer.exe - name: Upload Windows artifact uses: actions/upload-artifact@v4 with: - name: windows-portable - path: out/KoTalk-windows-x64.zip + name: windows-release + path: | + out/KoTalk-windows-x64.zip + out/KoTalk-windows-x64-onefile.exe + out/KoTalk-windows-x64-installer.exe build-android: if: ${{ hashFiles('src/PhysOn.Mobile.Android/*.csproj') != '' }} @@ -80,23 +89,16 @@ jobs: - name: Install Android workload run: dotnet workload install android - - name: Restore Android project - run: dotnet restore src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj - - name: Publish Android APK + env: + VERSION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }} run: | - dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \ - -c Release \ - -f net8.0-android \ - -p:AndroidPackageFormat=apk \ - -p:AndroidKeyStore=false \ - -o out/android + chmod +x scripts/release/build-android-apk.sh + ./scripts/release/build-android-apk.sh \ + --version "$VERSION_INPUT" \ + --output out - - name: Collect Android artifact - 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 + cp out/KoTalk-android-universal-"$VERSION_INPUT".apk out/KoTalk-android-universal.apk - name: Upload Android artifact uses: actions/upload-artifact@v4 @@ -118,7 +120,7 @@ jobs: - name: Download Windows artifact uses: actions/download-artifact@v4 with: - name: windows-portable + name: windows-release path: incoming/windows - name: Download Android artifact @@ -149,6 +151,8 @@ jobs: --version "$VERSION_INPUT" --channel "$channel" --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 ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759302b..32968ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ### 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` - GitHub 릴리즈 게시 스크립트 `scripts/release/release-publish-github.sh` - 제2·제3 공개 원격 순차 게시 스크립트 `scripts/release/release-publish-public.sh` @@ -53,6 +56,8 @@ ### Changed +- 공개 릴리즈 Assets에서 스크린샷과 릴리즈 노트 첨부를 제거하고, 최신 화면은 변경 노트 본문에서 직접 확인하도록 조정 +- Android 상태를 `계획`에서 `첫 APK 기준선 확보` 단계로 상향하고, iOS/Linux 계획을 공개 문서에 명시 - 데스크톱/웹 스크린샷 캡처를 앱 창·앱 셸 기준으로 정리하고, README·SHOWCASE 이미지를 실제 종횡비에 맞춘 `width/height` 기준으로 재배치 - 공개 원격 배포 정책을 `public/* 브랜치 + 버전 태그 + 릴리즈 페이지 + 자산` 기준으로 고정 - Gitea/GitHub 릴리즈 게시 스크립트가 지정 원격 기준으로 동작하고 최신 스크린샷 자산도 함께 첨부하도록 확장 diff --git a/CLIENT_PLATFORM_DECISION.md b/CLIENT_PLATFORM_DECISION.md new file mode 100644 index 0000000..18904bd --- /dev/null +++ b/CLIENT_PLATFORM_DECISION.md @@ -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?”: +- Microsoft Learn, “Supported platforms for .NET MAUI apps”: + +### 3. Uno Platform으로 장기 축 전환 + +장점: + +- 공식 문서 기준 Android, iOS, Web, macOS, Linux, Windows를 포괄한다 +- 광범위한 플랫폼 표면 자체는 매력적이다 + +한계: + +- 현재 Windows 클라이언트가 Avalonia 기반이라, Uno로 가면 데스크톱 UI 자산과 운영 경험을 다시 맞춰야 한다 +- 지금 시점의 저장소 기준으로는 전환 비용이 작지 않다 + +판단: + +- **대안으로는 충분히 검토 가능** +- **하지만 현재 코드베이스를 고려하면 1순위는 아님** + +참고: + +- Uno Platform, “Supported platforms”: + +### 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”: +- Avalonia Docs, “Welcome / What is Avalonia?”: + +## 최종 결정 + +### 전술 + +- 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 배포 준비 문서와 빌드 전제 정리 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 581d5f9..ea47375 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -80,22 +80,21 @@ dotnet test PhysOn.sln -c Debug Windows: ```bash -dotnet publish src/PhysOn.Desktop/PhysOn.Desktop.csproj \ - -c Release \ - -r win-x64 \ - --self-contained true \ - -o artifacts/release/v0.1.0-alpha.1-win-x64 +./scripts/release/build-windows-distributions.sh \ + --version 2026.04.16-alpha.6 ``` +생성물: + +- `KoTalk-windows-x64-.zip` +- `KoTalk-windows-x64-onefile-.exe` +- `KoTalk-windows-x64-installer-.exe` + Android: ```bash -dotnet workload install android -dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \ - -c Release \ - -f net8.0-android \ - -p:AndroidPackageFormat=apk \ - -o artifacts/release/android +./scripts/release/build-android-apk.sh \ + --version 2026.04.16-alpha.6 ``` 공개 산출물 네이밍은 `KoTalk-*` 기준으로 정리하는 방향이고, 현재 내부 스크립트와 프로젝트명은 별도 정렬 단계에 있습니다. @@ -104,10 +103,12 @@ dotnet publish src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj \ ```bash ./scripts/release/release-prepare-assets.sh \ - --version v0.1.0-alpha.1 \ + --version 2026.04.16-alpha.6 \ --channel alpha \ - --windows-zip artifacts/release/PhysOn-win-x64-v0.1.0-alpha.1.zip \ - --android-apk artifacts/release/PhysOn-android-universal-v0.1.0-alpha.1.apk \ + --windows-zip artifacts/builds/2026.04.16-alpha.6/KoTalk-windows-x64-2026.04.16-alpha.6.zip \ + --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 \ --force ``` diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 320cb42..2c6081c 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -9,7 +9,7 @@ | Public brand | `KoTalk` | | Stage | `Alpha` | | Most usable surface | Mobile web live + Windows build | -| Biggest current gap | Android 실빌드와 데스크톱 멀티윈도우 완성도 | +| Biggest current gap | 네이티브 모바일 공용 UI 수렴과 데스크톱 멀티윈도우 완성도 | | Signup direction | 공개형 1회성 인증 중심으로 재설계 중 | | Tone of this repo | 현재 동작 범위와 남은 갭을 함께 적는 제품형 저장소 | @@ -19,6 +19,7 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서 - Windows 데스크톱 클라이언트 빌드 - 모바일 웹 실서비스 채널 +- Android APK 빌드 기준선 - 기본 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 복구 루프 - 최신 기준 스크린샷 세트 - 릴리즈 경로와 다운로드 경로 문서 @@ -29,8 +30,10 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서 |---|---|---|---| | Windows desktop | 저장소 빌드 / 릴리즈 산출물 | Buildable | 핵심 메시징 루프 검증 가능 | | Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 제공 | -| Android | 저장소 릴리즈 예정 | In progress | 문서와 배포 구조 우선 정리 중 | -| Official mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest와 version manifest를 HTTPS로 제공 | +| Android | 저장소 APK / 릴리즈 파이프라인 | Buildable | alpha WebView 셸과 APK 산출물 생성 가능 | +| 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 @@ -38,6 +41,7 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서 - Windows 클라이언트는 저장소 기준으로 빌드 가능한 상태입니다. - 모바일 웹은 [vstalk.phy.kr](https://vstalk.phy.kr)에서 공개 중입니다. +- Android APK는 저장소 기준으로 생성 가능한 상태입니다. - 기본 메시징 루프와 세션 복구 흐름은 구현돼 있습니다. - 검색, 보관, 빈 상태 UX는 1차 개편이 반영돼 있습니다. - 최신 스크린샷은 저장소에 함께 보관됩니다. @@ -66,7 +70,9 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서 ## In Progress -- Android 첫 실사용 빌드 +- Android APK 서명과 배포 품질 정리 +- iOS 채널 준비와 Apple 배포 절차 정리 +- Linux 네이티브 패키징 기준선 - 릴리즈 페이지와 미러 간 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 @@ -111,3 +119,4 @@ KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서 - 라이브 우선순위: [문서/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) - 신뢰와 보안 표면: [TRUST_CENTER.md](TRUST_CENTER.md), [SECURITY.md](SECURITY.md) +- 클라이언트 플랫폼 결론: [CLIENT_PLATFORM_DECISION.md](CLIENT_PLATFORM_DECISION.md) diff --git a/PhysOn.sln b/PhysOn.sln index 2e37bd4..6d0ff60 100644 --- a/PhysOn.sln +++ b/PhysOn.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{04EBE1E2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Api.IntegrationTests", "tests\PhysOn.Api.IntegrationTests\PhysOn.Api.IntegrationTests.csproj", "{38821CD9-915B-4C96-8A35-11BDE991C16E}" 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 GlobalSection(NestedProjects) = preSolution {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} {90EF510F-E338-45C4-9EF4-0A4916725EF0} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB} {38821CD9-915B-4C96-8A35-11BDE991C16E} = {04EBE1E2-D374-4F58-A0D5-5062BB4674FA} + {FD4774AB-47F7-4C47-BCC7-FD97BCBF4101} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 7f0179e..e1b9a78 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ 짧은 답장, 빠른 복귀, 설명 가능한 제품 표면, 그리고 한국어 사용 습관에 맞는 조용한 UI를 핵심 기준으로 삼습니다.

+

+ 현재는 Windows, 모바일 웹, Android APK 기준선을 운영하고 있으며, iOS와 Linux 네이티브 채널은 같은 제품 경험 안으로 수렴시키는 방향을 공개적으로 준비 중입니다. +

+

status platforms @@ -31,6 +35,7 @@ Showcase · Background · Alternative Gap · + Platform Strategy · Brand Guide · FAQ · Releases · @@ -61,8 +66,8 @@ | Layer | Current read | |---|---| | What this is | 한국어 UI, 낮은 피로도, 업무형 메시징 복귀 흐름을 중심에 둔 Windows-first 오픈소스 메신저 | -| What works now | Windows 빌드, 모바일 웹 라이브, 기본 인증/대화/세션 루프 | -| What is still moving | Android 첫 실빌드, 파일 전송, 검색 확장, 공개 다운로드 미러 정합성 | +| What works now | Windows 빌드, 모바일 웹 라이브, Android APK 기준선, 기본 인증/대화/세션 루프 | +| What is still moving | 파일 전송, 검색 확장, 네이티브 모바일 공용 UI 수렴, 공개 다운로드 미러 정합성 | | What this repo tries to show | 화면, 빌드 산출물, 릴리즈 경로, 상태 문서, 배경 문서를 한 눈에 읽히게 정리한 제품형 저장소 | ## Why Now @@ -156,14 +161,16 @@ KoTalk는 바로 그 비어 있는 결합점을 목표로 둡니다. 이 항목 |---|---|---|---| | Windows desktop | 저장소 빌드와 버전별 산출물 | Buildable | 핵심 메시징 루프와 데스크톱 레이아웃 실험 진행 중 | | Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | Live | 가입, 대화, 검색, 보관 1차 흐름 검증 | -| Android | 저장소 릴리즈 예정 | In progress | 문서와 배포 구조 우선 정리 중 | -| Official download mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | Live | Windows latest와 version manifest를 HTTPS로 제공 | +| Android | 저장소 APK / 릴리즈 파이프라인 | Buildable | WebView 기반 alpha 셸과 APK 산출물 기준선 확보 | +| 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 KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각을 조합해 실서비스와 로컬 빌드를 함께 검증하는 쪽에 가깝습니다. -- 클라이언트: Windows 데스크톱 + 모바일 웹 + Android 예정 +- 클라이언트: Windows 데스크톱 + 모바일 웹 + Android APK 기준선 + iOS/Linux 예정 - API: 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 루프 - 배포: VPS 기반 same-origin 웹앱과 API 운영 - 공개 증거: 저장소 스크린샷, 빌드 산출물, 상태 문서, 릴리즈 경로 @@ -211,6 +218,7 @@ KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각 - [ROADMAP.md](ROADMAP.md) - [BUSINESS_MODEL.md](BUSINESS_MODEL.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) - [문서/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) diff --git a/RELEASING.md b/RELEASING.md index b7a84ce..5d8a1a7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -15,7 +15,7 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공 ## Current Note 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 @@ -25,13 +25,16 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공 4. 최신 스크린샷이 현재 UI를 대표해야 합니다. 5. 다운로드 경로와 릴리즈 링크가 함께 갱신돼야 합니다. 6. 공개 원격은 `브랜치 + 태그 + 릴리즈 페이지 + 자산`을 한 세트로 맞춥니다. -7. 공개 릴리즈 페이지에는 산출물과 최신 스크린샷을 함께 게시합니다. +7. 공개 릴리즈 페이지의 Assets는 실행 파일, 체크섬, 메타데이터처럼 실제 배포에 필요한 파일만 남깁니다. +8. 최신 스크린샷은 Assets 첨부 대신 변경 노트 안에 담고, 다운로드 미러의 정적 경로를 이미지 소스로 사용합니다. ## Platform Policy -- Windows: 빌드 산출물, 스크린샷, 체크섬을 함께 남깁니다. +- Windows: installer, onefile portable, zip, 체크섬을 기본 산출물로 유지합니다. - Mobile web: 라이브 반영이 있으면 스크린샷과 상태 문서를 함께 갱신합니다. - Android: APK 공개 시 공식 미러와 저장소 릴리즈를 함께 맞춥니다. +- iOS: 자체 미러나 저장소 Assets로 직접 배포하지 않고, Apple 배포 채널을 전제로 준비합니다. +- Linux: Windows와 함께 장기적인 네이티브 클라이언트 축으로 다루며, 공유 UI 프레임워크 기준선을 유지합니다. ## Public Release Sequence @@ -41,7 +44,8 @@ KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공 4. 제2 공개 레포에 브랜치와 태그를 푸시합니다. 5. 제2 공개 레포 릴리즈 페이지에 자산과 노트를 게시합니다. 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 diff --git a/ROADMAP.md b/ROADMAP.md index dae7837..8485f15 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,8 @@ ## 현재 상태 - Alpha 가입, 대화 목록, 대화창, 텍스트 전송이 동작하는 첫 사용 가능 프로토타입 확보 -- Windows x64 portable build 생성 가능 +- Windows x64 installer / onefile / zip 생성 가능 +- Android APK alpha 기준선 생성 가능 - `vstalk.phy.kr` 모바일 웹앱과 API를 VPS에 실제 배포 - 원격 저장소에 최신 기준 스크린샷 포함 시작 - `vstalk.phy.kr` 모바일 웹앱 MVP 빌드 및 same-origin API 검증 완료 @@ -15,9 +16,11 @@ - [x] 메시지 전송 - [x] 읽기 커서 갱신 - [x] Windows portable zip 생성 +- [x] Windows installer / onefile / zip 생성 - [x] 모바일 웹앱 PWA 셸/가입/대화/전송 - [x] VPS 공개 API 상시 구동 - [x] `vstalk.phy.kr` same-origin 웹앱 배포 +- [x] Android WebView 셸과 첫 APK 생성 - [ ] 데스크톱 WebSocket 실시간 반영 ## v0.2 Collaboration Basics @@ -30,12 +33,19 @@ ## v0.2 Android First-class -- [ ] Android 셸/네비게이션 골격 -- [ ] Android 로그인/대화 목록/대화 진입 MVP +- [x] Android 셸/네비게이션 골격 +- [x] Android 로그인/대화 목록/대화 진입 MVP 기준선 - [ ] APK 서명 및 산출물 규칙 확정 - [ ] Windows/Android 동시 릴리즈 메타데이터 검증 - [ ] Forge Releases + VPS 미러 동시 게시 +## v0.3 Shared Client Expansion + +- [ ] Avalonia 기반 공유 클라이언트 구조를 Android/iOS/Linux 관점으로 재정리 +- [ ] iOS 배포 채널 준비 문서와 빌드 체인 검증 +- [ ] Linux 패키징 기준선 확보 +- [ ] Android WebView 셸에서 네이티브 공용 UI 단계로 이행할 범위 정의 + ## v0.2 Mobile Web Entry - [x] `vstalk.phy.kr` 모바일 웹 IA와 핵심 사용자 흐름 확정 @@ -89,6 +99,6 @@ - 하나의 태그는 하나의 릴리즈 레코드를 뜻합니다. - 같은 버전 번호 아래에 Windows와 Android 자산을 함께 게시합니다. -- 원격 저장소에는 최신 기준 스크린샷도 함께 포함합니다. +- 원격 저장소 릴리즈 Assets에는 실행 파일과 체크섬만 두고, 최신 스크린샷은 변경 노트 안에서 참조합니다. - 다운로드 미러와 원격 Releases는 같은 자산 이름과 같은 노트를 기준으로 맞춥니다. - 모바일 웹은 설치형 산출물 대신 `https://vstalk.phy.kr`를 기준 진입점으로 관리합니다. diff --git a/deploy/Caddyfile b/deploy/Caddyfile index 5009816..96b0ad0 100644 --- a/deploy/Caddyfile +++ b/deploy/Caddyfile @@ -7,8 +7,8 @@ root * /srv/download redir /windows /windows/latest 302 redir /android /android/latest 302 - redir /windows/latest /latest/KoTalk-windows-x64.zip 302 - redir /android/latest /latest/KoTalk-android-universal.apk 302 + redir /windows/latest /latest/windows/index.html 302 + redir /android/latest /latest/android/index.html 302 redir /latest /latest/version.json 302 header { Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" diff --git a/docs/assets/latest/README.md b/docs/assets/latest/README.md index 9c6a8a5..478c8f9 100644 --- a/docs/assets/latest/README.md +++ b/docs/assets/latest/README.md @@ -13,4 +13,5 @@ - 현재 포함: - Windows 데스크톱 셸 - Windows 온보딩/대화 화면 + - Android APK shell 목업 - `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면 diff --git a/docs/assets/latest/kotalk-android-mockup.png b/docs/assets/latest/kotalk-android-mockup.png new file mode 100644 index 0000000000000000000000000000000000000000..adbab6de2a63ff13216a0c7ba6c18c8ff26279d4 GIT binary patch literal 80622 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 ZtDkQSvUZUH)wY>g(EI03HNJN_=O3uG%Par@ literal 0 HcmV?d00001 diff --git a/packaging/windows/KoTalkInstaller.nsi b/packaging/windows/KoTalkInstaller.nsi new file mode 100644 index 0000000..5e9d887 --- /dev/null +++ b/packaging/windows/KoTalkInstaller.nsi @@ -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 diff --git a/release-assets/README.md b/release-assets/README.md index 4e17265..d11c41e 100644 --- a/release-assets/README.md +++ b/release-assets/README.md @@ -19,10 +19,14 @@ release-assets/ SHA256SUMS.txt screenshots/ windows/ + index.html KoTalk-windows-x64.zip + KoTalk-windows-x64-onefile.exe + KoTalk-windows-x64-installer.exe SHA256SUMS.txt version.json android/ + index.html KoTalk-android-universal.apk SHA256SUMS.txt version.json @@ -35,6 +39,8 @@ release-assets/ windows/ x64/ 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 android/ universal/ @@ -46,14 +52,16 @@ release-assets/ - 같은 버전은 같은 서버 API 계약과 같은 릴리즈 노트를 공유합니다. - Windows와 Android는 같은 태그 아래 병렬 산출물로 게시합니다. -- Windows 기본 공개 형식은 `zip`, Android 기본 공개 형식은 `apk`입니다. +- Windows 기본 공개 형식은 `installer exe + onefile exe + zip`, Android 기본 공개 형식은 `apk`입니다. - APK는 공개 채널에 올릴 때 반드시 서명본을 사용합니다. - `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` -- 최신 Android: `https://download-vstalk.phy.kr/android/latest` +- 최신 Windows landing: `https://download-vstalk.phy.kr/windows/latest` +- 최신 Android landing: `https://download-vstalk.phy.kr/android/latest` - 전체 최신 메타데이터: `https://download-vstalk.phy.kr/latest/version.json` - 버전별 Windows: `https://download-vstalk.phy.kr/releases//windows/x64/...` - 버전별 Android: `https://download-vstalk.phy.kr/releases//android/universal/...` @@ -69,6 +77,8 @@ release-assets/ --version v0.2.0-alpha.1 \ --channel alpha \ --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 \ --screenshots artifacts/screenshots \ --force @@ -87,11 +97,14 @@ release-assets/ - Forge Releases: 버전별 원본 보관 - 다운로드 미러: 최신 포인터와 빠른 정적 다운로드 - 모바일 웹앱: `release-assets/`가 아니라 `vstalk.phy.kr` 배포 트랙에서 별도 운영 -- 공개 원격 릴리즈 페이지에는 ZIP/APK뿐 아니라 `screenshots/` 아래 최신 캡처도 함께 게시합니다. +- 공개 원격 릴리즈 페이지 Assets는 EXE/ZIP/APK, 최상위 `SHA256SUMS.txt`, 최상위 `version.json`만 유지합니다. +- 최신 스크린샷은 `release-assets/releases//screenshots/`에 보관하고, 릴리즈 노트 본문에서 직접 참조합니다. ## 운영 메모 - 생성된 버전별 산출물은 워크트리에 유지하며, 최신 로컬 검수와 서버 업로드 기준으로 사용합니다. - 공개 릴리즈마다 `RELEASE_NOTES.ko.md`, `SHA256SUMS.txt`, `version.json`을 함께 갱신합니다. - 같은 버전에서 Windows만 있고 Android가 아직 없을 수는 있지만, 장기 원칙은 `같은 버전 아래 두 플랫폼 병렬 게시`입니다. +- Windows latest landing은 설치형, onefile, 압축본을 동시에 노출합니다. +- Android latest landing은 APK 하나만 직접 노출하고, iOS는 저장소 Assets에 포함하지 않습니다. - 모바일 웹앱 정적 산출물은 `release-assets/`가 아니라 `/srv/vs-messanger/webapp/releases/`에 배포합니다. diff --git a/release-assets/templates/RELEASE_NOTES.ko.md b/release-assets/templates/RELEASE_NOTES.ko.md index 77db2c0..33835c6 100644 --- a/release-assets/templates/RELEASE_NOTES.ko.md +++ b/release-assets/templates/RELEASE_NOTES.ko.md @@ -5,11 +5,13 @@ ## 이번 빌드에 포함된 것 +- Windows x64 installer exe +- Windows x64 onefile portable exe - Windows x64 portable zip - Android universal apk - 무결성 체크용 `SHA256SUMS.txt` - 버전 메타데이터 `version.json`, `latest.json` -- 필요 시 한국어 스크린샷 +- 최신 스크린샷은 이 변경 노트 하단에서 직접 확인할 수 있습니다. ## 확인할 것 diff --git a/scripts/branding/generate_android_icon_assets.py b/scripts/branding/generate_android_icon_assets.py new file mode 100755 index 0000000..8e7a171 --- /dev/null +++ b/scripts/branding/generate_android_icon_assets.py @@ -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() diff --git a/scripts/release/build-android-apk.sh b/scripts/release/build-android-apk.sh new file mode 100755 index 0000000..f358def --- /dev/null +++ b/scripts/release/build-android-apk.sh @@ -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 Build configuration. Default: Release + --output

Output directory. Default: artifacts/builds/ + --dotnet 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" diff --git a/scripts/release/build-windows-distributions.sh b/scripts/release/build-windows-distributions.sh new file mode 100755 index 0000000..9c90a8c --- /dev/null +++ b/scripts/release/build-windows-distributions.sh @@ -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 Build configuration. Default: Release + --runtime Runtime identifier. Default: win-x64 + --output Output directory. Default: artifacts/builds/ + --dotnet 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" diff --git a/scripts/release/release-prepare-assets.sh b/scripts/release/release-prepare-assets.sh index 3a3d752..c84b505 100755 --- a/scripts/release/release-prepare-assets.sh +++ b/scripts/release/release-prepare-assets.sh @@ -8,6 +8,10 @@ Usage: Options: --windows-zip Windows x64 ZIP artifact path + --windows-portable-exe + Windows onefile portable EXE artifact path + --windows-installer-exe + Windows installer EXE artifact path --android-apk Android universal APK artifact path --zip Backward-compatible alias for --windows-zip --channel Release channel. Default: alpha @@ -23,6 +27,8 @@ EOF version="" channel="alpha" windows_zip="" +windows_portable_exe="" +windows_installer_exe="" android_apk="" notes_path="" screenshots_dir="" @@ -42,6 +48,14 @@ while [[ $# -gt 0 ]]; do windows_zip="${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="${2:-}" shift 2 @@ -75,8 +89,8 @@ if [[ -z "$version" ]]; then exit 1 fi -if [[ -z "$windows_zip" && -z "$android_apk" ]]; then - echo "At least one artifact must be provided: --windows-zip or --android-apk" >&2 +if [[ -z "$windows_zip" && -z "$windows_portable_exe" && -z "$windows_installer_exe" && -z "$android_apk" ]]; then + echo "At least one artifact must be provided for Windows or Android." >&2 usage >&2 exit 1 fi @@ -86,6 +100,16 @@ if [[ -n "$windows_zip" && ! -f "$windows_zip" ]]; then exit 1 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 echo "Android APK artifact not found: $android_apk" >&2 exit 1 @@ -146,6 +170,31 @@ else fi 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 '![%s](%s/%s)\n\n' "$label" "$screenshot_root_url" "$name" + done + } >> "$notes_file" +} + if [[ -n "$screenshots_dir" ]]; then while IFS= read -r screenshot; do 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) 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 platforms_json="" top_level_windows_alias="" @@ -168,6 +220,23 @@ append_platform_json() { 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() { local path="$1" local body="$2" @@ -187,38 +256,69 @@ $body EOF } -if [[ -n "$windows_zip" ]]; then - windows_release_name="KoTalk-windows-x64-$version.zip" - windows_latest_name="KoTalk-windows-x64.zip" +if [[ -n "$windows_zip" || -n "$windows_portable_exe" || -n "$windows_installer_exe" ]]; then windows_release_dir="$release_root/windows/x64" windows_latest_dir="$latest_root/windows" mkdir -p "$windows_release_dir" "$windows_latest_dir" - cp "$windows_zip" "$windows_release_dir/$windows_release_name" - cp "$windows_zip" "$windows_latest_dir/$windows_latest_name" + 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_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" - sha256sum "$windows_release_name" > SHA256SUMS.txt + sha256sum "${windows_release_hash_items[@]}" > SHA256SUMS.txt ) ( 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") - latest_hash_paths+=("windows/$windows_latest_name") - - windows_platform_body="$(cat < 0 )); then fi 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 < Windows Latest Windows build - ZIP package and SHA256 checksum + Installer, onefile portable, ZIP, SHA256 EOF )" @@ -321,6 +421,156 @@ EOF )" fi +windows_installer_card="" +if [[ -n "$windows_installer_exe" ]]; then + windows_installer_card="$(cat < + Installer + Windows installer + 설치형 EXE + +EOF +)" +fi + +windows_portable_card="" +if [[ -n "$windows_portable_exe" ]]; then + windows_portable_card="$(cat < + Portable + Onefile executable + 압축 해제 없이 바로 실행 + +EOF +)" +fi + +windows_zip_card="" +if [[ -n "$windows_zip" ]]; then + windows_zip_card="$(cat < + Archive + Extracted bundle ZIP + 폴더 배포본 압축 파일 + +EOF +)" +fi + +if [[ -d "${latest_root}/windows" ]]; then + cat > "${latest_root}/windows/index.html" < + + + + + KoTalk for Windows + + + +
+
+

KoTalk for Windows

+

설치형과 onefile portable, 압축본을 같은 기준선으로 제공합니다.

+
+$windows_installer_card +$windows_portable_card +$windows_zip_card + + Integrity + SHA256SUMS + 체크섬 확인 + +
+

version.json

+
+
+ + +EOF +fi + +if [[ -d "${latest_root}/android" ]]; then + cat > "${latest_root}/android/index.html" < + + + + + KoTalk for Android + + + +
+
+

KoTalk for Android

+

KoTalk Android shell APK와 버전 메타데이터를 같은 기준선으로 제공합니다.

+ +

version.json

+
+
+ + +EOF +fi + cat > "$download_root/index.html" < diff --git a/scripts/release/release-publish-forge.sh b/scripts/release/release-publish-forge.sh index 4dc928d..1f1a6d4 100755 --- a/scripts/release/release-publish-forge.sh +++ b/scripts/release/release-publish-forge.sh @@ -168,9 +168,11 @@ case "$version" in esac 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' \) \ - | sort + { + find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \) + [[ -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 diff --git a/scripts/release/release-publish-github.sh b/scripts/release/release-publish-github.sh index f8660ed..61ba7b9 100755 --- a/scripts/release/release-publish-github.sh +++ b/scripts/release/release-publish-github.sh @@ -123,9 +123,11 @@ case "$version" in esac 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' \) \ - | sort + { + find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \) + [[ -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 diff --git a/src/PhysOn.Desktop/PhysOn.Desktop.csproj b/src/PhysOn.Desktop/PhysOn.Desktop.csproj index e6eb2cc..2d6e552 100644 --- a/src/PhysOn.Desktop/PhysOn.Desktop.csproj +++ b/src/PhysOn.Desktop/PhysOn.Desktop.csproj @@ -12,10 +12,10 @@ KoTalk 한국어 중심의 차분한 메시징 경험을 다시 설계하는 Windows-first 메신저 KoTalk - 0.1.0.5 - 0.1.0.5 - 0.1.0-alpha.5 - 0.1.0-alpha.5 + 0.1.0.6 + 0.1.0.6 + 0.1.0-alpha.6 + 0.1.0-alpha.6 true diff --git a/src/PhysOn.Desktop/ViewModels/MainWindowViewModel.cs b/src/PhysOn.Desktop/ViewModels/MainWindowViewModel.cs index e9ce4c6..cddd308 100644 --- a/src/PhysOn.Desktop/ViewModels/MainWindowViewModel.cs +++ b/src/PhysOn.Desktop/ViewModels/MainWindowViewModel.cs @@ -329,7 +329,7 @@ public partial class MainWindowViewModel : ViewModelBase, IAsyncDisposable $"desktop-{Environment.MachineName.ToLowerInvariant()}", "windows", Environment.MachineName, - "0.1.0-alpha.5")); + "0.1.0-alpha.6")); var response = await _apiClient.RegisterAlphaQuickAsync(apiBaseUrl, request, CancellationToken.None); ApiBaseUrl = apiBaseUrl; diff --git a/src/PhysOn.Mobile.Android/AndroidManifest.xml b/src/PhysOn.Mobile.Android/AndroidManifest.xml new file mode 100644 index 0000000..f8c2c84 --- /dev/null +++ b/src/PhysOn.Mobile.Android/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/PhysOn.Mobile.Android/MainActivity.cs b/src/PhysOn.Mobile.Android/MainActivity.cs new file mode 100644 index 0000000..790fff3 --- /dev/null +++ b/src/PhysOn.Mobile.Android/MainActivity.cs @@ -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 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(Resource.Id.app_webview); + _loadingBar = FindViewById(Resource.Id.loading_bar); + _offlineOverlay = FindViewById(Resource.Id.offline_overlay); + _retryButton = FindViewById(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 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(); + } + } +} diff --git a/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj b/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj new file mode 100644 index 0000000..a58c1ed --- /dev/null +++ b/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj @@ -0,0 +1,37 @@ + + + net8.0-android + 26 + Exe + enable + enable + PhysOn.Mobile.Android + KoTalk.Mobile.Android + kr.physia.kotalk + 6 + 0.1.0-alpha.6 + PHYSIA + PHYSIA + KoTalk + KoTalk Android shell for the live Korean messaging experience + KoTalk + apk + false + + + + $(JAVA_HOME) + + + + /usr/lib/jvm/java-17-openjdk-amd64 + + + + $(ANDROID_SDK_ROOT) + + + + $([System.Environment]::GetEnvironmentVariable('HOME'))/Android/Sdk + + diff --git a/src/PhysOn.Mobile.Android/Resources/AboutResources.txt b/src/PhysOn.Mobile.Android/Resources/AboutResources.txt new file mode 100644 index 0000000..219f425 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/AboutResources.txt @@ -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. \ No newline at end of file diff --git a/src/PhysOn.Mobile.Android/Resources/drawable/retry_button_background.xml b/src/PhysOn.Mobile.Android/Resources/drawable/retry_button_background.xml new file mode 100644 index 0000000..2ef2c5b --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/drawable/retry_button_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml b/src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml new file mode 100644 index 0000000..41376b0 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon.xml b/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon_round.xml b/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon_round.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/mipmap-anydpi-v26/appicon_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..05cb38e7762a2d52ca93aef4e77900034586b257 GIT binary patch literal 1328 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2VEOLp;uumf=k2WMj?hqvhW+(x zYtP-8J6pri*5lGyHe+wWO$XW+@hG-P$a5KTUNYK~mi0tMMPw2GHvtJr)-e9B0gd|K zCN(v-CHRZFUT7`RJ7;2i+%kRr*Y5>s#|q~ZM$W4ZijbE%XSrtc_kY*7>-~<;me>4q zceTs1qkrx*-g)8T$hc?$LtsFIhK7TPhyoXv04r-p6BDt*6H+RRu4Y$1QM+EPxbvN{ zk9S&Ai0Da!51!(ueS*@OTv(pVw00|BX47v7HaK=+g=kqz^Tz3MPG?WEub87?`PMN!=#$qvyM;eaf4{J?Wu^4PUng=pB9jB(yX}va+_81qyL)!i z&g}0#W*&F+{4Um*u0RQ^9jz1B+aKL>hNXV0mvncz!p>PaT8`5un^>(&eRS8y#lY>w zyO&Qp-KTG?zMdxaDl724U})R7`~@X{xN~0X6l8voO8XKVmQpo)eox1Pz3B-F9a~q2 zZ!G*9B_SNS>%@iIuQldcwQV`Wb84knsOXYa5`txU)t%?2YJa#lJ$-qjTpM4Gg6(JC zOLyuHpJ>!dyL3&0t-USsbB_94?eGAdX#$}~B)HnY2Ut~#WX|TzTp6VpD;z5PBs6oy z`p1j=?H`|BeEh`oVvPm&f_G@3Jvv~)Us@u+qeCn;0 zK2tGK$$DQG*He<)I7B1f`2o|*obrwtoI5WpDSDofoBjCA%FAE&Z{zxWn?FH!*|vGd zWZPHN7x8Y3IM6kLL2R$jB-hj4+=`bw_kQ|Yu|>%Ec2s6|W?DjlfXF2i)zWvL?(Y)G z&3b&@zJ5{e`{F6@YB>T~lNpbin=C%3c&tIoGfHLYomJ;0zv zpDd>rlTuJHLuI4P|8LfOa#l{WrWMNwD$YN>vG(%kGy6Z6Mb$9s`f;t(b7Z_&WHfW% zeb;}_H%lz|@x^V@Pw$UhmoCi<72R}~f7SZcOZTnYBYM$hF{i+qt&b;veHVUhb4B9u z7lLPlx9#_-IB&a^Z}WTBHHIg8!nW+(8#C`=T|n97EgY-7R!&*8YR49vJ;$Du^q%s( zyd}o*>kAY8kb{;piusjKUr+kP)^)U~a@M^$)?EdeiCRJLEITHgetKhXu57n>*@HRC zp2wUftUj-Bt?9c;zy09}@O*WTOB8 literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_background.png b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce7c084198418de301f96e547af9ad3a0afb167 GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ydl>XLn`LHy|7WRL4o0bL(2ci zOdXw6CcUL%|An6v*?zBLcmMplVZX74ga{888*6iuU-A&RaRUrymMNwOW)fH)~s3lSq1>+@%qE=B9ZSleTg}B!H^`Rhw zwo-ec7K5Oupbz2=tq<0!X!XHMB@d;P+B7D+$KT^zWL_N zcg{c%1VIo4K@bE%5ClOG1VIo4K@jXgsbu!Yuv%ijjQhVQSq19r>$3v`1Frkr-Ycw$ z?{Y3Ns`|H2)c_SNcXn*$V!w&8jw3L+DWDEe$hV?f2Enm{nMJPUD- ziSAbwtO$snG}89nL#?ny6%olMNtq`Ip`50PSLd99-Sx5p))*3}R7Z!=n-{@?+wt(n zA~N>&aw^LoLz?rDx{2#S(5e$b8Ijay_D0lYo*-2*M#R*LM0R_{B5SBt6H@26_nzB0 zviAbK_ZanHBgS4`!q}RpF|FTVAs3)xrDM1xt&NV!aFOM%BrcQbd+ zX=s{ajMS2|>IuePdk$&qs3k@-TT1qQ&XYBeF@~YxVUC$`6wf?*KgS+31DB@Qs5XVl zvJ&HOE<>gs0^XD1I{TkYN`j0rl*<)PFV5zvhwr1KeHzX=thJCPl$Z8XePR=yCHBY@(2=17E=iM;Al=!yb#%0|_O&-zbVol!JBBgV;*%<7dM7QV)kMdgg8sJs zfJ)M&Zt$FQOz&vtlXahS{VjL$OB0fTVVG-bE1(O(idc3L8?;#kiVa> zOC5!LUPsCmKm6$RxcAno#IYhqWlD>-^sm(1`PZmc$5ch?Eu*ULxjCv`*BrTnV5{L_ zgB9r!B&zRKb(fAq${?va@_f(wJC~6tL_PFSX+>$c*yu4 z+E;uvq##)ycVwTSU3Aa8nrv)-u5eJzSSDr3q;V- zs=KLe&BmpTn0H-6J{6@>$!y-d*>-kzx_3YLIuC&0LCjy1Q!L&XUwC1sdyvKd9CUNh z4s2iVqe$-_^ZuMdE-Fw}t0fE@XGRQcOtP$;hU@F=vjB7F_0B(I{-xU8)0><*|1zD^ z)4KqmSS-eS8?%1yyzQ!4bH}gi8r^Z#tUCbo?6Vuz6EuFSVZ0Yajj_?6l~at@uV0_+ zZ7f-g*Sv$d#1ONa407*qoM6N<$g0aM@lmGw# literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d19247e8f7156fcaca4fa110ee98c9ff196dcb5a GIT binary patch literal 940 zcmV;d15^BoP)ScCz%v0^`MdBFd zXVFU+A-o}MKwgCE1CJs7gJ7EINi-~ARyzx)PXEqV-<)KrQb9xjBMw7Gr{~$fRmmNC zgWSLlm>PlOdtq!CqPTXP09C`~Ln>FV5cCg%8m#ewHYHFeiUL%tI0xP@5dPyF9aspIq->tQ$YNbHEne*Or_trjM*u9IM?k-f7 zAiTx0W(4;QB@KIX9?ia;R#m+BL{W6T(Yr0CfFwRP%W26??3}ALXKP6wn(F^=ir%w8;e0CKE`TOdxGCfwbB10{;P5JulmH;!jin O0000Cpuljz;KBds zNC}DC%2p9mA8}8rJYRRLUsv`+D037E@D}%j7#8yzpO+m3TEO7x>gTe~DWM4fWNRmk literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_foreground.png b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..20b637e088714fe89777aef97ec1f7b743a48996 GIT binary patch literal 1010 zcmV8l)7TA~E09BqrIt=Zp`#Hi1T(y|FFI{XGuM-1*O( zIdd+=#KgqJ#KgqJ#N00AX8CII!HRYHe-OC?k39Wcm8aJCjDKC_#>(sYXzoiI`hQ`r zU@LDegWCJ9i!&cODf2zuaO%hLD0vhmAd|_M-rio@)V%8{@q^Q9tx;KyVN_W#3AWfV z-o0lIuXQgHB#>kY+5XRP<0IfFxOQ3;5Vac;#+%FOk&087y1z-M{pP{JaIHD{#RwQ5 z9~S^Ma<^~%O`)@RvRs28@R_(eM{Ru_Wk+6zdmjcM?E3(j8pR|Mh1?510->Uw@q}#n z0Km3l5-L`}--viQRi_FTRRds*VSH+arY#K|c%uVXvl)QJ_q$j+eFCZ63~seK)ext? z3QJLPi6lT(1rSsP$+uu~YMR|Uo~5hfRZ5ac02WSkkUje$UR5nvyLzmAI|Pk3kC6nd zL`2AiA+NpM&i;Kb0E(OcgT>>A$qw})mDOO^e58xUTW1FG32RS{KC6t^-OfI5OGsT9@QnwpwImAq+G5fNVi)x0^(j5J3V2M06sGfQVDp>1}Pf^pF*dV4FQ- z-U-7nYkWW9tQ||1lPHpS?ug+Y&k>Ax9Q$rOA|B2bf#p;cpUY?Bl{m9ot zV|#nBv#!qe4}I?&<4c&!jk>Jee*WBC`y7?<^&wS2)A(E`q zPNo?~q?grwZU&i5#sED2%!z5oCK07*qoM6N<$f^VMXDgXcg literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4238084cec0b8ed50f4147e54acd14dc329f44ac GIT binary patch literal 1721 zcmcJQjXTqe0>^)2vF&;ZlM#lp#)_mT?M}wA@-i6?Dr9peuUE>%x{D^G)h$yyj>cF! z$HHScZh7evTRKaZ#Ss&E*+~=~abh;cx}E3#4d;12-_Pey_}&N$4KRk;zyJU+4hkeh zd@beAK@7h7?Y-Qi0ARojBKSqp`Ahr^cHF2nQ=sPKa9WS?stohSwqK*fa5(iDhvIrb zbo^i?%x_E$vDZWN`kY5nn1{|0XdUW(k*b+JryChl8bQyX7|2 zq@3iI-VOiZZh*k4fr*W9kVWBU<_@_M3zpBu_yuS{@$7ep^ zYicqbuiqCJ&xRnL{Id=vVKZA}Y6p#0-5gj<_R33!A$_)S`{=U6+B(JsNs&{^<|0#1 zmUGgeQe5JF=Gq*?yP5*+cU0v{CO&eDSvSelnD4=30rA@q28#1nM`xcC`a$!szC3 z*RqREMPMm%-|258l`PSm{)(zOyoa`Lqb1Y6_2jC2tlnFT(}Nz?A1VZ>F0cz93fGq7 zMRG^iJSLlxajn^_U?{`-VS)uQHSOkPyIVnq>0L}UtPuq^ee8W3za;!z$+)ux+1wuI3Z?n!+I7q3Mpd_drM;c zTciEO&YB^+@ZvKv)iguAM7@v*e)!(3!qAk`Jg$cV=^==}6>C$Yd}K>Ah8w!W=L&X0 zM!FmX6=D^SX(?Q&q-2qGRyzDZ17Id)kP=eZ$R=_>hM_LV?Z{cwNVJBjt&<*FFbm|J zY0CNI(+7uNyvVhm*JA@~*NaP0Z?mwP#WUHv+dwwPS;!|OF7F40Ew(7B8;L0hzKEUg z8eQ;!#}uY~dDVURIrrxHqq%&YT6!HUa5Xb~29cdUa#B3|A5}W&=jbF7J}~(#Bkw1_ z+27>vYIXjjy-e_y;`tq#8!(gB?sRPAJhLhpkkxPxtnwzjEVfv;Ek1VWCN7Fo4e?XT zRbp;cklj0N%c&W{bYIB6DQtO-i-|n$ZUtcG$YQFywHphUs+5@(i1z_wCA0j>%d*iI z`S#818g6^k%!6!25xlY1a`>Dh>pWh_Q-s;+W!raT2OCmDI_EjGJe)=`!OKW$*nU{1 z3_3p*I`?~&kt+6sEk=K%CPc1Z!(FlJeQ@K_R7ECfUS%278E=hth_x4P-X;p8s~U(t zsfRrCLIQB5d$C92yPPSQKgMZnD0Pa{AH{{caEQM3NQs>zg`j$bUc*M^bO_O{{Z-E1}6Xj literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_background.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..d8906837eba835bb8e8c5e0b5b2517a843f67123 GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGo?>t=`Ln`LHy||J0fC9q-g9rbk zBPAqGOUg;b3uu0x_qW%;oBdBL^9HsA-UCt%rVQB(*BI6?Mhs1XZw&f8EYnrFCi4P) O&fw|l=d#Wzp$Pyf=2j>G literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_foreground.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1dad34d08de95aeb68eefda74de1c346cca8ec74 GIT binary patch literal 1859 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 Gz4DuQ^fs2N)X+p00i_>zopr04`2^_y7O^ literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_foreground.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..db7b22486bc791d77bb3d27df6ccf64d4b944bde GIT binary patch literal 2749 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

yRHuLCR;CQYB@rp z#em^Ls*)`U>VmEZ+AkkUdUbeM9@1wzduNNujYe*~-#GD-?XjhP31rEy+86&?N1%7N z2W2AMyCmJ5FLT^X$rqN($Dg*$!+_^uJC!>i_&B%tjQpIT>6AsU>} z;|C_%jaxU4>t$-U7Mb_X;$i#tn89;0OU7t}G)nJ@Se0PK=Q_r}oZT%{kteMl20~S{ zxxB8Lu|uLp)@hd%Od7c6$p>1rzI$2+8B)53*gQ8W(PxdBxbIQZVH1Ai5hk>|>-;ZO z{@{3*NUDNBPXIZFhw*CIa8>5XfQ$+EAV~f#ipC#nK&O$UAoaD}1us}#ATebhx?5hC z(_xC93n-qBGvc_iy^3$|=6fM9dDv-(_L1)T+ zUu>tG$ALnK^Ynlyn>)TWve3E!4+UQEMAdY$Y}S8u z$)~VtF}1mzB0dFRji$2T|H!zQh_9drxrWJhBgK3bn?*%tI43ZxJ5%-qLu2)Npr$-^ zmeL5b;66P&tc3Au3VK8c$4BYgME)p&wDy|L@t{UajwTwvSl79u$mt4m2ge@_xuzx6 zt4$KDtyneAPmKs9amgs~OZRVhtyXlN7`NtO_3-Q8cD9q=WGgWCFyrTE3j-ch4XDR0 zkH-1_0`qXmtz{cDH!)Yo(~1D@Ugwy@eApqT-kBsO`q3rB@gbxL-?W@Tr7FnY6RM|k ztM9&|KVs%XM!x)0XzKCu`8A~dmouQ_pwB(``@!p0A;92(Dn#dZcke1F)zy0ENRWJL z_}X5{I+`oa6J4;Ux;v)0Al5DYXzRjb$ZAg?_K>HH-k(#5Ks+11uuML^YJ5Q7?dUiP z&M~mdB*3#1!vsB1){NRVHaf{C3?esaT#vaNNNMcouhUwzG37QPV> z&e(|CQx#T|Mxn9b$jPhAU%yqm?DhDujOCce5*9^O*FJvwq6@f*Hm&UF$>=DfH+)DS zG70me3OV|JE99o%-e-hv;^8r(o!k1m3()=UDh}<$~W4@m`olw@v!I5S|6DPGN}*g;%g7WxPOk-zmvzcJ#^N!(@-=0}cjPn$-yl z?-F7UI$)OnRt2TrXwhvH|Aan+#a={|Fyh;6F}pdt_!G=LW5$S6gX8*>~#W-byiJ z-`c8Be4Dhdwy=Fk84)sxX+EpbpdxQg@^>|ROmCJCZcb#x6${)_P6Xx4L{g>~j${U7 zEP3t|68$aK19&T}oGLCNqO@hNdUPja>bgGLmK(u`F(H=ll_8OGuO+C#KE&XF_76CQ)o2ges}*zc*NT&|UniwFcn(Efeg~tI6RH rPG05Y_?pCtIrkqP#J`9BB9m?7)LiiGLg3i%=LBG7W@p-L3`zPQ%XmA` literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_background.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..228d0c6e1e8c957bbb8f4b429d1d36ae29816df7 GIT binary patch literal 594 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zf+;V3PE7aSW-L^Y-#W#sdlr2R3Z{ zpMO+gLIK;NldPt))n`9DUioju5NE`Y$)<3YVToje3G;+BhD$sSvlu;$M+Jv*faozV WW(_}{A}0<^D-52lelF{r5}E)!sjGzm literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_foreground.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1e690c30263c7255e09a30e4ac624b8541d49280 GIT binary patch literal 3387 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 literal 0 HcmV?d00001 diff --git a/src/PhysOn.Mobile.Android/Resources/values/colors.xml b/src/PhysOn.Mobile.Android/Resources/values/colors.xml new file mode 100644 index 0000000..9437d23 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/values/colors.xml @@ -0,0 +1,8 @@ + + + #F7F3EE + #FFFFFF + #DDD1C4 + #20242B + #F05B2B + diff --git a/src/PhysOn.Mobile.Android/Resources/values/ic_launcher_background.xml b/src/PhysOn.Mobile.Android/Resources/values/ic_launcher_background.xml new file mode 100644 index 0000000..6ec24e6 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/src/PhysOn.Mobile.Android/Resources/values/strings.xml b/src/PhysOn.Mobile.Android/Resources/values/strings.xml new file mode 100644 index 0000000..c3b649c --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + KoTalk + 다시 불러오기 + diff --git a/src/PhysOn.Mobile.Android/Resources/values/styles.xml b/src/PhysOn.Mobile.Android/Resources/values/styles.xml new file mode 100644 index 0000000..043bc62 --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/src/PhysOn.Mobile.Android/Resources/xml/network_security_config.xml b/src/PhysOn.Mobile.Android/Resources/xml/network_security_config.xml new file mode 100644 index 0000000..68be2bc --- /dev/null +++ b/src/PhysOn.Mobile.Android/Resources/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/PhysOn.Web/package-lock.json b/src/PhysOn.Web/package-lock.json index 524a1e5..7d01fd4 100644 --- a/src/PhysOn.Web/package-lock.json +++ b/src/PhysOn.Web/package-lock.json @@ -1,12 +1,12 @@ { "name": "physon-web", - "version": "0.1.0-alpha.5", + "version": "0.1.0-alpha.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "physon-web", - "version": "0.1.0-alpha.5", + "version": "0.1.0-alpha.6", "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/src/PhysOn.Web/package.json b/src/PhysOn.Web/package.json index b66ad08..23735de 100644 --- a/src/PhysOn.Web/package.json +++ b/src/PhysOn.Web/package.json @@ -1,7 +1,7 @@ { "name": "physon-web", "private": true, - "version": "0.1.0-alpha.5", + "version": "0.1.0-alpha.6", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/src/PhysOn.Web/src/App.tsx b/src/PhysOn.Web/src/App.tsx index 35de5b5..aa1f79b 100644 --- a/src/PhysOn.Web/src/App.tsx +++ b/src/PhysOn.Web/src/App.tsx @@ -53,7 +53,7 @@ type IconName = | 'group' 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 = { idle: '준비 중', diff --git a/문서/33-platform-capability-matrix.md b/문서/33-platform-capability-matrix.md index 32b550a..1720dbf 100644 --- a/문서/33-platform-capability-matrix.md +++ b/문서/33-platform-capability-matrix.md @@ -17,22 +17,22 @@ Windows, Mobile Web, Android를 병렬로 운영하면 언제든지 `문서상 | 기능 | Windows | Mobile Web | Android | 비고 | |---|---|---|---|---| -| 초간단 가입 | Buildable | Live | Planned | Alpha 기준 | -| 자기 자신과의 대화 | Buildable | Live | Planned | 첫 진입 루프 | -| 텍스트 메시지 전송 | Buildable | Live | Planned | 기본 루프 | -| 읽음 커서 갱신 | Partial | Partial | Planned | 복귀 정확도 보강 필요 | -| 실시간 수신 | Partial | Partial | Planned | 경계 조건 검증 부족 | -| 로컬 검색 | Partial | Partial | Planned | 제목/기본 검색 수준 | +| 초간단 가입 | Buildable | Live | Buildable | Android는 WebView 셸 기준 | +| 자기 자신과의 대화 | Buildable | Live | Buildable | 첫 진입 루프 | +| 텍스트 메시지 전송 | Buildable | Live | Buildable | 기본 루프 | +| 읽음 커서 갱신 | Partial | Partial | Partial | 복귀 정확도 보강 필요 | +| 실시간 수신 | Partial | Partial | Partial | 경계 조건 검증 부족 | +| 로컬 검색 | Partial | Partial | Partial | 제목/기본 검색 수준 | | 전역 검색 | Planned | Planned | Planned | 핵심 우선순위 | -| 드래프트 보존 | Partial | Partial | Planned | 실제 복원 체감 부족 | -| 세션 자동 갱신 | Planned | Planned | Planned | 현 시점 취약점 | +| 드래프트 보존 | Partial | Partial | Partial | 실제 복원 체감 부족 | +| 세션 자동 갱신 | Planned | Planned | Partial | WebView 셸 기준 | | 파일 첨부 | Planned | Planned | Planned | 다음 대규모 과제 | | 링크 프리뷰 | Planned | Planned | Planned | 제품성 핵심 | | 알림 묶음 정책 | Planned | Planned | Planned | 정책 문서화 완료 | | 팝아웃 창 | Planned | N/A | N/A | 데스크톱 핵심 | | 다중 창 | Planned | N/A | N/A | 데스크톱 핵심 | -| Android APK | N/A | N/A | Planned | 병렬 도입 예정 | -| Releases 배포 | Partial | Partial | Planned | 표면 정리 필요 | +| Android APK | N/A | N/A | Buildable | alpha 기준선 확보 | +| Releases 배포 | Partial | Partial | Buildable | 표면 정리 진행 중 | ## 제품 메시지 규칙 @@ -58,7 +58,13 @@ Windows, Mobile Web, Android를 병렬로 운영하면 언제든지 `문서상 - 장기적으로 일상 주사용 모바일 채널 - 푸시/미디어/백그라운드 안정성으로 모바일 웹을 보완 -- 아직 설계 우선 단계다 +- 현재는 WebView 기반 APK 셸을 확보했고, 장기적으로는 공용 네이티브 UI 축으로 옮길지 판단 중이다 + +## iOS / Linux + +- iOS는 저장소 Assets 직접 배포가 아니라 Apple 채널 기준으로 준비한다 +- Linux는 Windows와 같은 장기 네이티브 데스크톱 축으로 본다 +- 두 채널 모두 공용 UI 프레임워크 선택에 직접 연결되므로, Android 단독 최적화와 분리해 판단하면 안 된다 ## 기능 우선순위 매핑