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 0000000..adbab6d Binary files /dev/null and b/docs/assets/latest/kotalk-android-mockup.png differ 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 0000000..05cb38e Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png differ 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 0000000..8ce7c08 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_background.png differ diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_foreground.png b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_foreground.png new file mode 100644 index 0000000..5f5ccfa Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon_foreground.png differ 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 0000000..d19247e Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png differ diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_background.png b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_background.png new file mode 100644 index 0000000..d867285 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_background.png differ 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 0000000..20b637e Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon_foreground.png differ 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 0000000..4238084 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png differ 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 0000000..d890683 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_background.png differ 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 0000000..1dad34d Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon_foreground.png differ diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png new file mode 100644 index 0000000..57783e9 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png differ diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_background.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_background.png new file mode 100644 index 0000000..39e0160 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_background.png differ 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 0000000..db7b224 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon_foreground.png differ diff --git a/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png new file mode 100644 index 0000000..dc9464b Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png differ 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 0000000..228d0c6 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_background.png differ 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 0000000..1e690c3 Binary files /dev/null and b/src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon_foreground.png differ 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 단독 최적화와 분리해 판단하면 안 된다 ## 기능 우선순위 매핑