공개: 플랫폼 결론과 릴리즈 노트 규칙 반영
54
.github/workflows/release-portable.yml
vendored
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -0,0 +1,160 @@
|
|||
# Client Platform Decision
|
||||
|
||||
마지막 정리일: `2026-04-16`
|
||||
|
||||
## 결론
|
||||
|
||||
KoTalk의 장기적인 네이티브 클라이언트 기준선은 **Avalonia 중심**으로 유지하는 편이 맞습니다.
|
||||
현재 Android의 WebView 셸은 `alpha` 단계의 빠른 APK 배포를 위한 전술적 경로로 유지하되, iOS와 Linux까지 포함하는 장기 클라이언트 전략은 Avalonia를 축으로 정리합니다.
|
||||
|
||||
## 왜 다시 판단했는가
|
||||
|
||||
초기에는 `Windows + mobile web + Android APK`만 빨리 닫는 것이 우선이었습니다. 그런데 이제 조건이 바뀌었습니다.
|
||||
|
||||
- Android만이 아니라 iOS도 예정돼 있습니다.
|
||||
- Linux 공식 지원도 예정돼 있습니다.
|
||||
- Windows는 이미 네이티브 데스크톱 축으로 유지해야 합니다.
|
||||
- 저장소와 공식 서비스 모두에서 UI/UX 일관성을 더 강하게 요구받고 있습니다.
|
||||
|
||||
즉, Android만 빨리 붙이는 기준이 아니라 `Windows + Android + iOS + Linux`를 함께 수용하는 클라이언트 구조가 필요해졌습니다.
|
||||
|
||||
## 현재 기준선
|
||||
|
||||
### 지금 이미 있는 것
|
||||
|
||||
- Windows 네이티브 데스크톱 앱: Avalonia 기반
|
||||
- Mobile web: 실제 운영 중
|
||||
- Android: WebView 기반 첫 APK 셸 생성 가능
|
||||
|
||||
### 지금 없는 것
|
||||
|
||||
- iOS 클라이언트
|
||||
- Linux 네이티브 패키징
|
||||
- Android/iOS 전용 네이티브 경험을 공용 UI 축으로 가져가는 체계
|
||||
|
||||
## 검토한 선택지
|
||||
|
||||
### 1. Android/iOS를 계속 각각 별도 네이티브 셸로 유지
|
||||
|
||||
장점:
|
||||
|
||||
- 지금 당장 APK를 빠르게 배포하기 쉽다
|
||||
- 모바일 웹을 감싸는 방식이라 초기 유지 비용이 낮다
|
||||
|
||||
한계:
|
||||
|
||||
- Windows와 Linux 쪽 UI 자산을 재사용하기 어렵다
|
||||
- iOS까지 같은 방식으로 늘어나면 표면은 빠르게 늘지만 제품 일관성은 약해진다
|
||||
- 장기적으로는 플랫폼마다 다른 껍데기를 유지하게 된다
|
||||
|
||||
판단:
|
||||
|
||||
- **전술적으로는 유효**
|
||||
- **장기 기준선으로는 부적합**
|
||||
|
||||
### 2. .NET MAUI로 장기 축 전환
|
||||
|
||||
장점:
|
||||
|
||||
- Android와 iOS, Windows까지 한 프레임워크로 묶기 좋다
|
||||
- 모바일 장치 API 접근이 익숙한 생태계다
|
||||
|
||||
한계:
|
||||
|
||||
- Microsoft 공식 문서 기준 `.NET MAUI`의 기본 지원 플랫폼은 Android, iOS, macOS, Windows다. Linux는 기본 축에 없다.
|
||||
- 현재 저장소의 Windows 클라이언트는 이미 Avalonia 기반이라, MAUI로 옮기면 데스크톱 축을 다시 크게 재구성해야 한다.
|
||||
|
||||
판단:
|
||||
|
||||
- **Linux 공식 지원 예정 조건과 현재 코드베이스 기준을 함께 놓고 보면, 주 프레임워크로 선택하기 어렵다**
|
||||
|
||||
참고:
|
||||
|
||||
- Microsoft Learn, “What is .NET MAUI?”: <https://learn.microsoft.com/en-us/dotnet/maui/what-is-maui>
|
||||
- Microsoft Learn, “Supported platforms for .NET MAUI apps”: <https://learn.microsoft.com/en-us/dotnet/maui/supported-platforms?view=net-maui-9.0>
|
||||
|
||||
### 3. Uno Platform으로 장기 축 전환
|
||||
|
||||
장점:
|
||||
|
||||
- 공식 문서 기준 Android, iOS, Web, macOS, Linux, Windows를 포괄한다
|
||||
- 광범위한 플랫폼 표면 자체는 매력적이다
|
||||
|
||||
한계:
|
||||
|
||||
- 현재 Windows 클라이언트가 Avalonia 기반이라, Uno로 가면 데스크톱 UI 자산과 운영 경험을 다시 맞춰야 한다
|
||||
- 지금 시점의 저장소 기준으로는 전환 비용이 작지 않다
|
||||
|
||||
판단:
|
||||
|
||||
- **대안으로는 충분히 검토 가능**
|
||||
- **하지만 현재 코드베이스를 고려하면 1순위는 아님**
|
||||
|
||||
참고:
|
||||
|
||||
- Uno Platform, “Supported platforms”: <https://platform.uno/docs/articles/getting-started/requirements.html>
|
||||
|
||||
### 4. Avalonia를 장기 축으로 유지
|
||||
|
||||
장점:
|
||||
|
||||
- 현재 Windows 클라이언트가 이미 Avalonia 기반이다
|
||||
- 공식 문서 기준 Windows, Linux, iOS, Android, WebAssembly까지 포괄한다
|
||||
- 데스크톱 밀도, 멀티 윈도우, 플랫한 커스텀 UI를 유지하기 유리하다
|
||||
- Linux 공식 지원 예정이라는 조건과 가장 자연스럽게 맞는다
|
||||
|
||||
전제 조건:
|
||||
|
||||
- Avalonia 공식 문서 기준 데스크톱은 `.NET 8` 이상이지만, 모바일인 iOS/Android는 `.NET 10` 기준을 따른다
|
||||
- 따라서 현재 저장소의 `.NET 8` 기준선을 유지한 채 즉시 Android/iOS를 Avalonia로 통합하는 것은 현실적이지 않다
|
||||
|
||||
주의:
|
||||
|
||||
- 모바일은 데스크톱과 같은 방식으로 밀어붙이면 안 된다
|
||||
- 작은 화면, 제스처, OS 관례를 반영한 모바일 전용 UX 조정은 별도로 필요하다
|
||||
- 현재 Android alpha 셸을 곧바로 폐기할 필요는 없지만, 장기적으로는 공용 UI 구조와의 관계를 분명히 해야 한다
|
||||
|
||||
판단:
|
||||
|
||||
- **현 시점 KoTalk의 가장 현실적인 장기 기준선**
|
||||
|
||||
참고:
|
||||
|
||||
- Avalonia Docs, “Supported platforms”: <https://docs.avaloniaui.net/docs/supported-platforms>
|
||||
- Avalonia Docs, “Welcome / What is Avalonia?”: <https://docs.avaloniaui.net/docs/welcome>
|
||||
|
||||
## 최종 결정
|
||||
|
||||
### 전술
|
||||
|
||||
- Android는 당분간 현재 WebView 셸을 유지합니다.
|
||||
- 이유는 APK 배포, 설치 동선 검증, 알파 피드백 수집을 빠르게 계속하기 위해서입니다.
|
||||
- 현재 저장소가 `.NET 8` 기준선이므로, 모바일 공용 UI로 바로 넘어가기 전까지는 이 경로가 가장 안전합니다.
|
||||
|
||||
### 전략
|
||||
|
||||
- 장기적인 네이티브 클라이언트 축은 **Avalonia 기반 공유 구조**로 정리합니다.
|
||||
- 다만 이 단계는 `.NET 10` 모바일 기준선을 감당할 준비가 됐을 때 들어갑니다.
|
||||
- Windows와 Linux는 같은 데스크톱 계열 기준으로 묶습니다.
|
||||
- Android와 iOS는 같은 제품 경험 원칙 아래로 수렴시키되, 모바일 전용 UX 조정은 별도 계층으로 둡니다.
|
||||
|
||||
## 배포 원칙
|
||||
|
||||
- 자체 미러와 공개 저장소 Assets:
|
||||
- Windows installer / onefile / zip
|
||||
- Android APK
|
||||
- Apple 배포 채널:
|
||||
- iOS는 저장소 Assets나 자체 미러 직접 배포가 아니라 Apple 채널을 전제로 준비합니다
|
||||
|
||||
## 지금 바로 바뀌는 문장
|
||||
|
||||
- `Android는 계획 단계`라고 쓰지 않습니다.
|
||||
- `Android APK 기준선은 확보됐고, 장기 모바일 전략은 재정의 중`이라고 씁니다.
|
||||
- `iOS와 Linux는 예정`이라고 공개적으로 적되, 아직 완성된 것처럼 쓰지 않습니다.
|
||||
|
||||
## 다음 실행 항목
|
||||
|
||||
1. Android alpha 셸을 유지하면서 배포와 설치 루프를 먼저 안정화
|
||||
2. `.NET 10` 전환 부담과 시점을 포함해 Avalonia 기반 공유 클라이언트 구조를 설계
|
||||
3. Linux 패키징 기준선 정리
|
||||
4. iOS 배포 준비 문서와 빌드 전제 정리
|
||||
|
|
@ -80,22 +80,21 @@ dotnet test PhysOn.sln -c Debug
|
|||
Windows:
|
||||
|
||||
```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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
18
README.md
|
|
@ -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)
|
||||
|
|
|
|||
12
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
|
||||
|
||||
|
|
|
|||
18
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`를 기준 진입점으로 관리합니다.
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@
|
|||
- 현재 포함:
|
||||
- Windows 데스크톱 셸
|
||||
- Windows 온보딩/대화 화면
|
||||
- Android APK shell 목업
|
||||
- `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면
|
||||
|
|
|
|||
BIN
docs/assets/latest/kotalk-android-mockup.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
85
packaging/windows/KoTalkInstaller.nsi
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
Unicode True
|
||||
ManifestDPIAware True
|
||||
RequestExecutionLevel user
|
||||
SetCompressor /SOLID lzma
|
||||
|
||||
!include "MUI2.nsh"
|
||||
|
||||
!ifndef APP_NAME
|
||||
!define APP_NAME "KoTalk"
|
||||
!endif
|
||||
|
||||
!ifndef APP_COMPANY
|
||||
!define APP_COMPANY "PHYSIA"
|
||||
!endif
|
||||
|
||||
!ifndef APP_VERSION
|
||||
!define APP_VERSION "0.1.0"
|
||||
!endif
|
||||
|
||||
!ifndef SOURCE_DIR
|
||||
!error "SOURCE_DIR define is required"
|
||||
!endif
|
||||
|
||||
!ifndef OUTPUT_FILE
|
||||
!error "OUTPUT_FILE define is required"
|
||||
!endif
|
||||
|
||||
!ifndef APP_ICON
|
||||
!define APP_ICON "branding/ico/kotalk.ico"
|
||||
!endif
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_ICON "${APP_ICON}"
|
||||
!define MUI_UNICON "${APP_ICON}"
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\KoTalk.exe"
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "KoTalk 실행"
|
||||
|
||||
Name "${APP_NAME}"
|
||||
OutFile "${OUTPUT_FILE}"
|
||||
InstallDir "$LOCALAPPDATA\KoTalk"
|
||||
InstallDirRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_LANGUAGE "Korean"
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
File /r "${SOURCE_DIR}/*"
|
||||
WriteUninstaller "$INSTDIR\Uninstall KoTalk.exe"
|
||||
|
||||
WriteRegStr HKCU "Software\${APP_COMPANY}\${APP_NAME}" "InstallDir" "$INSTDIR"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayName" "${APP_NAME}"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "Publisher" "${APP_COMPANY}"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayVersion" "${APP_VERSION}"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayIcon" "$INSTDIR\KoTalk.exe"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "UninstallString" "$INSTDIR\Uninstall KoTalk.exe"
|
||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoModify" 1
|
||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "NoRepair" 1
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\KoTalk"
|
||||
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
|
||||
CreateShortcut "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk" "$INSTDIR\Uninstall KoTalk.exe"
|
||||
CreateShortcut "$DESKTOP\KoTalk.lnk" "$INSTDIR\KoTalk.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$DESKTOP\KoTalk.lnk"
|
||||
Delete "$SMPROGRAMS\KoTalk\KoTalk.lnk"
|
||||
Delete "$SMPROGRAMS\KoTalk\KoTalk 제거.lnk"
|
||||
RMDir "$SMPROGRAMS\KoTalk"
|
||||
|
||||
Delete "$INSTDIR\Uninstall KoTalk.exe"
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
|
||||
DeleteRegKey HKCU "Software\${APP_COMPANY}\${APP_NAME}"
|
||||
SectionEnd
|
||||
|
|
@ -19,10 +19,14 @@ release-assets/
|
|||
SHA256SUMS.txt
|
||||
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>`에 배포합니다.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
- 필요 시 한국어 스크린샷
|
||||
- 최신 스크린샷은 이 변경 노트 하단에서 직접 확인할 수 있습니다.
|
||||
|
||||
## 확인할 것
|
||||
|
||||
|
|
|
|||
50
scripts/branding/generate_android_icon_assets.py
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "branding" / "png" / "kotalk-transparent-1024.png"
|
||||
ANDROID_RES = ROOT / "src" / "PhysOn.Mobile.Android" / "Resources"
|
||||
|
||||
BACKGROUND = (247, 243, 238, 255)
|
||||
ICON_SIZES = {
|
||||
"mipmap-mdpi": 48,
|
||||
"mipmap-hdpi": 72,
|
||||
"mipmap-xhdpi": 96,
|
||||
"mipmap-xxhdpi": 144,
|
||||
"mipmap-xxxhdpi": 192,
|
||||
}
|
||||
|
||||
|
||||
def resize_logo(size: int, scale: float) -> Image.Image:
|
||||
source = Image.open(SOURCE).convert("RGBA")
|
||||
logo_size = max(8, round(size * scale))
|
||||
logo = source.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||
|
||||
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
offset = ((size - logo_size) // 2, (size - logo_size) // 2)
|
||||
canvas.alpha_composite(logo, offset)
|
||||
return canvas
|
||||
|
||||
|
||||
def render_assets() -> None:
|
||||
for folder, size in ICON_SIZES.items():
|
||||
directory = ANDROID_RES / folder
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
background = Image.new("RGBA", (size, size), BACKGROUND)
|
||||
foreground = resize_logo(size, 0.74)
|
||||
full_icon = background.copy()
|
||||
full_icon.alpha_composite(resize_logo(size, 0.64))
|
||||
|
||||
background.save(directory / "appicon_background.png")
|
||||
foreground.save(directory / "appicon_foreground.png")
|
||||
full_icon.save(directory / "appicon.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
render_assets()
|
||||
119
scripts/release/build-android-apk.sh
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/build-android-apk.sh --version 2026.04.16-alpha.6 [options]
|
||||
|
||||
Options:
|
||||
--configuration <name> Build configuration. Default: Release
|
||||
--output <dir> Output directory. Default: artifacts/builds/<version>
|
||||
--dotnet <path> Explicit dotnet binary path
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
configuration="Release"
|
||||
output_dir=""
|
||||
dotnet_bin="${DOTNET_BIN:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--configuration)
|
||||
configuration="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
output_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dotnet)
|
||||
dotnet_bin="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
if [[ -z "$dotnet_bin" ]]; then
|
||||
if command -v dotnet >/dev/null 2>&1; then
|
||||
dotnet_bin="$(command -v dotnet)"
|
||||
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||
dotnet_bin="$HOME/.dotnet/dotnet"
|
||||
else
|
||||
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${JAVA_HOME:-}" && -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
fi
|
||||
|
||||
if [[ -z "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
|
||||
fi
|
||||
|
||||
if [[ -z "$output_dir" ]]; then
|
||||
output_dir="$repo_root/artifacts/builds/$version"
|
||||
fi
|
||||
|
||||
publish_dir="$output_dir/android/publish"
|
||||
apk_path="$output_dir/KoTalk-android-universal-$version.apk"
|
||||
|
||||
rm -rf "$publish_dir"
|
||||
mkdir -p "$publish_dir" "$output_dir"
|
||||
|
||||
if [[ ! -d "$ANDROID_SDK_ROOT/platforms" ]]; then
|
||||
mkdir -p "$ANDROID_SDK_ROOT"
|
||||
"$dotnet_bin" build "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
|
||||
-t:InstallAndroidDependencies \
|
||||
-f net8.0-android \
|
||||
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
|
||||
-p:JavaSdkDirectory="${JAVA_HOME:-}" >/dev/null
|
||||
fi
|
||||
|
||||
"$dotnet_bin" publish "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
|
||||
-c "$configuration" \
|
||||
-f net8.0-android \
|
||||
-p:AndroidPackageFormat=apk \
|
||||
-p:AndroidKeyStore=false \
|
||||
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
|
||||
-p:JavaSdkDirectory="${JAVA_HOME:-}" \
|
||||
-o "$publish_dir"
|
||||
|
||||
apk_source="$(find "$publish_dir" -type f -name '*.apk' | head -n 1)"
|
||||
if [[ -z "$apk_source" ]]; then
|
||||
echo "Android publish did not produce an APK." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$apk_source" "$apk_path"
|
||||
|
||||
(
|
||||
cd "$output_dir"
|
||||
sha256sum "$(basename "$apk_path")" > KoTalk-android-universal-$version.apk.sha256
|
||||
)
|
||||
|
||||
echo "Built Android APK in $output_dir"
|
||||
130
scripts/release/build-windows-distributions.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/build-windows-distributions.sh --version 2026.04.16-alpha.6 [options]
|
||||
|
||||
Options:
|
||||
--configuration <name> Build configuration. Default: Release
|
||||
--runtime <rid> Runtime identifier. Default: win-x64
|
||||
--output <dir> Output directory. Default: artifacts/builds/<version>
|
||||
--dotnet <path> Explicit dotnet binary path
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
configuration="Release"
|
||||
runtime="win-x64"
|
||||
output_dir=""
|
||||
dotnet_bin="${DOTNET_BIN:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--configuration)
|
||||
configuration="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--runtime)
|
||||
runtime="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
output_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dotnet)
|
||||
dotnet_bin="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
if [[ -z "$dotnet_bin" ]]; then
|
||||
if command -v dotnet >/dev/null 2>&1; then
|
||||
dotnet_bin="$(command -v dotnet)"
|
||||
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||
dotnet_bin="$HOME/.dotnet/dotnet"
|
||||
else
|
||||
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$output_dir" ]]; then
|
||||
output_dir="$repo_root/artifacts/builds/$version"
|
||||
fi
|
||||
|
||||
publish_dir="$output_dir/publish/$runtime"
|
||||
onefile_dir="$output_dir/onefile/$runtime"
|
||||
zip_path="$output_dir/KoTalk-windows-x64-$version.zip"
|
||||
portable_exe_path="$output_dir/KoTalk-windows-x64-onefile-$version.exe"
|
||||
installer_path="$output_dir/KoTalk-windows-x64-installer-$version.exe"
|
||||
installer_script="$repo_root/packaging/windows/KoTalkInstaller.nsi"
|
||||
app_icon="$repo_root/branding/ico/kotalk.ico"
|
||||
|
||||
rm -rf "$publish_dir" "$onefile_dir"
|
||||
mkdir -p "$publish_dir" "$onefile_dir" "$output_dir"
|
||||
|
||||
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
|
||||
-c "$configuration" \
|
||||
-r "$runtime" \
|
||||
--self-contained true \
|
||||
-p:DebugSymbols=false \
|
||||
-p:DebugType=None \
|
||||
-o "$publish_dir"
|
||||
|
||||
rm -f "$zip_path"
|
||||
(cd "$publish_dir" && zip -rq "$zip_path" .)
|
||||
|
||||
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
|
||||
-c "$configuration" \
|
||||
-r "$runtime" \
|
||||
--self-contained true \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||
-p:EnableCompressionInSingleFile=true \
|
||||
-p:DebugSymbols=false \
|
||||
-p:DebugType=None \
|
||||
-o "$onefile_dir"
|
||||
|
||||
cp "$onefile_dir/KoTalk.exe" "$portable_exe_path"
|
||||
|
||||
makensis \
|
||||
-DAPP_VERSION="$version" \
|
||||
-DAPP_ICON="$app_icon" \
|
||||
-DSOURCE_DIR="$publish_dir" \
|
||||
-DOUTPUT_FILE="$installer_path" \
|
||||
"$installer_script" >/dev/null
|
||||
|
||||
(
|
||||
cd "$output_dir"
|
||||
sha256sum \
|
||||
"$(basename "$zip_path")" \
|
||||
"$(basename "$portable_exe_path")" \
|
||||
"$(basename "$installer_path")" \
|
||||
> SHA256SUMS.txt
|
||||
)
|
||||
|
||||
echo "Built Windows release assets in $output_dir"
|
||||
|
|
@ -8,6 +8,10 @@ Usage:
|
|||
|
||||
Options:
|
||||
--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 '\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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
14
src/PhysOn.Mobile.Android/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/appicon"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/appicon_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="false">
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
268
src/PhysOn.Mobile.Android/MainActivity.cs
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Graphics;
|
||||
using Android.Net;
|
||||
using Android.Net.Http;
|
||||
using Android.OS;
|
||||
using Android.Views;
|
||||
using Android.Webkit;
|
||||
using Android.Widget;
|
||||
|
||||
namespace PhysOn.Mobile.Android;
|
||||
|
||||
[Activity(
|
||||
Label = "@string/app_name",
|
||||
MainLauncher = true,
|
||||
Exported = true,
|
||||
LaunchMode = LaunchMode.SingleTask,
|
||||
Theme = "@style/KoTalkTheme",
|
||||
ConfigurationChanges =
|
||||
ConfigChanges.Orientation |
|
||||
ConfigChanges.ScreenSize |
|
||||
ConfigChanges.SmallestScreenSize |
|
||||
ConfigChanges.ScreenLayout |
|
||||
ConfigChanges.UiMode |
|
||||
ConfigChanges.KeyboardHidden |
|
||||
ConfigChanges.Density)]
|
||||
public class MainActivity : Activity
|
||||
{
|
||||
private const string AppVersion = "0.1.0-alpha.6";
|
||||
private const string HomeUrl = "https://vstalk.phy.kr";
|
||||
|
||||
private static readonly HashSet<string> AllowedHosts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"vstalk.phy.kr",
|
||||
"download-vstalk.phy.kr"
|
||||
};
|
||||
|
||||
private WebView? _webView;
|
||||
private ProgressBar? _loadingBar;
|
||||
private View? _offlineOverlay;
|
||||
private ImageButton? _retryButton;
|
||||
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
ConfigureWindowChrome();
|
||||
SetContentView(Resource.Layout.activity_main);
|
||||
|
||||
_webView = FindViewById<WebView>(Resource.Id.app_webview);
|
||||
_loadingBar = FindViewById<ProgressBar>(Resource.Id.loading_bar);
|
||||
_offlineOverlay = FindViewById<View>(Resource.Id.offline_overlay);
|
||||
_retryButton = FindViewById<ImageButton>(Resource.Id.retry_button);
|
||||
|
||||
if (_webView is null || _loadingBar is null || _offlineOverlay is null || _retryButton is null)
|
||||
{
|
||||
throw new InvalidOperationException("KoTalk Android shell layout failed to load.");
|
||||
}
|
||||
|
||||
_retryButton.Click += HandleRetryClick;
|
||||
ConfigureWebView(_webView, _loadingBar);
|
||||
|
||||
if (savedInstanceState is not null)
|
||||
{
|
||||
_webView.RestoreState(savedInstanceState);
|
||||
}
|
||||
else
|
||||
{
|
||||
_webView.LoadUrl(HomeUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSaveInstanceState(Bundle outState)
|
||||
{
|
||||
base.OnSaveInstanceState(outState);
|
||||
_webView?.SaveState(outState);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (_retryButton is not null)
|
||||
{
|
||||
_retryButton.Click -= HandleRetryClick;
|
||||
}
|
||||
|
||||
if (_webView is not null)
|
||||
{
|
||||
_webView.StopLoading();
|
||||
_webView.Destroy();
|
||||
_webView = null;
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
public override void OnBackPressed()
|
||||
{
|
||||
if (_webView?.CanGoBack() == true)
|
||||
{
|
||||
_webView.GoBack();
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1422
|
||||
base.OnBackPressed();
|
||||
#pragma warning restore CA1422
|
||||
}
|
||||
|
||||
private void HandleRetryClick(object? sender, EventArgs e)
|
||||
{
|
||||
_webView?.Reload();
|
||||
}
|
||||
|
||||
private void ConfigureWindowChrome()
|
||||
{
|
||||
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
|
||||
|
||||
if (Window is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Window.SetStatusBarColor(Color.ParseColor("#F7F3EE"));
|
||||
Window.SetNavigationBarColor(Color.ParseColor("#F7F3EE"));
|
||||
}
|
||||
|
||||
private void ConfigureWebView(WebView webView, ProgressBar loadingBar)
|
||||
{
|
||||
WebView.SetWebContentsDebuggingEnabled(System.Diagnostics.Debugger.IsAttached);
|
||||
|
||||
var settings = webView.Settings!;
|
||||
settings.JavaScriptEnabled = true;
|
||||
settings.DomStorageEnabled = true;
|
||||
settings.DatabaseEnabled = true;
|
||||
settings.AllowFileAccess = false;
|
||||
settings.AllowContentAccess = false;
|
||||
settings.SetSupportZoom(false);
|
||||
settings.BuiltInZoomControls = false;
|
||||
settings.DisplayZoomControls = false;
|
||||
settings.LoadWithOverviewMode = true;
|
||||
settings.UseWideViewPort = true;
|
||||
settings.MixedContentMode = MixedContentHandling.NeverAllow;
|
||||
settings.CacheMode = CacheModes.Default;
|
||||
settings.MediaPlaybackRequiresUserGesture = true;
|
||||
settings.UserAgentString = $"{settings.UserAgentString} KoTalkAndroid/{AppVersion}";
|
||||
|
||||
var cookies = CookieManager.Instance;
|
||||
cookies?.SetAcceptCookie(true);
|
||||
cookies?.SetAcceptThirdPartyCookies(webView, false);
|
||||
|
||||
webView.SetBackgroundColor(Color.ParseColor("#F7F3EE"));
|
||||
webView.SetWebChromeClient(new KoTalkWebChromeClient(loadingBar));
|
||||
webView.SetWebViewClient(
|
||||
new KoTalkWebViewClient(
|
||||
AllowedHosts,
|
||||
ShowOfflineOverlay,
|
||||
HideOfflineOverlay));
|
||||
}
|
||||
|
||||
private void ShowOfflineOverlay()
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
if (_offlineOverlay is not null)
|
||||
{
|
||||
_offlineOverlay.Visibility = ViewStates.Visible;
|
||||
}
|
||||
|
||||
if (_loadingBar is not null)
|
||||
{
|
||||
_loadingBar.Visibility = ViewStates.Invisible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void HideOfflineOverlay()
|
||||
{
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
if (_offlineOverlay is not null)
|
||||
{
|
||||
_offlineOverlay.Visibility = ViewStates.Gone;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class KoTalkWebChromeClient(ProgressBar loadingBar) : WebChromeClient
|
||||
{
|
||||
public override void OnProgressChanged(WebView? view, int newProgress)
|
||||
{
|
||||
base.OnProgressChanged(view, newProgress);
|
||||
loadingBar.Progress = newProgress;
|
||||
loadingBar.Visibility = newProgress >= 100 ? ViewStates.Invisible : ViewStates.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class KoTalkWebViewClient(
|
||||
IReadOnlySet<string> allowedHosts,
|
||||
Action showOfflineOverlay,
|
||||
Action hideOfflineOverlay) : WebViewClient
|
||||
{
|
||||
public override bool ShouldOverrideUrlLoading(WebView? view, IWebResourceRequest? request)
|
||||
{
|
||||
if (request?.Url is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var url = request.Url;
|
||||
var scheme = url.Scheme?.ToLowerInvariant();
|
||||
var host = url.Host?.ToLowerInvariant();
|
||||
|
||||
if (scheme is "http" or "https" && host is not null && allowedHosts.Contains(host))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (view?.Context is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse(url.ToString()));
|
||||
intent.AddFlags(ActivityFlags.NewTask);
|
||||
view.Context.StartActivity(intent);
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
showOfflineOverlay();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
|
||||
{
|
||||
base.OnPageStarted(view, url, favicon);
|
||||
hideOfflineOverlay();
|
||||
}
|
||||
|
||||
public override void OnPageFinished(WebView? view, string? url)
|
||||
{
|
||||
base.OnPageFinished(view, url);
|
||||
hideOfflineOverlay();
|
||||
}
|
||||
|
||||
public override void OnReceivedError(
|
||||
WebView? view,
|
||||
IWebResourceRequest? request,
|
||||
WebResourceError? error)
|
||||
{
|
||||
base.OnReceivedError(view, request, error);
|
||||
|
||||
if (request?.IsForMainFrame ?? true)
|
||||
{
|
||||
showOfflineOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnReceivedSslError(WebView? view, SslErrorHandler? handler, SslError? error)
|
||||
{
|
||||
handler?.Cancel();
|
||||
showOfflineOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-android</TargetFramework>
|
||||
<SupportedOSPlatformVersion>26</SupportedOSPlatformVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>PhysOn.Mobile.Android</RootNamespace>
|
||||
<AssemblyName>KoTalk.Mobile.Android</AssemblyName>
|
||||
<ApplicationId>kr.physia.kotalk</ApplicationId>
|
||||
<ApplicationVersion>6</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>0.1.0-alpha.6</ApplicationDisplayVersion>
|
||||
<Company>PHYSIA</Company>
|
||||
<Authors>PHYSIA</Authors>
|
||||
<Product>KoTalk</Product>
|
||||
<Description>KoTalk Android shell for the live Korean messaging experience</Description>
|
||||
<ApplicationTitle>KoTalk</ApplicationTitle>
|
||||
<AndroidPackageFormat>apk</AndroidPackageFormat>
|
||||
<AndroidUseDesignerAssembly>false</AndroidUseDesignerAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and '$(JAVA_HOME)' != ''">
|
||||
<JavaSdkDirectory>$(JAVA_HOME)</JavaSdkDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(JavaSdkDirectory)' == '' and Exists('/usr/lib/jvm/java-17-openjdk-amd64')">
|
||||
<JavaSdkDirectory>/usr/lib/jvm/java-17-openjdk-amd64</JavaSdkDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and '$(ANDROID_SDK_ROOT)' != ''">
|
||||
<AndroidSdkDirectory>$(ANDROID_SDK_ROOT)</AndroidSdkDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(AndroidSdkDirectory)' == '' and Exists('$([System.Environment]::GetEnvironmentVariable(`HOME`))/Android/Sdk')">
|
||||
<AndroidSdkDirectory>$([System.Environment]::GetEnvironmentVariable('HOME'))/Android/Sdk</AndroidSdkDirectory>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
44
src/PhysOn.Mobile.Android/Resources/AboutResources.txt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
Images, layout descriptions, binary blobs and string dictionaries can be included
|
||||
in your application as resource files. Various Android APIs are designed to
|
||||
operate on the resource IDs instead of dealing with images, strings or binary blobs
|
||||
directly.
|
||||
|
||||
For example, a sample Android app that contains a user interface layout (main.xml),
|
||||
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
|
||||
would keep its resources in the "Resources" directory of the application:
|
||||
|
||||
Resources/
|
||||
drawable/
|
||||
icon.png
|
||||
|
||||
layout/
|
||||
main.xml
|
||||
|
||||
values/
|
||||
strings.xml
|
||||
|
||||
In order to get the build system to recognize Android resources, set the build action to
|
||||
"AndroidResource". The native Android APIs do not operate directly with filenames, but
|
||||
instead operate on resource IDs. When you compile an Android application that uses resources,
|
||||
the build system will package the resources for distribution and generate a class called "Resource"
|
||||
(this is an Android convention) that contains the tokens for each one of the resources
|
||||
included. For example, for the above Resources layout, this is what the Resource class would expose:
|
||||
|
||||
public class Resource {
|
||||
public class Drawable {
|
||||
public const int icon = 0x123;
|
||||
}
|
||||
|
||||
public class Layout {
|
||||
public const int main = 0x456;
|
||||
}
|
||||
|
||||
public class Strings {
|
||||
public const int first_string = 0xabc;
|
||||
public const int second_string = 0xbcd;
|
||||
}
|
||||
}
|
||||
|
||||
You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or
|
||||
Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string
|
||||
to reference the first string in the dictionary file values/strings.xml.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="@color/surface_card" />
|
||||
<stroke android:width="1dp" android:color="@color/surface_border" />
|
||||
<corners android:radius="2dp" />
|
||||
</shape>
|
||||
52
src/PhysOn.Mobile.Android/Resources/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/surface_canvas">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/app_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
android:overScrollMode="never" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:layout_gravity="top"
|
||||
android:indeterminate="false"
|
||||
android:max="100"
|
||||
android:progressBackgroundTint="@color/surface_border"
|
||||
android:progressTint="@color/accent_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/offline_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="28dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@mipmap/appicon" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/retry_button"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/retry_button_background"
|
||||
android:contentDescription="@string/reload"
|
||||
android:padding="16dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@android:drawable/ic_popup_sync" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/appicon_background" />
|
||||
<foreground android:drawable="@mipmap/appicon_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/appicon_background" />
|
||||
<foreground android:drawable="@mipmap/appicon_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-hdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-mdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 940 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 1,010 B |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xxhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/PhysOn.Mobile.Android/Resources/mipmap-xxxhdpi/appicon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 3.3 KiB |
8
src/PhysOn.Mobile.Android/Resources/values/colors.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="surface_canvas">#F7F3EE</color>
|
||||
<color name="surface_card">#FFFFFF</color>
|
||||
<color name="surface_border">#DDD1C4</color>
|
||||
<color name="ink_primary">#20242B</color>
|
||||
<color name="accent_primary">#F05B2B</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#2C3E50</color>
|
||||
</resources>
|
||||
4
src/PhysOn.Mobile.Android/Resources/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">KoTalk</string>
|
||||
<string name="reload">다시 불러오기</string>
|
||||
</resources>
|
||||
8
src/PhysOn.Mobile.Android/Resources/values/styles.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="KoTalkTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@color/surface_canvas</item>
|
||||
<item name="android:statusBarColor">@color/surface_canvas</item>
|
||||
<item name="android:navigationBarColor">@color/surface_canvas</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
</network-security-config>
|
||||
4
src/PhysOn.Web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "physon-web",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: '준비 중',
|
||||
|
|
|
|||
|
|
@ -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 단독 최적화와 분리해 판단하면 안 된다
|
||||
|
||||
## 기능 우선순위 매핑
|
||||
|
||||
|
|
|
|||