공개: 플랫폼 결론과 릴리즈 노트 규칙 반영
Some checks are pending
ci / server (push) Waiting to run
ci / web (push) Waiting to run
ci / desktop-windows (push) Waiting to run

This commit is contained in:
Ian 2026-04-16 13:54:11 +09:00
commit 799b975406
55 changed files with 1440 additions and 115 deletions

View file

@ -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
)

View file

@ -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 릴리즈 게시 스크립트가 지정 원격 기준으로 동작하고 최신 스크린샷 자산도 함께 첨부하도록 확장

160
CLIENT_PLATFORM_DECISION.md Normal file
View file

@ -0,0 +1,160 @@
# Client Platform Decision
마지막 정리일: `2026-04-16`
## 결론
KoTalk의 장기적인 네이티브 클라이언트 기준선은 **Avalonia 중심**으로 유지하는 편이 맞습니다.
현재 Android의 WebView 셸은 `alpha` 단계의 빠른 APK 배포를 위한 전술적 경로로 유지하되, iOS와 Linux까지 포함하는 장기 클라이언트 전략은 Avalonia를 축으로 정리합니다.
## 왜 다시 판단했는가
초기에는 `Windows + mobile web + Android APK`만 빨리 닫는 것이 우선이었습니다. 그런데 이제 조건이 바뀌었습니다.
- Android만이 아니라 iOS도 예정돼 있습니다.
- Linux 공식 지원도 예정돼 있습니다.
- Windows는 이미 네이티브 데스크톱 축으로 유지해야 합니다.
- 저장소와 공식 서비스 모두에서 UI/UX 일관성을 더 강하게 요구받고 있습니다.
즉, Android만 빨리 붙이는 기준이 아니라 `Windows + Android + iOS + Linux`를 함께 수용하는 클라이언트 구조가 필요해졌습니다.
## 현재 기준선
### 지금 이미 있는 것
- Windows 네이티브 데스크톱 앱: Avalonia 기반
- Mobile web: 실제 운영 중
- Android: WebView 기반 첫 APK 셸 생성 가능
### 지금 없는 것
- iOS 클라이언트
- Linux 네이티브 패키징
- Android/iOS 전용 네이티브 경험을 공용 UI 축으로 가져가는 체계
## 검토한 선택지
### 1. Android/iOS를 계속 각각 별도 네이티브 셸로 유지
장점:
- 지금 당장 APK를 빠르게 배포하기 쉽다
- 모바일 웹을 감싸는 방식이라 초기 유지 비용이 낮다
한계:
- Windows와 Linux 쪽 UI 자산을 재사용하기 어렵다
- iOS까지 같은 방식으로 늘어나면 표면은 빠르게 늘지만 제품 일관성은 약해진다
- 장기적으로는 플랫폼마다 다른 껍데기를 유지하게 된다
판단:
- **전술적으로는 유효**
- **장기 기준선으로는 부적합**
### 2. .NET MAUI로 장기 축 전환
장점:
- Android와 iOS, Windows까지 한 프레임워크로 묶기 좋다
- 모바일 장치 API 접근이 익숙한 생태계다
한계:
- Microsoft 공식 문서 기준 `.NET MAUI`의 기본 지원 플랫폼은 Android, iOS, macOS, Windows다. Linux는 기본 축에 없다.
- 현재 저장소의 Windows 클라이언트는 이미 Avalonia 기반이라, MAUI로 옮기면 데스크톱 축을 다시 크게 재구성해야 한다.
판단:
- **Linux 공식 지원 예정 조건과 현재 코드베이스 기준을 함께 놓고 보면, 주 프레임워크로 선택하기 어렵다**
참고:
- Microsoft Learn, “What is .NET MAUI?”: <https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui>
- Microsoft Learn, “Supported platforms for .NET MAUI apps”: <https://learn.microsoft.com/en-us/dotnet/maui/supported-platforms?view=net-maui-9.0>
### 3. Uno Platform으로 장기 축 전환
장점:
- 공식 문서 기준 Android, iOS, Web, macOS, Linux, Windows를 포괄한다
- 광범위한 플랫폼 표면 자체는 매력적이다
한계:
- 현재 Windows 클라이언트가 Avalonia 기반이라, Uno로 가면 데스크톱 UI 자산과 운영 경험을 다시 맞춰야 한다
- 지금 시점의 저장소 기준으로는 전환 비용이 작지 않다
판단:
- **대안으로는 충분히 검토 가능**
- **하지만 현재 코드베이스를 고려하면 1순위는 아님**
참고:
- Uno Platform, “Supported platforms”: <https://platform.uno/docs/articles/getting-started/requirements.html>
### 4. Avalonia를 장기 축으로 유지
장점:
- 현재 Windows 클라이언트가 이미 Avalonia 기반이다
- 공식 문서 기준 Windows, Linux, iOS, Android, WebAssembly까지 포괄한다
- 데스크톱 밀도, 멀티 윈도우, 플랫한 커스텀 UI를 유지하기 유리하다
- Linux 공식 지원 예정이라는 조건과 가장 자연스럽게 맞는다
전제 조건:
- Avalonia 공식 문서 기준 데스크톱은 `.NET 8` 이상이지만, 모바일인 iOS/Android는 `.NET 10` 기준을 따른다
- 따라서 현재 저장소의 `.NET 8` 기준선을 유지한 채 즉시 Android/iOS를 Avalonia로 통합하는 것은 현실적이지 않다
주의:
- 모바일은 데스크톱과 같은 방식으로 밀어붙이면 안 된다
- 작은 화면, 제스처, OS 관례를 반영한 모바일 전용 UX 조정은 별도로 필요하다
- 현재 Android alpha 셸을 곧바로 폐기할 필요는 없지만, 장기적으로는 공용 UI 구조와의 관계를 분명히 해야 한다
판단:
- **현 시점 KoTalk의 가장 현실적인 장기 기준선**
참고:
- Avalonia Docs, “Supported platforms”: <https://docs.avaloniaui.net/docs/supported-platforms>
- Avalonia Docs, “Welcome / What is Avalonia?”: <https://docs.avaloniaui.net/docs/welcome>
## 최종 결정
### 전술
- Android는 당분간 현재 WebView 셸을 유지합니다.
- 이유는 APK 배포, 설치 동선 검증, 알파 피드백 수집을 빠르게 계속하기 위해서입니다.
- 현재 저장소가 `.NET 8` 기준선이므로, 모바일 공용 UI로 바로 넘어가기 전까지는 이 경로가 가장 안전합니다.
### 전략
- 장기적인 네이티브 클라이언트 축은 **Avalonia 기반 공유 구조**로 정리합니다.
- 다만 이 단계는 `.NET 10` 모바일 기준선을 감당할 준비가 됐을 때 들어갑니다.
- Windows와 Linux는 같은 데스크톱 계열 기준으로 묶습니다.
- Android와 iOS는 같은 제품 경험 원칙 아래로 수렴시키되, 모바일 전용 UX 조정은 별도 계층으로 둡니다.
## 배포 원칙
- 자체 미러와 공개 저장소 Assets:
- Windows installer / onefile / zip
- Android APK
- Apple 배포 채널:
- iOS는 저장소 Assets나 자체 미러 직접 배포가 아니라 Apple 채널을 전제로 준비합니다
## 지금 바로 바뀌는 문장
- `Android는 계획 단계`라고 쓰지 않습니다.
- `Android APK 기준선은 확보됐고, 장기 모바일 전략은 재정의 중`이라고 씁니다.
- `iOS와 Linux는 예정`이라고 공개적으로 적되, 아직 완성된 것처럼 쓰지 않습니다.
## 다음 실행 항목
1. Android alpha 셸을 유지하면서 배포와 설치 루프를 먼저 안정화
2. `.NET 10` 전환 부담과 시점을 포함해 Avalonia 기반 공유 클라이언트 구조를 설계
3. Linux 패키징 기준선 정리
4. iOS 배포 준비 문서와 빌드 전제 정리

View file

@ -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-<version>.zip`
- `KoTalk-windows-x64-onefile-<version>.exe`
- `KoTalk-windows-x64-installer-<version>.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
```

View file

@ -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)

View file

@ -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

View file

@ -16,6 +16,10 @@
짧은 답장, 빠른 복귀, 설명 가능한 제품 표면, 그리고 한국어 사용 습관에 맞는 조용한 UI를 핵심 기준으로 삼습니다.
</p>
<p align="center">
현재는 Windows, 모바일 웹, Android APK 기준선을 운영하고 있으며, iOS와 Linux 네이티브 채널은 같은 제품 경험 안으로 수렴시키는 방향을 공개적으로 준비 중입니다.
</p>
<p align="center">
<a href="PROJECT_STATUS.md"><img alt="status" src="https://img.shields.io/badge/status-active%20alpha-166534"></a>
<a href="PROJECT_STATUS.md"><img alt="platforms" src="https://img.shields.io/badge/platforms-windows%20%7C%20web%20%7C%20android-1D4ED8"></a>
@ -31,6 +35,7 @@
<a href="SHOWCASE.md">Showcase</a> ·
<a href="BACKGROUND.md">Background</a> ·
<a href="ALTERNATIVE_GAP.md">Alternative Gap</a> ·
<a href="CLIENT_PLATFORM_DECISION.md">Platform Strategy</a> ·
<a href="branding/BRAND_GUIDE.md">Brand Guide</a> ·
<a href="FAQ.md">FAQ</a> ·
<a href="RELEASING.md">Releases</a> ·
@ -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)

View file

@ -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

View file

@ -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`를 기준 진입점으로 관리합니다.

View file

@ -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'"

View file

@ -13,4 +13,5 @@
- 현재 포함:
- Windows 데스크톱 셸
- Windows 온보딩/대화 화면
- Android APK shell 목업
- `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,85 @@
Unicode True
ManifestDPIAware True
RequestExecutionLevel user
SetCompressor /SOLID lzma
!include "MUI2.nsh"
!ifndef APP_NAME
!define APP_NAME "KoTalk"
!endif
!ifndef APP_COMPANY
!define APP_COMPANY "PHYSIA"
!endif
!ifndef APP_VERSION
!define APP_VERSION "0.1.0"
!endif
!ifndef SOURCE_DIR
!error "SOURCE_DIR define is required"
!endif
!ifndef OUTPUT_FILE
!error "OUTPUT_FILE define is required"
!endif
!ifndef APP_ICON
!define APP_ICON "branding/ico/kotalk.ico"
!endif
!define MUI_ABORTWARNING
!define MUI_ICON "${APP_ICON}"
!define MUI_UNICON "${APP_ICON}"
!define MUI_FINISHPAGE_RUN "$INSTDIR\KoTalk.exe"
!define MUI_FINISHPAGE_RUN_TEXT "KoTalk 실행"
Name "${APP_NAME}"
OutFile "${OUTPUT_FILE}"
InstallDir "$LOCALAPPDATA\KoTalk"
InstallDirRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "Korean"
Section "Install"
SetOutPath "$INSTDIR"
File /r "${SOURCE_DIR}/*"
WriteUninstaller "$INSTDIR\Uninstall KoTalk.exe"
WriteRegStr HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayName" "${APP_NAME}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "Publisher" "${APP_COMPANY}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayVersion" "${APP_VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "InstallLocation" "$INSTDIR"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayIcon" "$INSTDIR\KoTalk.exe"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "UninstallString" "$INSTDIR\Uninstall KoTalk.exe"
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoRepair" 1
CreateDirectory "$SMPROGRAMS\KoTalk"
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk" "$INSTDIR\Uninstall KoTalk.exe"
CreateShortcut "$DESKTOP\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
SectionEnd
Section "Uninstall"
Delete "$DESKTOP\KoTalk.lnk"
Delete "$SMPROGRAMS\KoTalk\KoTalk.lnk"
Delete "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk"
RMDir "$SMPROGRAMS\KoTalk"
Delete "$INSTDIR\Uninstall KoTalk.exe"
RMDir /r "$INSTDIR"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
DeleteRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}"
SectionEnd

View file

@ -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/<version>/windows/x64/...`
- 버전별 Android: `https://download-vstalk.phy.kr/releases/<version>/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/<version>/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/<version>`에 배포합니다.

View file

@ -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`
- 필요 시 한국어 스크린샷
- 최신 스크린샷은 이 변경 노트 하단에서 직접 확인할 수 있습니다.
## 확인할 것

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "branding" / "png" / "kotalk-transparent-1024.png"
ANDROID_RES = ROOT / "src" / "PhysOn.Mobile.Android" / "Resources"
BACKGROUND = (247, 243, 238, 255)
ICON_SIZES = {
"mipmap-mdpi": 48,
"mipmap-hdpi": 72,
"mipmap-xhdpi": 96,
"mipmap-xxhdpi": 144,
"mipmap-xxxhdpi": 192,
}
def resize_logo(size: int, scale: float) -> Image.Image:
source = Image.open(SOURCE).convert("RGBA")
logo_size = max(8, round(size * scale))
logo = source.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
offset = ((size - logo_size) // 2, (size - logo_size) // 2)
canvas.alpha_composite(logo, offset)
return canvas
def render_assets() -> None:
for folder, size in ICON_SIZES.items():
directory = ANDROID_RES / folder
directory.mkdir(parents=True, exist_ok=True)
background = Image.new("RGBA", (size, size), BACKGROUND)
foreground = resize_logo(size, 0.74)
full_icon = background.copy()
full_icon.alpha_composite(resize_logo(size, 0.64))
background.save(directory / "appicon_background.png")
foreground.save(directory / "appicon_foreground.png")
full_icon.save(directory / "appicon.png")
if __name__ == "__main__":
render_assets()

View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/build-android-apk.sh --version 2026.04.16-alpha.6 [options]
Options:
--configuration <name> Build configuration. Default: Release
--output <dir> Output directory. Default: artifacts/builds/<version>
--dotnet <path> Explicit dotnet binary path
EOF
}
version=""
configuration="Release"
output_dir=""
dotnet_bin="${DOTNET_BIN:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--configuration)
configuration="${2:-}"
shift 2
;;
--output)
output_dir="${2:-}"
shift 2
;;
--dotnet)
dotnet_bin="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$version" ]]; then
usage >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ -z "$dotnet_bin" ]]; then
if command -v dotnet >/dev/null 2>&1; then
dotnet_bin="$(command -v dotnet)"
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
dotnet_bin="$HOME/.dotnet/dotnet"
else
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
exit 1
fi
fi
if [[ -z "${JAVA_HOME:-}" && -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"
fi
if [[ -z "${ANDROID_SDK_ROOT:-}" ]]; then
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
fi
if [[ -z "$output_dir" ]]; then
output_dir="$repo_root/artifacts/builds/$version"
fi
publish_dir="$output_dir/android/publish"
apk_path="$output_dir/KoTalk-android-universal-$version.apk"
rm -rf "$publish_dir"
mkdir -p "$publish_dir" "$output_dir"
if [[ ! -d "$ANDROID_SDK_ROOT/platforms" ]]; then
mkdir -p "$ANDROID_SDK_ROOT"
"$dotnet_bin" build "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
-t:InstallAndroidDependencies \
-f net8.0-android \
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
-p:JavaSdkDirectory="${JAVA_HOME:-}" >/dev/null
fi
"$dotnet_bin" publish "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
-c "$configuration" \
-f net8.0-android \
-p:AndroidPackageFormat=apk \
-p:AndroidKeyStore=false \
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
-p:JavaSdkDirectory="${JAVA_HOME:-}" \
-o "$publish_dir"
apk_source="$(find "$publish_dir" -type f -name '*.apk' | head -n 1)"
if [[ -z "$apk_source" ]]; then
echo "Android publish did not produce an APK." >&2
exit 1
fi
cp "$apk_source" "$apk_path"
(
cd "$output_dir"
sha256sum "$(basename "$apk_path")" > KoTalk-android-universal-$version.apk.sha256
)
echo "Built Android APK in $output_dir"

View file

@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/build-windows-distributions.sh --version 2026.04.16-alpha.6 [options]
Options:
--configuration <name> Build configuration. Default: Release
--runtime <rid> Runtime identifier. Default: win-x64
--output <dir> Output directory. Default: artifacts/builds/<version>
--dotnet <path> Explicit dotnet binary path
EOF
}
version=""
configuration="Release"
runtime="win-x64"
output_dir=""
dotnet_bin="${DOTNET_BIN:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--configuration)
configuration="${2:-}"
shift 2
;;
--runtime)
runtime="${2:-}"
shift 2
;;
--output)
output_dir="${2:-}"
shift 2
;;
--dotnet)
dotnet_bin="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$version" ]]; then
usage >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ -z "$dotnet_bin" ]]; then
if command -v dotnet >/dev/null 2>&1; then
dotnet_bin="$(command -v dotnet)"
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
dotnet_bin="$HOME/.dotnet/dotnet"
else
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
exit 1
fi
fi
if [[ -z "$output_dir" ]]; then
output_dir="$repo_root/artifacts/builds/$version"
fi
publish_dir="$output_dir/publish/$runtime"
onefile_dir="$output_dir/onefile/$runtime"
zip_path="$output_dir/KoTalk-windows-x64-$version.zip"
portable_exe_path="$output_dir/KoTalk-windows-x64-onefile-$version.exe"
installer_path="$output_dir/KoTalk-windows-x64-installer-$version.exe"
installer_script="$repo_root/packaging/windows/KoTalkInstaller.nsi"
app_icon="$repo_root/branding/ico/kotalk.ico"
rm -rf "$publish_dir" "$onefile_dir"
mkdir -p "$publish_dir" "$onefile_dir" "$output_dir"
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
-c "$configuration" \
-r "$runtime" \
--self-contained true \
-p:DebugSymbols=false \
-p:DebugType=None \
-o "$publish_dir"
rm -f "$zip_path"
(cd "$publish_dir" && zip -rq "$zip_path" .)
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
-c "$configuration" \
-r "$runtime" \
--self-contained true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugSymbols=false \
-p:DebugType=None \
-o "$onefile_dir"
cp "$onefile_dir/KoTalk.exe" "$portable_exe_path"
makensis \
-DAPP_VERSION="$version" \
-DAPP_ICON="$app_icon" \
-DSOURCE_DIR="$publish_dir" \
-DOUTPUT_FILE="$installer_path" \
"$installer_script" >/dev/null
(
cd "$output_dir"
sha256sum \
"$(basename "$zip_path")" \
"$(basename "$portable_exe_path")" \
"$(basename "$installer_path")" \
> SHA256SUMS.txt
)
echo "Built Windows release assets in $output_dir"

View file

@ -8,6 +8,10 @@ Usage:
Options:
--windows-zip <path> Windows x64 ZIP artifact path
--windows-portable-exe <path>
Windows onefile portable EXE artifact path
--windows-installer-exe <path>
Windows installer EXE artifact path
--android-apk <path> Android universal APK artifact path
--zip <path> Backward-compatible alias for --windows-zip
--channel <name> 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 <<EOF
"name": "KoTalk for Windows",
"kind": "desktop",
"arch": "x64",
"latestUrl": "$download_base_url/windows/latest",
"portableZipUrl": "$download_base_url/windows/latest/$windows_latest_name",
"sha256Url": "$download_base_url/windows/latest/SHA256SUMS.txt"
EOF
)"
windows_platform_body="$(join_json_lines "${windows_platform_lines[@]}")"
append_platform_json "$(cat <<EOF
"windows": {
@ -298,12 +398,12 @@ if (( ${#latest_hash_paths[@]} > 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 <<EOF
<a class="card" href="/windows/latest">
<span class="eyebrow">Windows</span>
<strong>Latest Windows build</strong>
<span>ZIP package and SHA256 checksum</span>
<span>Installer, onefile portable, ZIP, SHA256</span>
</a>
EOF
)"
@ -321,6 +421,156 @@ EOF
)"
fi
windows_installer_card=""
if [[ -n "$windows_installer_exe" ]]; then
windows_installer_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64-installer.exe">
<span class="eyebrow">Installer</span>
<strong>Windows installer</strong>
<span>설치형 EXE</span>
</a>
EOF
)"
fi
windows_portable_card=""
if [[ -n "$windows_portable_exe" ]]; then
windows_portable_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64-onefile.exe">
<span class="eyebrow">Portable</span>
<strong>Onefile executable</strong>
<span>압축 해제 없이 바로 실행</span>
</a>
EOF
)"
fi
windows_zip_card=""
if [[ -n "$windows_zip" ]]; then
windows_zip_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64.zip">
<span class="eyebrow">Archive</span>
<strong>Extracted bundle ZIP</strong>
<span>폴더 배포본 압축 파일</span>
</a>
EOF
)"
fi
if [[ -d "${latest_root}/windows" ]]; then
cat > "${latest_root}/windows/index.html" <<EOF
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KoTalk for Windows</title>
<style>
:root {
color-scheme: light;
--bg: #f7f3ee;
--surface: #ffffff;
--border: #ddd1c4;
--text: #20242b;
--soft: #5f5a54;
--accent: #f05b2b;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
main { max-width: 920px; margin: 0 auto; padding: 40px 20px 56px; }
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
.hero { padding: 24px; }
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
p { margin: 0; line-height: 1.6; color: var(--soft); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
.card:hover { border-color: var(--accent); }
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
.meta { margin-top: 16px; color: var(--soft); }
.meta a { color: inherit; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>KoTalk for Windows</h1>
<p>설치형과 onefile portable, 압축본을 같은 기준선으로 제공합니다.</p>
<div class="grid">
$windows_installer_card
$windows_portable_card
$windows_zip_card
<a class="card" href="/windows/latest/SHA256SUMS.txt">
<span class="eyebrow">Integrity</span>
<strong>SHA256SUMS</strong>
<span>체크섬 확인</span>
</a>
</div>
<p class="meta"><a href="/latest/version.json">version.json</a></p>
</section>
</main>
</body>
</html>
EOF
fi
if [[ -d "${latest_root}/android" ]]; then
cat > "${latest_root}/android/index.html" <<EOF
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KoTalk for Android</title>
<style>
:root {
color-scheme: light;
--bg: #f7f3ee;
--surface: #ffffff;
--border: #ddd1c4;
--text: #20242b;
--soft: #5f5a54;
--accent: #f05b2b;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
main { max-width: 760px; margin: 0 auto; padding: 40px 20px 56px; }
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
.hero { padding: 24px; }
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
p { margin: 0; line-height: 1.6; color: var(--soft); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
.card:hover { border-color: var(--accent); }
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
.meta { margin-top: 16px; color: var(--soft); }
.meta a { color: inherit; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>KoTalk for Android</h1>
<p>KoTalk Android shell APK와 버전 메타데이터를 같은 기준선으로 제공합니다.</p>
<div class="grid">
<a class="card" href="/android/latest/KoTalk-android-universal.apk">
<span class="eyebrow">APK</span>
<strong>Universal APK</strong>
<span>직접 설치용 패키지</span>
</a>
<a class="card" href="/android/latest/SHA256SUMS.txt">
<span class="eyebrow">Integrity</span>
<strong>SHA256SUMS</strong>
<span>체크섬 확인</span>
</a>
</div>
<p class="meta"><a href="/latest/version.json">version.json</a></p>
</section>
</main>
</body>
</html>
EOF
fi
cat > "$download_root/index.html" <<EOF
<!doctype html>
<html lang="ko">

View file

@ -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

View file

@ -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

View file

@ -12,10 +12,10 @@
<Product>KoTalk</Product>
<Description>한국어 중심의 차분한 메시징 경험을 다시 설계하는 Windows-first 메신저</Description>
<AssemblyTitle>KoTalk</AssemblyTitle>
<AssemblyVersion>0.1.0.5</AssemblyVersion>
<FileVersion>0.1.0.5</FileVersion>
<Version>0.1.0-alpha.5</Version>
<InformationalVersion>0.1.0-alpha.5</InformationalVersion>
<AssemblyVersion>0.1.0.6</AssemblyVersion>
<FileVersion>0.1.0.6</FileVersion>
<Version>0.1.0-alpha.6</Version>
<InformationalVersion>0.1.0-alpha.6</InformationalVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>

View file

@ -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;

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/appicon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true"
android:usesCleartextTraffic="false">
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View file

@ -0,0 +1,268 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Net;
using Android.Net.Http;
using Android.OS;
using Android.Views;
using Android.Webkit;
using Android.Widget;
namespace PhysOn.Mobile.Android;
[Activity(
Label = "@string/app_name",
MainLauncher = true,
Exported = true,
LaunchMode = LaunchMode.SingleTask,
Theme = "@style/KoTalkTheme",
ConfigurationChanges =
ConfigChanges.Orientation |
ConfigChanges.ScreenSize |
ConfigChanges.SmallestScreenSize |
ConfigChanges.ScreenLayout |
ConfigChanges.UiMode |
ConfigChanges.KeyboardHidden |
ConfigChanges.Density)]
public class MainActivity : Activity
{
private const string AppVersion = "0.1.0-alpha.6";
private const string HomeUrl = "https://vstalk.phy.kr";
private static readonly HashSet<string> AllowedHosts = new(StringComparer.OrdinalIgnoreCase)
{
"vstalk.phy.kr",
"download-vstalk.phy.kr"
};
private WebView? _webView;
private ProgressBar? _loadingBar;
private View? _offlineOverlay;
private ImageButton? _retryButton;
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
ConfigureWindowChrome();
SetContentView(Resource.Layout.activity_main);
_webView = FindViewById<WebView>(Resource.Id.app_webview);
_loadingBar = FindViewById<ProgressBar>(Resource.Id.loading_bar);
_offlineOverlay = FindViewById<View>(Resource.Id.offline_overlay);
_retryButton = FindViewById<ImageButton>(Resource.Id.retry_button);
if (_webView is null || _loadingBar is null || _offlineOverlay is null || _retryButton is null)
{
throw new InvalidOperationException("KoTalk Android shell layout failed to load.");
}
_retryButton.Click += HandleRetryClick;
ConfigureWebView(_webView, _loadingBar);
if (savedInstanceState is not null)
{
_webView.RestoreState(savedInstanceState);
}
else
{
_webView.LoadUrl(HomeUrl);
}
}
protected override void OnSaveInstanceState(Bundle outState)
{
base.OnSaveInstanceState(outState);
_webView?.SaveState(outState);
}
protected override void OnDestroy()
{
if (_retryButton is not null)
{
_retryButton.Click -= HandleRetryClick;
}
if (_webView is not null)
{
_webView.StopLoading();
_webView.Destroy();
_webView = null;
}
base.OnDestroy();
}
public override void OnBackPressed()
{
if (_webView?.CanGoBack() == true)
{
_webView.GoBack();
return;
}
#pragma warning disable CA1422
base.OnBackPressed();
#pragma warning restore CA1422
}
private void HandleRetryClick(object? sender, EventArgs e)
{
_webView?.Reload();
}
private void ConfigureWindowChrome()
{
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
if (Window is null)
{
return;
}
Window.SetStatusBarColor(Color.ParseColor("#F7F3EE"));
Window.SetNavigationBarColor(Color.ParseColor("#F7F3EE"));
}
private void ConfigureWebView(WebView webView, ProgressBar loadingBar)
{
WebView.SetWebContentsDebuggingEnabled(System.Diagnostics.Debugger.IsAttached);
var settings = webView.Settings!;
settings.JavaScriptEnabled = true;
settings.DomStorageEnabled = true;
settings.DatabaseEnabled = true;
settings.AllowFileAccess = false;
settings.AllowContentAccess = false;
settings.SetSupportZoom(false);
settings.BuiltInZoomControls = false;
settings.DisplayZoomControls = false;
settings.LoadWithOverviewMode = true;
settings.UseWideViewPort = true;
settings.MixedContentMode = MixedContentHandling.NeverAllow;
settings.CacheMode = CacheModes.Default;
settings.MediaPlaybackRequiresUserGesture = true;
settings.UserAgentString = $"{settings.UserAgentString} KoTalkAndroid/{AppVersion}";
var cookies = CookieManager.Instance;
cookies?.SetAcceptCookie(true);
cookies?.SetAcceptThirdPartyCookies(webView, false);
webView.SetBackgroundColor(Color.ParseColor("#F7F3EE"));
webView.SetWebChromeClient(new KoTalkWebChromeClient(loadingBar));
webView.SetWebViewClient(
new KoTalkWebViewClient(
AllowedHosts,
ShowOfflineOverlay,
HideOfflineOverlay));
}
private void ShowOfflineOverlay()
{
RunOnUiThread(() =>
{
if (_offlineOverlay is not null)
{
_offlineOverlay.Visibility = ViewStates.Visible;
}
if (_loadingBar is not null)
{
_loadingBar.Visibility = ViewStates.Invisible;
}
});
}
private void HideOfflineOverlay()
{
RunOnUiThread(() =>
{
if (_offlineOverlay is not null)
{
_offlineOverlay.Visibility = ViewStates.Gone;
}
});
}
private sealed class KoTalkWebChromeClient(ProgressBar loadingBar) : WebChromeClient
{
public override void OnProgressChanged(WebView? view, int newProgress)
{
base.OnProgressChanged(view, newProgress);
loadingBar.Progress = newProgress;
loadingBar.Visibility = newProgress >= 100 ? ViewStates.Invisible : ViewStates.Visible;
}
}
private sealed class KoTalkWebViewClient(
IReadOnlySet<string> allowedHosts,
Action showOfflineOverlay,
Action hideOfflineOverlay) : WebViewClient
{
public override bool ShouldOverrideUrlLoading(WebView? view, IWebResourceRequest? request)
{
if (request?.Url is null)
{
return false;
}
var url = request.Url;
var scheme = url.Scheme?.ToLowerInvariant();
var host = url.Host?.ToLowerInvariant();
if (scheme is "http" or "https" && host is not null && allowedHosts.Contains(host))
{
return false;
}
if (view?.Context is null)
{
return true;
}
try
{
var intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(url.ToString()));
intent.AddFlags(ActivityFlags.NewTask);
view.Context.StartActivity(intent);
}
catch (ActivityNotFoundException)
{
showOfflineOverlay();
}
return true;
}
public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
{
base.OnPageStarted(view, url, favicon);
hideOfflineOverlay();
}
public override void OnPageFinished(WebView? view, string? url)
{
base.OnPageFinished(view, url);
hideOfflineOverlay();
}
public override void OnReceivedError(
WebView? view,
IWebResourceRequest? request,
WebResourceError? error)
{
base.OnReceivedError(view, request, error);
if (request?.IsForMainFrame ?? true)
{
showOfflineOverlay();
}
}
public override void OnReceivedSslError(WebView? view, SslErrorHandler? handler, SslError? error)
{
handler?.Cancel();
showOfflineOverlay();
}
}
}

View file

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-android</TargetFramework>
<SupportedOSPlatformVersion>26</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>PhysOn.Mobile.Android</RootNamespace>
<AssemblyName>KoTalk.Mobile.Android</AssemblyName>
<ApplicationId>kr.physia.kotalk</ApplicationId>
<ApplicationVersion>6</ApplicationVersion>
<ApplicationDisplayVersion>0.1.0-alpha.6</ApplicationDisplayVersion>
<Company>PHYSIA</Company>
<Authors>PHYSIA</Authors>
<Product>KoTalk</Product>
<Description>KoTalk Android shell for the live Korean messaging experience</Description>
<ApplicationTitle>KoTalk</ApplicationTitle>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidUseDesignerAssembly>false</AndroidUseDesignerAssembly>
</PropertyGroup>
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and '$(JAVA_HOME)' != ''">
<JavaSdkDirectory>$(JAVA_HOME)</JavaSdkDirectory>
</PropertyGroup>
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and Exists('/usr/lib/jvm/java-17-openjdk-amd64')">
<JavaSdkDirectory>/usr/lib/jvm/java-17-openjdk-amd64</JavaSdkDirectory>
</PropertyGroup>
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and '$(ANDROID_SDK_ROOT)' != ''">
<AndroidSdkDirectory>$(ANDROID_SDK_ROOT)</AndroidSdkDirectory>
</PropertyGroup>
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and Exists('$([System.Environment]::GetEnvironmentVariable(`HOME`))/Android/Sdk')">
<AndroidSdkDirectory>$([System.Environment]::GetEnvironmentVariable('HOME'))/Android/Sdk</AndroidSdkDirectory>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,44 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.xml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.xml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "Resource"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the Resource class would expose:
public class Resource {
public class Drawable {
public const int icon = 0x123;
}
public class Layout {
public const int main = 0x456;
}
public class Strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or
Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string
to reference the first string in the dictionary file values/strings.xml.

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/surface_card" />
<stroke android:width="1dp" android:color="@color/surface_border" />
<corners android:radius="2dp" />
</shape>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/surface_canvas">
<WebView
android:id="@+id/app_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
android:overScrollMode="never" />
<ProgressBar
android:id="@+id/loading_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_gravity="top"
android:indeterminate="false"
android:max="100"
android:progressBackgroundTint="@color/surface_border"
android:progressTint="@color/accent_primary"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/offline_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="28dp"
android:visibility="gone">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginBottom="20dp"
android:contentDescription="@string/app_name"
android:src="@mipmap/appicon" />
<ImageButton
android:id="@+id/retry_button"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/retry_button_background"
android:contentDescription="@string/reload"
android:padding="16dp"
android:scaleType="centerInside"
android:src="@android:drawable/ic_popup_sync" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/appicon_background" />
<foreground android:drawable="@mipmap/appicon_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/appicon_background" />
<foreground android:drawable="@mipmap/appicon_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="surface_canvas">#F7F3EE</color>
<color name="surface_card">#FFFFFF</color>
<color name="surface_border">#DDD1C4</color>
<color name="ink_primary">#20242B</color>
<color name="accent_primary">#F05B2B</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#2C3E50</color>
</resources>

View file

@ -0,0 +1,4 @@
<resources>
<string name="app_name">KoTalk</string>
<string name="reload">다시 불러오기</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="KoTalkTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
<item name="android:windowBackground">@color/surface_canvas</item>
<item name="android:statusBarColor">@color/surface_canvas</item>
<item name="android:navigationBarColor">@color/surface_canvas</item>
</style>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>

View file

@ -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"

View file

@ -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",

View file

@ -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<ConnectionState, string> = {
idle: '준비 중',

View file

@ -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 단독 최적화와 분리해 판단하면 안 된다
## 기능 우선순위 매핑