공개: KoTalk 최신 기준선
11
.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.git
|
||||
.github
|
||||
.workspace-secrets
|
||||
artifacts
|
||||
docs/assets
|
||||
release-assets
|
||||
src/VsMessenger.Web/node_modules
|
||||
src/VsMessenger.Web/dist
|
||||
**/bin
|
||||
**/obj
|
||||
**/*.tsbuildinfo
|
||||
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: 버그나 회귀를 기록합니다
|
||||
title: "[bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 요약
|
||||
|
||||
문제를 한 문장으로 적어 주세요.
|
||||
|
||||
## 재현 절차
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## 기대 결과
|
||||
|
||||
원래 기대한 동작을 적어 주세요.
|
||||
|
||||
## 실제 결과
|
||||
|
||||
실제 나타난 동작을 적어 주세요.
|
||||
|
||||
## 관련 문서 / 스크린샷
|
||||
|
||||
상태 문서, README, 스크린샷, 릴리즈 링크가 있으면 같이 적어 주세요.
|
||||
|
||||
## 환경
|
||||
|
||||
- OS:
|
||||
- 앱 버전:
|
||||
- 빌드 경로:
|
||||
|
||||
## 추가 정보
|
||||
|
||||
스크린샷, 로그, 관련 문서를 적어 주세요.
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Live web
|
||||
url: https://vstalk.phy.kr
|
||||
about: 현재 공개 중인 모바일 웹 진입점을 직접 확인할 수 있습니다.
|
||||
- name: Download mirror
|
||||
url: https://download-vstalk.phy.kr
|
||||
about: 공식 다운로드 미러 주소입니다.
|
||||
- name: Release channels
|
||||
url: https://github.com/werther24601/kotalk/releases
|
||||
about: 공개 저장소 릴리즈 경로를 확인할 수 있습니다.
|
||||
- name: Security contact
|
||||
url: mailto:ian@physia.kr
|
||||
about: 보안 이슈는 공개 이슈 대신 메일로 먼저 알려 주세요.
|
||||
35
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: 기능 제안이나 개선안을 기록합니다
|
||||
title: "[feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
왜 필요한지 적어 주세요.
|
||||
|
||||
## 제안
|
||||
|
||||
원하는 동작을 구체적으로 적어 주세요.
|
||||
|
||||
## 기대 효과
|
||||
|
||||
누가 어떤 점에서 더 편해지는지 적어 주세요.
|
||||
|
||||
## 영향 채널
|
||||
|
||||
- Windows
|
||||
- Mobile Web
|
||||
- Android
|
||||
- Release / Deploy
|
||||
- Docs
|
||||
|
||||
## 현재 한계와의 관계
|
||||
|
||||
`PROJECT_STATUS.md`, `ROADMAP.md`, `문서/` 중 관련 항목이 있으면 적어 주세요.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
관련된 `문서/` 파일이 있으면 링크해 주세요.
|
||||
34
.github/ISSUE_TEMPLATE/ux_review.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: UX review
|
||||
about: 사용 흐름, 정보구조, 문구, 피로도 관점의 리뷰를 남깁니다
|
||||
title: "[ux] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 어떤 흐름인가요
|
||||
|
||||
- 온보딩 / 가입
|
||||
- 대화 목록
|
||||
- 검색 / 재탐색
|
||||
- 대화 / 답장
|
||||
- 세션 복구
|
||||
- 릴리즈 / 다운로드
|
||||
|
||||
## 현재 불편한 점
|
||||
|
||||
사용자 입장에서 어떤 지점이 막히거나 피로한지 적어 주세요.
|
||||
|
||||
## 기대하는 방향
|
||||
|
||||
더 간편해지기 위해 어떤 변화가 필요하다고 보는지 적어 주세요.
|
||||
|
||||
## 영향 채널
|
||||
|
||||
- Windows
|
||||
- Mobile Web
|
||||
- Android
|
||||
|
||||
## 관련 근거
|
||||
|
||||
스크린샷, 영상, 문서 링크, 실제 사용 사례가 있으면 같이 남겨 주세요.
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
## Summary
|
||||
|
||||
- 무엇을 바꿨는지
|
||||
- 왜 바꿨는지
|
||||
|
||||
## Scope
|
||||
|
||||
- 영향 채널: Windows / Mobile Web / Android / Release / Docs
|
||||
- 관련 문서:
|
||||
- UI 변경 시 스크린샷:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] README 또는 `문서/` 반영 여부 확인
|
||||
- [ ] CHANGELOG 반영 여부 확인
|
||||
- [ ] 가입/보안/릴리즈 정책 영향 확인
|
||||
- [ ] 다운로드 경로 영향 확인 (`download-vstalk.phy.kr`)
|
||||
52
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- feat/**
|
||||
- release/**
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore PhysOn.sln
|
||||
|
||||
- name: Build API
|
||||
run: dotnet build src/PhysOn.Api/PhysOn.Api.csproj -c Release --no-restore
|
||||
|
||||
- name: Build Worker
|
||||
run: dotnet build src/PhysOn.Worker/PhysOn.Worker.csproj -c Release --no-restore
|
||||
|
||||
- name: Run API integration tests
|
||||
run: dotnet test tests/PhysOn.Api.IntegrationTests/PhysOn.Api.IntegrationTests.csproj -c Release --no-restore
|
||||
|
||||
desktop-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Restore desktop project
|
||||
run: dotnet restore src/PhysOn.Desktop/PhysOn.Desktop.csproj
|
||||
|
||||
- name: Build desktop project
|
||||
run: dotnet build src/PhysOn.Desktop/PhysOn.Desktop.csproj -c Release --no-restore
|
||||
231
.github/workflows/release-portable.yml
vendored
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
name: release-clients
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "릴리즈 버전. 예: v0.1.0-alpha.1"
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: "릴리즈 채널"
|
||||
required: true
|
||||
default: alpha
|
||||
type: choice
|
||||
options:
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- stable
|
||||
upload_to_vps:
|
||||
description: "준비된 릴리즈 번들을 VPS 다운로드 호스트로 업로드"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Restore desktop project
|
||||
run: dotnet restore src/VsMessenger.Desktop/VsMessenger.Desktop.csproj
|
||||
|
||||
- name: Publish portable desktop build
|
||||
run: dotnet publish src/VsMessenger.Desktop/VsMessenger.Desktop.csproj -c Release -r win-x64 --self-contained true -o out/win-x64
|
||||
|
||||
- name: Create portable ZIP
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path out/win-x64/* -DestinationPath out/VsMessenger-win-x64.zip
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-portable
|
||||
path: out/VsMessenger-win-x64.zip
|
||||
|
||||
build-android:
|
||||
if: ${{ hashFiles('src/VsMessenger.Mobile.Android/*.csproj') != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Install Android workload
|
||||
run: dotnet workload install android
|
||||
|
||||
- name: Restore Android project
|
||||
run: dotnet restore src/VsMessenger.Mobile.Android/VsMessenger.Mobile.Android.csproj
|
||||
|
||||
- name: Publish Android APK
|
||||
run: |
|
||||
dotnet publish src/VsMessenger.Mobile.Android/VsMessenger.Mobile.Android.csproj \
|
||||
-c Release \
|
||||
-f net8.0-android \
|
||||
-p:AndroidPackageFormat=apk \
|
||||
-p:AndroidKeyStore=false \
|
||||
-o out/android
|
||||
|
||||
- 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/VsMessenger-android-universal.apk
|
||||
|
||||
- name: Upload Android artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-apk
|
||||
path: out/VsMessenger-android-universal.apk
|
||||
|
||||
assemble-release:
|
||||
if: ${{ always() && needs.build-windows.result == 'success' && (needs.build-android.result == 'success' || needs.build-android.result == 'skipped') }}
|
||||
needs:
|
||||
- build-windows
|
||||
- build-android
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-portable
|
||||
path: incoming/windows
|
||||
|
||||
- name: Download Android artifact
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-apk
|
||||
path: incoming/android
|
||||
|
||||
- name: Prepare release bundle
|
||||
env:
|
||||
VERSION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
||||
CHANNEL_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
run: |
|
||||
chmod +x scripts/release/release-prepare-assets.sh
|
||||
|
||||
channel="${CHANNEL_INPUT}"
|
||||
if [[ -z "$channel" ]]; then
|
||||
case "$VERSION_INPUT" in
|
||||
*alpha*) channel="alpha" ;;
|
||||
*beta*) channel="beta" ;;
|
||||
*rc*) channel="rc" ;;
|
||||
*) channel="stable" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
prepare_args=(
|
||||
--version "$VERSION_INPUT"
|
||||
--channel "$channel"
|
||||
--windows-zip incoming/windows/VsMessenger-win-x64.zip
|
||||
--force
|
||||
)
|
||||
|
||||
if [[ -f incoming/android/VsMessenger-android-universal.apk ]]; then
|
||||
prepare_args+=(--android-apk incoming/android/VsMessenger-android-universal.apk)
|
||||
fi
|
||||
|
||||
./scripts/release/release-prepare-assets.sh \
|
||||
"${prepare_args[@]}"
|
||||
|
||||
- name: Upload release bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-bundle
|
||||
path: release-assets
|
||||
|
||||
publish-forge-release:
|
||||
if: ${{ secrets.FORGE_RELEASE_TOKEN != '' }}
|
||||
needs: assemble-release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download release bundle
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-bundle
|
||||
path: .
|
||||
|
||||
- name: Publish release to forge
|
||||
env:
|
||||
VERSION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
||||
FORGE_RELEASE_TOKEN: ${{ secrets.FORGE_RELEASE_TOKEN }}
|
||||
run: |
|
||||
chmod +x scripts/release/release-publish-forge.sh
|
||||
./scripts/release/release-publish-forge.sh --version "$VERSION_INPUT"
|
||||
|
||||
upload-to-vps:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.upload_to_vps
|
||||
needs: assemble-release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download release bundle
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-bundle
|
||||
path: .
|
||||
|
||||
- name: Configure SSH key
|
||||
env:
|
||||
DOWNLOAD_SSH_KEY: ${{ secrets.DOWNLOAD_SSH_KEY }}
|
||||
run: |
|
||||
test -n "$DOWNLOAD_SSH_KEY"
|
||||
install -d -m 700 ~/.ssh
|
||||
printf '%s\n' "$DOWNLOAD_SSH_KEY" > ~/.ssh/download_host
|
||||
chmod 600 ~/.ssh/download_host
|
||||
|
||||
- name: Upload bundle to download host
|
||||
env:
|
||||
VERSION_INPUT: ${{ inputs.version }}
|
||||
DOWNLOAD_SSH_HOST: ${{ secrets.DOWNLOAD_SSH_HOST }}
|
||||
DOWNLOAD_SSH_USER: ${{ secrets.DOWNLOAD_SSH_USER }}
|
||||
DOWNLOAD_ROOT: ${{ secrets.DOWNLOAD_ROOT }}
|
||||
run: |
|
||||
test -n "$DOWNLOAD_SSH_HOST"
|
||||
test -n "$DOWNLOAD_SSH_USER"
|
||||
test -n "$DOWNLOAD_ROOT"
|
||||
chmod +x scripts/release/release-upload-assets.sh
|
||||
./scripts/release/release-upload-assets.sh \
|
||||
--version "$VERSION_INPUT" \
|
||||
--host "$DOWNLOAD_SSH_HOST" \
|
||||
--user "$DOWNLOAD_SSH_USER" \
|
||||
--target "$DOWNLOAD_ROOT" \
|
||||
--ssh-key ~/.ssh/download_host
|
||||
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.workspace-secrets/
|
||||
.workspace-policy/
|
||||
.vs/
|
||||
.playwright-cli/
|
||||
.vite/
|
||||
artifacts/
|
||||
releases/
|
||||
output/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
**/bin/
|
||||
**/obj/
|
||||
77
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Architecture
|
||||
|
||||
## 시스템 구성도
|
||||
|
||||
```text
|
||||
Windows Desktop (Avalonia 12)
|
||||
|
|
||||
REST / WebSocket
|
||||
|
|
||||
ASP.NET Core 8 API
|
||||
|
|
||||
SQLite (current local alpha)
|
||||
|
|
||||
PostgreSQL / Redis / MinIO (target VPS stack)
|
||||
```
|
||||
|
||||
## 핵심 컴포넌트 역할
|
||||
|
||||
| 컴포넌트 | 역할 |
|
||||
|---|---|
|
||||
| `VsMessenger.Desktop` | 한국어 Windows UX, 세션 보존, 대화 목록/대화창, 전송 흐름 |
|
||||
| `VsMessenger.Api` | 인증, 부트스트랩, 대화/메시지 REST API, WebSocket 엔드포인트 |
|
||||
| `VsMessenger.Application` | 유스케이스와 서비스 로직 |
|
||||
| `VsMessenger.Domain` | 계정, 세션, 대화, 메시지 등 핵심 도메인 모델 |
|
||||
| `VsMessenger.Infrastructure` | DB, 토큰, 시계, 실시간 연결 허브 등 인프라 구현 |
|
||||
| `release-assets` | 릴리즈 메타데이터, 체크섬, 스크린샷 번들 |
|
||||
| `deploy` | VPS용 Compose, Caddy, systemd, Dockerfile 초안 |
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
### 가입
|
||||
|
||||
1. 데스크톱 앱이 이름 + 초대코드를 보냅니다.
|
||||
2. API가 초대코드를 검증하고 계정/세션을 생성합니다.
|
||||
3. 앱은 반환된 세션을 저장하고 부트스트랩 데이터를 요청합니다.
|
||||
|
||||
### 메시지 전송
|
||||
|
||||
1. 사용자가 텍스트를 입력합니다.
|
||||
2. 앱이 REST API로 메시지를 전송합니다.
|
||||
3. API가 메시지를 저장하고 관련 사용자에게 WebSocket 이벤트를 보냅니다.
|
||||
4. 앱은 읽기 상태와 목록을 갱신합니다.
|
||||
|
||||
## 현재 구조와 목표 구조
|
||||
|
||||
| 구분 | 현재 실행 구조 | 목표 배포 구조 |
|
||||
|---|---|---|
|
||||
| 클라이언트 | Avalonia 12 desktop | Windows x64 portable / 향후 설치형 |
|
||||
| API 저장소 | SQLite | PostgreSQL |
|
||||
| 실시간 | API 내 WebSocket | API + Redis 기반 팬아웃 보조 |
|
||||
| 파일 저장 | 미구현 | MinIO |
|
||||
| 리버스 프록시 | 로컬 직접 포트 | Caddy |
|
||||
| 운영 환경 | 로컬/WSL 중심 | Rocky Linux VPS |
|
||||
|
||||
## 보안 경계
|
||||
|
||||
- 데스크톱 앱은 세션과 사용자 데이터를 OS 환경에 맞게 최소한으로 저장해야 합니다.
|
||||
- API는 메시지 본문과 민감 정보를 로그에 남기지 않아야 합니다.
|
||||
- 실사용 배포 전에는 `root + 비밀번호 SSH` 상태의 VPS를 그대로 사용하지 않습니다.
|
||||
- 공개 다운로드 채널은 TLS와 체크섬 검증을 전제로 합니다.
|
||||
|
||||
보안 상세 정책은 [SECURITY.md](SECURITY.md)와 [문서/05-security-privacy-and-risk.md](문서/05-security-privacy-and-risk.md)를 참고하세요.
|
||||
|
||||
## 기술 선택 이유
|
||||
|
||||
- `Avalonia 12`: 현재 워크스페이스에서 빠르게 데스크톱 UI를 반복하고 Windows portable 산출물을 만들기 쉬움
|
||||
- `.NET 8`: API와 데스크톱 모두에서 일관된 개발/배포 흐름 확보
|
||||
- `ASP.NET Core 8`: REST + WebSocket 수직 슬라이스를 빠르게 구성 가능
|
||||
- `SQLite`: Alpha 단계에서 복잡도와 운영 부담을 낮춤
|
||||
- `PostgreSQL / Redis / MinIO`: VPS 운영 단계에서 필요한 데이터/캐시/파일 분리를 준비
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- 제품 전략: [문서/01-product-strategy-and-mvp.md](문서/01-product-strategy-and-mvp.md)
|
||||
- Windows 앱 구조: [문서/03-windows-client-architecture.md](문서/03-windows-client-architecture.md)
|
||||
- 서버/VPS 구조: [문서/04-chat-server-vps-architecture.md](문서/04-chat-server-vps-architecture.md)
|
||||
- API 계약: [문서/13-v0.1-api-and-events-contract.md](문서/13-v0.1-api-and-events-contract.md)
|
||||
76
BACKGROUND.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Background
|
||||
|
||||
KoTalk의 배경은 단순합니다. 많은 사람이 여전히 쓰는 기준 메신저를 부정하기보다, 최근 한국어 사용자들이 실제로 드러낸 피로와 미충족 수요를 더 조용하고 더 예측 가능한 제품 설계로 풀어 보자는 것입니다.
|
||||
|
||||
## Why This Project Exists
|
||||
|
||||
최근 국내 메신저 여론에서 반복적으로 읽히는 신호는 꽤 선명했습니다.
|
||||
|
||||
- 대화보다 피드, 광고, 추천 콘텐츠가 먼저 보일 때 생기는 피로
|
||||
- 운영정책과 제재 기준이 충분히 설명되지 않을 때 생기는 불신
|
||||
- 개인정보와 프라이버시 이슈가 누적될 때 생기는 경계심
|
||||
- 장애가 반복될 때 생기는 “언제 또 끊길지 모른다”는 피로감
|
||||
|
||||
KoTalk는 이 신호를 자극적인 비판의 소재로 소비하지 않습니다. 대신, 메신저라면 적어도 아래 네 가지는 다시 잘해야 한다고 봅니다.
|
||||
|
||||
- 대화와 답장이 가장 먼저 보일 것
|
||||
- 개인적 대화와 업무적 대화 모두에서 복귀가 짧을 것
|
||||
- 상태와 정책이 설명 가능할 것
|
||||
- 장애나 세션 끊김이 생겨도 복구 흐름이 예측 가능할 것
|
||||
|
||||
## Signals Seen In Public Coverage
|
||||
|
||||
최근 공개 기사와 공식 자료를 기준으로 보면, KoTalk의 배경은 대체로 아래 네 가지 축으로 정리됩니다.
|
||||
|
||||
| Signal | What public coverage suggests | KoTalk response |
|
||||
|---|---|---|
|
||||
| 메신저 본질에 대한 피로 | 2025년 국내 기사들은 카카오톡 대규모 개편 이후 친구 탭 피드화, 숏폼, 광고 노출, 전체 구조 변화에 대한 이용자 불만이 빠르게 커졌다고 정리했습니다. 사용자는 기능 확대보다 `대화 흐름이 덜 방해받는가`를 더 엄격하게 봤습니다. | KoTalk는 첫 화면을 대화와 재진입 흐름 중심으로 유지합니다. 광고, 피드, 추천 콘텐츠는 핵심 표면에서 의도적으로 밀어냅니다. |
|
||||
| 프라이버시와 보안 신뢰 요구 | 오픈채팅 관련 개인정보 유출과 과징금 이슈는 대형 메신저일수록 프라이버시를 옵션이 아니라 기본 설계로 다뤄야 한다는 기대를 더 높였습니다. | KoTalk는 최소 수집, 명확한 데이터 경계, 세션 통제, 설명 가능한 보안 문서를 제품 표면의 일부로 둡니다. |
|
||||
| 운영정책 설명 책임 | 안전 조치 자체보다도, 어디까지 제한하는지와 어떤 기준으로 처리하는지를 투명하게 설명해야 신뢰를 얻는다는 신호가 반복됐습니다. | KoTalk는 제한, 차단, 신고, 복구 절차를 가능한 한 짧고 이해 가능한 문장으로 정리하는 방향을 택합니다. |
|
||||
| 안정성과 복원력 기대 | 메시지 지연, 로그인 오류, 접속 장애 보도는 이제 메신저 품질에서 `잠깐의 장애`보다 `반복될 때 어떻게 복구되는가`가 더 중요하다는 점을 드러냈습니다. | KoTalk는 재연결, 읽기 커서, 전송 실패 표시, 복구 상태 안내를 핵심 기능으로 봅니다. |
|
||||
|
||||
## What KoTalk Is Trying To Rebuild
|
||||
|
||||
KoTalk가 다시 붙잡으려는 감각은 “메신저가 해야 할 기본기”에 가깝습니다.
|
||||
|
||||
- Windows에서 읽기와 답장이 편한 구조
|
||||
- 모바일에서 길게 설명을 읽지 않아도 되는 진입
|
||||
- 검색, 보관, 다시 열기처럼 나중에 필요한 대화를 다시 꺼내기 쉬운 흐름
|
||||
- 장식보다 밀도와 구조가 먼저 보이는 한국어 UI
|
||||
|
||||
즉, 낯선 제품 철학을 강요하기보다 익숙한 대화 문법을 현대적인 방식으로 다시 정돈하는 프로젝트입니다.
|
||||
|
||||
## Tone And Position
|
||||
|
||||
이 프로젝트는 특정 서비스를 공격하거나, “누군가를 대체하기 위해 무너져야 한다”는 식으로 접근하지 않습니다. 오히려 아래와 같은 태도에 가깝습니다.
|
||||
|
||||
- 지금의 기준 제품이 여전히 중요한 이유를 인정하기
|
||||
- 이용자가 실제로 말한 불편을 흘려보내지 않기
|
||||
- 설명 가능한 UI와 운영 표면을 만들기
|
||||
- 공개 저장소에서 보여지는 것과 실제 구현 상태를 최대한 맞추기
|
||||
|
||||
## What That Means For The Product Surface
|
||||
|
||||
그래서 이 저장소의 공개 표면도 같은 방향을 따릅니다.
|
||||
|
||||
- README에는 화면, 상태, 배경, 릴리즈 경로를 함께 둡니다.
|
||||
- 상태 문서에는 잘 되는 것과 아직 부족한 것을 같이 적습니다.
|
||||
- 스크린샷은 제품의 방향을 보여 주는 근거로 남깁니다.
|
||||
- 긴 기획과 UX 확장 문서는 별도 문서 묶음으로 보관합니다.
|
||||
|
||||
## Sources
|
||||
|
||||
- 아주경제, `카카오톡 개편에 42% '불만'…"대체 메신저 언급까지 확산"`: <https://www.ajunews.com/view/20250928142420661>
|
||||
- 더팩트, `카카오톡 '국민 메신저' 위상 휘청…개편 실패 책임론 급부상`: <https://news.tf.co.kr/read/economy/2249675.htm>
|
||||
- YTN, `카톡 검열 논란`: <https://www.ytn.co.kr/_ln/0519_202506171620012347>
|
||||
- YTN, `카카오톡, 오전 한때 6분간 접속·메시지 전송 오류`: <https://www.ytn.co.kr/_ln/0102_202409201022119491>
|
||||
- 연합뉴스, `"최소 6만5천명 정보 유출"…카카오에 과징금 151억 '역대 최대'`: <https://www.yna.co.kr/view/AKR20240523065500530>
|
||||
- 배경 정리 장문: [문서/14-project-background-and-market-context.md](문서/14-project-background-and-market-context.md)
|
||||
|
||||
## Read More
|
||||
|
||||
- 배경과 여론 맥락의 긴 문서: [문서/14-project-background-and-market-context.md](문서/14-project-background-and-market-context.md)
|
||||
- 제품 전략: [문서/01-product-strategy-and-mvp.md](문서/01-product-strategy-and-mvp.md)
|
||||
- 업무형 UX 방향: [문서/22-work-communication-ux-playbook.md](문서/22-work-communication-ux-playbook.md)
|
||||
- 현재 상태: [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
- 현재 화면 묶음: [SHOWCASE.md](SHOWCASE.md)
|
||||
123
BRANCHING_STRATEGY.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Branching Strategy
|
||||
|
||||
## 목적
|
||||
|
||||
문서 중심 기획 저장소에서 시작하더라도, 이후 구현 단계까지 이어질 수 있는 브랜치 전략을 미리 고정합니다.
|
||||
|
||||
## 기본 원칙
|
||||
|
||||
- `main`은 내부 워크스페이스 기준선입니다.
|
||||
- 커밋은 의미 단위로 작게 유지합니다.
|
||||
- 내부 기본 원격으로의 푸시는 계속 빠르게 유지합니다.
|
||||
- 공개용 브랜치는 `main`과 분리된 별도 계보로 관리합니다.
|
||||
- 릴리즈와 다운로드 서브도메인 갱신은 `release/*` 브랜치에서 마감합니다.
|
||||
|
||||
## 브랜치 종류
|
||||
|
||||
### `main`
|
||||
|
||||
- 내부 워크스페이스 최신 상태
|
||||
- 자동 반영과 빠른 반복 작업의 기준선
|
||||
- README, CHANGELOG, 문서 인덱스가 항상 최신이어야 합니다.
|
||||
|
||||
### `workspace/main`
|
||||
|
||||
- 현재 내부 워크스페이스 기준을 보존하는 백업 성격 브랜치
|
||||
- 공개용 이력 재구성 전후의 안전 기준점
|
||||
|
||||
### `public/main`
|
||||
|
||||
- 공개 레포용 큐레이션 브랜치
|
||||
- 내부 히스토리를 그대로 노출하지 않고, 공개 가능한 이력만 최소 선형 체인으로 유지합니다.
|
||||
- 공개 배포 직전의 파일/문구/링크/히스토리 검수를 통과한 상태만 반영합니다.
|
||||
- `main`에서 구조/네이밍 정리가 크게 들어가면 다시 재생성하거나 재큐레이션하는 것을 기본 원칙으로 둡니다.
|
||||
|
||||
### `feat/<topic>`
|
||||
|
||||
- 기능 개발용 브랜치
|
||||
- 예: `feat/auth-alpha`, `feat/winui-shell`, `feat/search-index`
|
||||
|
||||
### `docs/<topic>`
|
||||
|
||||
- 문서 보강/개편 전용 브랜치
|
||||
- 예: `docs/readme-overhaul`, `docs/release-playbook`
|
||||
|
||||
### `release/<version>`
|
||||
|
||||
- 릴리즈 마감용 브랜치
|
||||
- 예: `release/v0.1.0-alpha.1`
|
||||
- 아래 항목을 함께 정리합니다.
|
||||
- 빌드 산출물
|
||||
- `CHANGELOG.md`
|
||||
- README 상태 반영
|
||||
- 다운로드 경로 점검
|
||||
|
||||
### `hotfix/<topic>`
|
||||
|
||||
- 배포 후 긴급 수정
|
||||
- 예: `hotfix/download-link`, `hotfix/auth-token-rotation`
|
||||
|
||||
### `spike/<topic>`
|
||||
|
||||
- 실험/검증
|
||||
- 장기 유지 코드로 합칠지 미정인 경우만 사용
|
||||
|
||||
## 머지 원칙
|
||||
|
||||
- 개인 저장소 기준으로도 PR 스타일 설명을 남깁니다.
|
||||
- `main` 머지 전 확인 항목:
|
||||
- README가 최신 상태인가
|
||||
- `문서/`의 결정과 충돌하지 않는가
|
||||
- CHANGELOG 반영이 필요한가
|
||||
- 다운로드/릴리즈 경로 영향이 있는가
|
||||
- `public/main` 반영 전 추가 확인 항목:
|
||||
- 공개 부적합 링크나 내부 경로가 남아 있지 않은가
|
||||
- AI/프롬프트/에이전트 같은 메타 흔적이 남아 있지 않은가
|
||||
- 공개용 커밋 이력이 과도하거나 어색하지 않은가
|
||||
- 공개 릴리즈 문구가 현재 구현 상태를 넘어서지 않는가
|
||||
- `.workspace-*` 경로의 비공개 정책/시크릿이 추적 대상에 섞이지 않았는가
|
||||
|
||||
## 커밋 규칙
|
||||
|
||||
- 권장 접두사:
|
||||
- `docs:`
|
||||
- `feat:`
|
||||
- `fix:`
|
||||
- `release:`
|
||||
- `chore:`
|
||||
- 예:
|
||||
- `docs: define korean-first signup policy`
|
||||
- `feat: scaffold winui shell`
|
||||
- `release: prepare alpha download endpoint`
|
||||
|
||||
## 릴리즈 운영 규칙
|
||||
|
||||
릴리즈 브랜치에서는 아래를 반드시 같이 처리합니다.
|
||||
|
||||
1. Windows 빌드 생성
|
||||
2. VPS 업로드
|
||||
3. `https://download-vstalk.phy.kr` 동작 확인
|
||||
4. README 버전/상태 갱신
|
||||
5. CHANGELOG 갱신
|
||||
6. 태그 생성
|
||||
7. `main` 반영
|
||||
|
||||
공개 릴리즈는 아래 순서를 따릅니다.
|
||||
|
||||
1. 내부 기본 원격 반영
|
||||
2. 명시적 요청이 있을 때만 제2 공개 레포 반영
|
||||
3. 다시 명시적 요청이 있을 때만 제3 공개 레포 반영
|
||||
|
||||
제3 공개 레포는 항상 제2 공개 레포보다 늦게 공개하며, 제2 공개 레포 경로를 함께 명시합니다.
|
||||
|
||||
## 문서 갱신 규칙
|
||||
|
||||
다음 변화가 있으면 README와 문서를 같이 갱신합니다.
|
||||
|
||||
- 제품 방향 변경
|
||||
- 가입 방식 변경
|
||||
- 릴리즈 방식 변경
|
||||
- 다운로드 경로 변경
|
||||
- 우위/패리티 판단 변경
|
||||
|
||||
즉, 구현만 바꾸고 문서를 방치하지 않습니다.
|
||||
37
BUSINESS_MODEL.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Business Model
|
||||
|
||||
KoTalk는 오픈소스 코어를 유지하면서, 운영 서비스와 지원을 별도 가치로 만드는 방식을 지향합니다.
|
||||
|
||||
## Core Position
|
||||
|
||||
- 저장소 코드는 읽고, 수정하고, 자체 배포할 수 있어야 합니다.
|
||||
- 공식 운영 서비스는 호스팅, 운영 책임, 배포 지원, 조직 기능, 보안·감사성 같은 영역에서 가치를 만들어야 합니다.
|
||||
- 공개 저장소와 운영 서비스는 같은 문장으로 뭉개지지 않게 설명합니다.
|
||||
|
||||
## Why This Structure
|
||||
|
||||
메신저는 기능만으로 끝나지 않습니다. 실제 도입에서는 운영 책임, 업데이트 경로, 지원, 보안 설명 가능성도 중요합니다.
|
||||
KoTalk는 이 차이를 숨기지 않고, 코어와 서비스 경계를 분리해 설명하는 방식을 택합니다.
|
||||
|
||||
## Three Surfaces
|
||||
|
||||
| Surface | Meaning |
|
||||
|---|---|
|
||||
| Open-source core | 코드, 문서, 기본 배포 골격, 자체 호스팅 가능한 경로 |
|
||||
| Official service | PHYSIA가 운영하는 서비스와 다운로드 미러 |
|
||||
| Support and deployment packages | 운영 지원, 규제 환경 대응, 설치·운영 지원 |
|
||||
|
||||
## What The Official Service Should Add
|
||||
|
||||
- 관리형 배포와 업데이트 운영
|
||||
- 운영 책임과 장애 대응
|
||||
- 조직용 정책과 감사성
|
||||
- 도입 및 전환 지원
|
||||
|
||||
## What KoTalk Should Avoid
|
||||
|
||||
- 코어 기능을 의도적으로 훼손해 유료 전환을 강제하는 구조
|
||||
- 구현보다 앞서는 과장된 문구
|
||||
- 검증 전 항목을 “즉시 도입 가능”처럼 말하는 방식
|
||||
|
||||
라이선스와 상표 경계는 [LICENSE-FAQ.md](LICENSE-FAQ.md), [TRADEMARKS.md](TRADEMARKS.md)를 참고하세요.
|
||||
93
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Changelog
|
||||
|
||||
이 프로젝트는 현재 초기 설계 단계를 넘어 첫 실행 가능한 Alpha 프로토타입 단계에 들어갔습니다.
|
||||
|
||||
모든 의미 있는 변경은 이 파일에 기록합니다.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `KoTalk` 공개 브랜드 기준과 다운로드/릴리즈 표면 정리
|
||||
- 공개 가입 전략을 `1회성 인증 중심`으로 재정의한 기획 문서 보강
|
||||
- Apache-2.0 기준의 라이선스/상표/기여 정책 정리
|
||||
- 공개 루트 문서 전면 개편과 다운로드 경로 하이퍼링크 정리
|
||||
|
||||
- 한국어 Windows 메신저 프로젝트 방향 수립
|
||||
- `문서/` 기준의 마스터 기획 세트 작성
|
||||
- 최근 국내 카카오톡 여론을 반영한 프로젝트 배경/시장 맥락 문서 추가
|
||||
- `MIT License`, `CODE_OF_CONDUCT.md`, `DEVELOPMENT.md`, `ARCHITECTURE.md`, `ROADMAP.md`, `SUPPORT.md` 추가
|
||||
- Android 병렬 채널 전략 문서 추가
|
||||
- Forge Releases 게시 스크립트 추가
|
||||
- 한국어 UI 문체 시스템 문서 추가
|
||||
- 가입/온보딩/인증 정책 문서 추가
|
||||
- 카카오톡 PC 패리티/상위호환 매트릭스 문서 추가
|
||||
- 공개 저장소용 `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `BRANCHING_STRATEGY.md` 추가
|
||||
- 릴리즈 다운로드 도메인 정책 반영: `https://download-vstalk.phy.kr`
|
||||
- `ASP.NET Core + JWT + EF Core + SQLite + WebSocket` 기반 Alpha 서버 수직 슬라이스
|
||||
- `Avalonia 12 + .NET 8` 기반 Windows 데스크톱 Alpha 셸
|
||||
- `v0.1.0-alpha.1` Windows x64 portable zip 산출물
|
||||
- 릴리즈 번들 메타데이터, 체크섬, 스크린샷 생성 규약
|
||||
- VPS용 MVP 배포 스캐폴딩, Caddy 예시, 릴리즈 업로드 스크립트
|
||||
- `https://vstalk.phy.kr` 모바일 웹앱 실배포와 same-origin API 운영 경로
|
||||
- `PROJECT_STATUS.md`, `GOVERNANCE.md`, 저장소 전용 README 시각 자산 추가
|
||||
- `문서/18-white-material-compact-ui-system.md`, `문서/19-desktop-adaptive-window-and-multiwindow-guidelines.md`, `문서/20-kakao-public-pattern-benchmark-and-vs-translation.md` 추가
|
||||
- 사용자 여정별 점검 기준과 QA 문서 주제 강화를 위한 `문서/63-user-journey-review-framework-and-qa-topics.md` 추가
|
||||
- `COMMUNITY.md`, `MAINTAINERS.md`, `RELEASING.md`, `FIRST_CONTRIBUTION.md` 추가
|
||||
- README 전용 공개 저장소 시각 자산 `open-source-surface.svg`, `contribution-path.svg` 추가
|
||||
- UX 중심 저장소 표면을 위한 `ux_review` 이슈 템플릿 추가
|
||||
- 사용자 관점 리뷰와 비판적 QA 범주 확장을 위한 `문서/112-review-surface-expansion-and-critical-qa-proposal.md` 추가
|
||||
- 루트 120개 문서와 세부 아틀라스 253개 문서로 구성된 `문서/atlas/` 확장 세트 추가
|
||||
- 공개 저장소 첫 진입을 위한 `FAQ.md`, `SHOWCASE.md` 추가
|
||||
- README 전용 공개 표면 자산 `public-contract.svg`, `evaluation-paths.svg` 추가
|
||||
- 공개 사업모델 기준 문서 `BUSINESS_MODEL.md`, `문서/113-open-core-platform-business-and-procurement-strategy.md` 추가
|
||||
- 핵심 차별점 고정 문서 `문서/114-core-differentiation-pillars.md` 추가
|
||||
- `TRUST_CENTER.md`, `SECURITY_RESPONSE.md`, `DEPLOYMENT_MODES.md`, `PRIVACY_AND_DATA_HANDLING.md`, `PROCUREMENT_READINESS.md`, `PORTFOLIO_CAPABILITIES.md`, `TRADEMARKS.md`, `CONTRIBUTOR_LICENSE_POLICY.md`, `LICENSE-FAQ.md` 추가
|
||||
|
||||
### Changed
|
||||
|
||||
- 공개 브랜드를 `KoTalk`로 정리하고 공개 문서의 직접적·내부지향 표현을 제거
|
||||
- README를 대중용 첫인상 기준으로 다시 구성하고 다운로드는 공식 미러와 저장소 릴리즈를 함께 표기
|
||||
- 보안/신뢰 문서에서 운영 힌트와 공유 접근값 노출을 줄이고 공개 범위를 재정의
|
||||
- 라이선스를 Apache-2.0으로 정리하고 일반 기여의 기본 규칙을 단순화
|
||||
- 공개 가입 정책을 `초대코드 중심`에서 `이메일/휴대폰 기반 1회성 인증` 방향으로 수정
|
||||
- 디자인 지침을 `각진, 플랫, 텍스트 최소화, 머터리얼 계열` 원칙으로 보강
|
||||
|
||||
- 제품 방향을 `복제형`이 아니라 `한국어 Windows 메신저 최적화`로 명확히 조정
|
||||
- 가입 정책을 `Alpha 즉시 실행형`과 `Beta 기본형`으로 분리
|
||||
- README와 마스터 문서 세트 전면 보강
|
||||
- 최근 기사와 공개 자료를 바탕으로 프로젝트 배경 설명을 신사적 톤으로 재구성
|
||||
- README를 스크린샷, 빠른 시작, 아키텍처, 로드맵 중심의 공개 저장소형 구조로 개편
|
||||
- 저장소 문서 링크를 원격에서 읽기 좋은 상대경로 기준으로 정리
|
||||
- 멀티플랫폼 릴리즈 구조, OS별 latest 라우트, 원격 Releases 연계 구조로 확장
|
||||
- 최신 기준 스크린샷을 원격 저장소에 함께 유지하는 정책 반영
|
||||
- 클라이언트 실제 구현 스택을 `WinUI 3 계획안`에서 `Avalonia 12 실행안`으로 조정
|
||||
- 프록시 환경에서 WebSocket URL이 `wss://`로 내려오도록 API forwarded headers 처리 추가
|
||||
- 배포 문서를 실제 운영 구조 기준 `Caddy + ASP.NET Core API + nginx webapp + SQLite`로 정정
|
||||
- README를 저장소 공개면 중심 구조로 전면 재구성
|
||||
- 데스크톱 UI를 모던 화이트/플랫/컴팩트 기준으로 대규모 개편하고 기본 서버 주소를 `https://vstalk.phy.kr`로 조정
|
||||
- 모바일 웹 UI를 화이트 원톤 메신저 셸로 재설계하고 `전체/안읽음/고정` 필터, 검색, 가입 직후 첫 대화 진입 흐름 추가
|
||||
- 최신 기준 README 스크린샷 자산을 현재 UI 목업과 모바일 웹 캡처 기준으로 갱신
|
||||
- 모바일 웹 세션 복구를 refresh token 회전 경쟁에 안전한 구조로 보강하고, 일반 네트워크 오류 시 마지막 정상 화면을 유지하도록 조정
|
||||
- 모바일 웹 대화 전환 시 초안이 다른 방으로 잠깐 보이는 상태 불일치를 줄이고, 자동 스크롤을 하단 근처 또는 내 전송 직후로 제한
|
||||
- 모바일 웹 상태 메시지를 온보딩/세션 화면에 맞게 분리하고, JSON이 아닌 오류 응답도 친화적 메시지로 처리
|
||||
- 모바일 웹 최신 기준 목록/대화 스크린샷 자동 캡처 스크립트 추가
|
||||
- 모바일 웹 하단 바를 목적지형 `대화/검색/보관/내 공간` 구조로 재편하고, 검색/보관/내 공간을 분리된 표면으로 1차 구현
|
||||
- 모바일 웹 온보딩 카피, 빈 상태 CTA, 내 공간 액션 배치를 덜 기술적이고 더 사용자 중심으로 정리
|
||||
- 모바일 웹 최신 기준 스크린샷에 검색 화면을 추가하고, 스크린샷 생성 스크립트 의존성을 재현 가능하게 정리
|
||||
- README, PROJECT_STATUS, 문서 인덱스에 강화된 사용자 리뷰 프레임 링크와 문서 규모를 반영
|
||||
- 업무/일상 UX 확장 문서를 `118개` 규모의 마스터 세트로 재구성하고, 모바일 웹 실사용 리뷰·저피로 UI 규칙·정보구조·adoption/support 문서를 추가
|
||||
- README 상단을 `신뢰/상태/진입 경로` 중심으로 재구성하고, 커뮤니티·메인테이너·릴리즈 문서로 공개면을 확장
|
||||
- Issue / PR 템플릿에 플랫폼, 문서 정합성, UX 리뷰 흐름을 더 명시적으로 반영
|
||||
- 사용자 관점 리뷰 체계를 `119개` 문서 규모로 확장하고, 다음 단계 플랫폼별/실패유형별 QA 분리 제안서를 추가
|
||||
- 문서 체계를 루트 120개 + 아틀라스 253개, 총 373개 문서 규모로 확장하고, 실제 모바일 웹 비판 리뷰와 세부 QA 아틀라스를 연결
|
||||
- 모바일 웹 버전을 `web-0.1.0-alpha.2`로 올리고, 첫 대화방 empty state를 행동 패널로 재설계
|
||||
- 모바일 웹 검색을 대화/최근 메시지 기반 재발견 표면으로 확장하고 결과를 메시지/대화 섹션으로 분리
|
||||
- 모바일 웹 보관함을 `답장 필요 / 중요 대화 / 최근 다시 열기` 허브로 재구성
|
||||
- 세션 신뢰 카피를 현재 화면 유지 중심으로 조정하고, reconnect 후 최신 메시지 재동기화와 초기 WebSocket 재연결을 보강
|
||||
- 최신 모바일 웹 스크린샷 세트에 `보관함` 화면을 추가하고 캡처 스크립트를 확장
|
||||
- README 상단을 즉시 체험형 CTA, 공개 계약, 평가 경로, FAQ/Showcase 중심으로 재구성하고 이슈 진입 링크도 함께 정리
|
||||
- 저장소 전략 기준을 `오픈소스 코어 + 공식 플랫폼/관리형 운영 + 공공/기관 대응 가능성`으로 명문화하고 공개 문서에 반영
|
||||
- 범용성, 업무형 간편성, 멀티플랫폼, 셀프호스팅/내부망, 보안/운영 투명성, 커뮤니티 기반 개선 구조를 핵심 차별점으로 공개면과 전략 문서에 고정
|
||||
- 공개 저장소 표면에 메인테이너 실명, 활동명, GitHub 계정, 운영사 `PHYSIA`, 문의 채널을 일관된 기준으로 반영
|
||||
- 기본 JWT 서명키 거부, 세션 재검증, WebSocket 전용 티켓, 인증 no-store, 기본 rate limiting, 보수적 초대코드 시드 정책으로 기본 보안선을 강화
|
||||
52
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Code Of Conduct
|
||||
|
||||
## 우리의 커뮤니티 원칙
|
||||
|
||||
이 프로젝트는 한국어 Windows 메신저를 함께 더 낫게 만드는 공개 협업 공간입니다. 참여자는 아래 원칙을 지켜야 합니다.
|
||||
|
||||
- 기술적 의견 차이는 환영하지만, 사람을 공격하지 않습니다.
|
||||
- 근거 없는 비난보다 재현 가능한 사실과 구체적 제안을 우선합니다.
|
||||
- 초보 기여자도 이해할 수 있는 설명을 지향합니다.
|
||||
- 사적인 조롱, 혐오 표현, 모욕, 집단 괴롭힘을 허용하지 않습니다.
|
||||
|
||||
## 권장되는 참여 방식
|
||||
|
||||
- 재현 절차가 있는 버그 제보
|
||||
- 문서 개선, 오탈자 수정, 구조 정리
|
||||
- 제품 방향과 구현의 차이를 줄이는 제안
|
||||
- 테스트, 릴리즈, 배포 품질 개선
|
||||
|
||||
## 허용되지 않는 행위
|
||||
|
||||
- 인신공격, 비하, 혐오 표현
|
||||
- 반복적 도발, 괴롭힘, 위협
|
||||
- 타인의 개인정보 또는 비공개 정보 공개
|
||||
- 악성 스팸, 광고, 무관한 홍보
|
||||
- 보안 이슈를 사전 연락 없이 공개 이슈로 노출하는 행위
|
||||
|
||||
## 적용 범위
|
||||
|
||||
이 규범은 저장소 이슈, PR, 커밋 메시지, 코드 리뷰, 문서 토론, 릴리즈 메모 등 프로젝트와 관련된 공개 협업 전반에 적용됩니다.
|
||||
|
||||
## 위반 시 조치
|
||||
|
||||
관리자는 상황에 따라 아래 조치를 할 수 있습니다.
|
||||
|
||||
- 내용 수정 요청
|
||||
- 경고
|
||||
- 코멘트/이슈 잠금
|
||||
- 일시적 참여 제한
|
||||
- 영구 차단
|
||||
|
||||
## 신고 절차
|
||||
|
||||
행동 규범 위반이나 괴롭힘이 의심되면 아래로 알려 주세요.
|
||||
|
||||
- `ian@physia.kr`
|
||||
|
||||
가능하면 다음 정보를 함께 보내 주세요.
|
||||
|
||||
- 발생 위치 링크
|
||||
- 관련 사용자
|
||||
- 상황 설명
|
||||
- 필요 시 스크린샷 또는 로그
|
||||
30
COMMUNITY.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Community
|
||||
|
||||
KoTalk는 아직 작은 알파 프로젝트이지만, 공개 저장소는 누구나 읽기 쉬운 상태를 유지하려고 합니다.
|
||||
|
||||
## Where To Start
|
||||
|
||||
- 제품을 먼저 보고 싶다면: [README.md](README.md), [SHOWCASE.md](SHOWCASE.md), [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
- 기여를 시작하고 싶다면: [FIRST_CONTRIBUTION.md](FIRST_CONTRIBUTION.md), [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
- 보안 이슈라면: [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Best First Contributions
|
||||
|
||||
- 문서와 실제 상태 정합성 개선
|
||||
- 모바일 웹과 데스크톱 사용성 보강
|
||||
- 릴리즈, 스크린샷, 다운로드 경로 정리
|
||||
- 재현 가능한 버그 리포트 작성
|
||||
|
||||
## Good Community Signals
|
||||
|
||||
- 문제보다 먼저 사용자 맥락을 설명합니다.
|
||||
- 어떤 흐름이 왜 느린지, 무엇이 더 짧아져야 하는지 적습니다.
|
||||
- 스크린샷, 운영체제, 버전, 재현 절차를 남깁니다.
|
||||
- 문서와 구현이 엇갈리면 그 사실을 함께 적습니다.
|
||||
|
||||
## Contact
|
||||
|
||||
- 일반 도움: [help@physia.kr](mailto:help@physia.kr)
|
||||
- 제휴/운영 문의: [contact@physia.kr](mailto:contact@physia.kr)
|
||||
|
||||
운영 원칙은 [GOVERNANCE.md](GOVERNANCE.md), 메인테이너 정보는 [MAINTAINERS.md](MAINTAINERS.md)에서 확인할 수 있습니다.
|
||||
38
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Contributing
|
||||
|
||||
KoTalk는 코드, 문서, 스크린샷, 릴리즈 표면을 함께 관리하는 저장소입니다.
|
||||
좋은 기여는 기능 추가뿐 아니라 저장소 공개면이 더 정확하고 읽기 쉬워지는 방향까지 포함합니다.
|
||||
|
||||
## Before You Start
|
||||
|
||||
1. [README.md](README.md)
|
||||
2. [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
3. [ROADMAP.md](ROADMAP.md)
|
||||
4. [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||
5. [GOVERNANCE.md](GOVERNANCE.md)
|
||||
|
||||
## Contribution Principles
|
||||
|
||||
- 한국어 UI, 낮은 피로도, 높은 밀도를 기본 방향으로 유지합니다.
|
||||
- 큰 기능은 가능하면 문서로 먼저 방향을 맞춘 뒤 구현합니다.
|
||||
- README와 상태 문서는 실제 구현보다 앞서 나가지 않습니다.
|
||||
- 다운로드 경로나 릴리즈에 영향이 있으면 관련 문서를 함께 갱신합니다.
|
||||
|
||||
## Good Contributions
|
||||
|
||||
- 검색, 복귀, 보관, 멀티 윈도우 같은 핵심 메시징 흐름 개선
|
||||
- 문서와 실제 상태 정합성 보강
|
||||
- 릴리즈, 체크섬, 스크린샷 운영 개선
|
||||
- 재현 가능한 버그 리포트와 테스트 보강
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
- 무엇이 바뀌었는지 설명했는가
|
||||
- 왜 필요한지 설명했는가
|
||||
- 사용자 흐름, 릴리즈, 보안에 어떤 영향이 있는지 적었는가
|
||||
- 관련 문서 업데이트 필요 여부를 확인했는가
|
||||
|
||||
## Licensing
|
||||
|
||||
일반적인 코드와 문서 기여는 이 저장소의 [Apache License 2.0](LICENSE) 조건으로 받습니다.
|
||||
브랜드 자산, 로고, 별도 권리 정리가 필요한 자료는 [CONTRIBUTOR_LICENSE_POLICY.md](CONTRIBUTOR_LICENSE_POLICY.md)를 참고하세요.
|
||||
18
CONTRIBUTOR_LICENSE_POLICY.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Contributor License Policy
|
||||
|
||||
일반적인 코드와 문서 기여는 이 저장소의 [Apache License 2.0](LICENSE) 조건으로 받습니다.
|
||||
|
||||
## Default Rule
|
||||
|
||||
- PR을 제출하면 해당 변경을 이 저장소 라이선스 조건으로 제공할 권리가 있다고 간주합니다.
|
||||
- 별도 CLA는 일반적인 코드와 문서 기여에 요구하지 않습니다.
|
||||
|
||||
## Extra Review Cases
|
||||
|
||||
아래 항목은 추가 확인이 필요할 수 있습니다.
|
||||
|
||||
- 로고, 브랜드 자산, 디자인 원본
|
||||
- 제3자 권리가 얽힌 자료
|
||||
- 별도 재배포 제한이 있는 외부 자산
|
||||
|
||||
문의: [contact@physia.kr](mailto:contact@physia.kr)
|
||||
18
DEPLOYMENT_MODES.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Deployment Modes
|
||||
|
||||
KoTalk는 하나의 운영 방식만을 전제로 설명하지 않습니다.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Description | Current read |
|
||||
|---|---|---|
|
||||
| Official service | PHYSIA가 운영하는 공개 서비스와 다운로드 미러 | Alpha 운영 |
|
||||
| Self-hosted | 저장소와 배포 골격을 직접 설치·운영 | 공개 문서 제공 |
|
||||
| Private network | 외부 인터넷 없이 조직 내부에 설치 | 방향 정의 단계 |
|
||||
| Dedicated environment | 조직별 분리 운영, 별도 지원 | 방향 정의 단계 |
|
||||
|
||||
## Boundary Rules
|
||||
|
||||
- 저장소 라이선스와 운영 서비스 약관은 같은 표면이 아닙니다.
|
||||
- 공식 서비스의 운영 책임은 PHYSIA가 지고, 자체 호스팅 운영 책임은 설치 주체가 집니다.
|
||||
- 폐쇄망과 규제 환경 배포는 “가능성”과 “검증 완료”를 구분해서 설명합니다.
|
||||
138
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Development Guide
|
||||
|
||||
## Naming Note
|
||||
|
||||
공개 브랜드는 `KoTalk`이지만, 현재 저장소의 프로젝트 파일과 네임스페이스는 아직 `PhysOn.*`를 사용합니다.
|
||||
문서 개편이 먼저 진행 중이며, 코드 네임스페이스 정렬은 별도 작업으로 다룹니다.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `.NET 8 SDK`
|
||||
- Git
|
||||
- Node.js 20+
|
||||
- Windows portable 빌드를 만들려면 `win-x64` publish 가능한 .NET 환경
|
||||
|
||||
Android 채널을 다룰 때는 아래가 추가로 필요합니다.
|
||||
|
||||
- `OpenJDK 17+`
|
||||
- `.NET Android workload`
|
||||
- Android SDK / cmdline-tools
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd vs-messanger
|
||||
dotnet build PhysOn.sln -c Debug
|
||||
```
|
||||
|
||||
## Run The API
|
||||
|
||||
```bash
|
||||
dotnet run --project src/PhysOn.Api --urls http://127.0.0.1:5082
|
||||
```
|
||||
|
||||
기본 확인 URL:
|
||||
|
||||
- [http://127.0.0.1:5082/health](http://127.0.0.1:5082/health)
|
||||
- [http://127.0.0.1:5082/](http://127.0.0.1:5082/)
|
||||
|
||||
접근 게이트나 시드 값은 공개 문서에 고정하지 않습니다. 필요한 값은 로컬 환경 변수나 비공개 배포 설정에서 넣어야 합니다.
|
||||
|
||||
## Run The Desktop Client
|
||||
|
||||
```bash
|
||||
dotnet run --project src/PhysOn.Desktop
|
||||
```
|
||||
|
||||
기본 입력값:
|
||||
|
||||
- 서버 주소: `http://127.0.0.1:5082`
|
||||
|
||||
## Run The Mobile Web Client
|
||||
|
||||
```bash
|
||||
cd src/PhysOn.Web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
기본 개발 주소:
|
||||
|
||||
- 웹앱: [http://127.0.0.1:4173](http://127.0.0.1:4173)
|
||||
- API 프록시 기본값: [http://127.0.0.1:5082](http://127.0.0.1:5082)
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
dotnet test tests/PhysOn.Api.IntegrationTests/PhysOn.Api.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
필요 시 전체 확인:
|
||||
|
||||
```bash
|
||||
dotnet build PhysOn.sln -c Debug
|
||||
dotnet test PhysOn.sln -c Debug
|
||||
```
|
||||
|
||||
## Release Builds
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
공개 산출물 네이밍은 `KoTalk-*` 기준으로 정리하는 방향이고, 현재 내부 스크립트와 프로젝트명은 별도 정렬 단계에 있습니다.
|
||||
|
||||
## Release Metadata
|
||||
|
||||
```bash
|
||||
./scripts/release/release-prepare-assets.sh \
|
||||
--version v0.1.0-alpha.1 \
|
||||
--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 \
|
||||
--screenshots artifacts/screenshots \
|
||||
--force
|
||||
```
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- 공개 웹 진입점: [vstalk.phy.kr](https://vstalk.phy.kr)
|
||||
- 공식 다운로드 미러: [download-vstalk.phy.kr](https://download-vstalk.phy.kr)
|
||||
- 저장소 릴리즈 경로: [RELEASING.md](RELEASING.md)
|
||||
|
||||
실제 호스트 주소, 관리자 계정, 배포용 비밀값은 공개 문서에 적지 않습니다.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Desktop window does not open on Linux/WSL
|
||||
|
||||
- X 서버 또는 데스크톱 세션이 있는지 확인합니다.
|
||||
- GUI가 없는 환경이면 API와 테스트만 먼저 확인합니다.
|
||||
|
||||
### Download links do not open
|
||||
|
||||
- [download-vstalk.phy.kr](https://download-vstalk.phy.kr) DNS와 HTTPS 상태를 확인합니다.
|
||||
- 저장소 릴리즈 경로가 최신인지 함께 확인합니다.
|
||||
|
||||
### Web app does not open
|
||||
|
||||
- [vstalk.phy.kr](https://vstalk.phy.kr) DNS와 프록시 상태를 확인합니다.
|
||||
- 정적 파일 배포 루트가 맞는지 확인합니다.
|
||||
60
FAQ.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# FAQ
|
||||
|
||||
KoTalk를 처음 보는 사람을 위한 짧은 안내입니다.
|
||||
|
||||
## KoTalk는 무엇인가요
|
||||
|
||||
KoTalk는 한국어 중심의 메시징 경험을 다시 설계하는 오픈소스 프로젝트입니다. 현재는 Windows 데스크톱과 모바일 웹을 우선 채널로 두고, Android를 병렬 확장하고 있습니다.
|
||||
|
||||
## 왜 Windows를 먼저 만드나요
|
||||
|
||||
이 프로젝트의 강점이 큰 화면에서 드러나는 검색, 복귀, 다중 창, 후속조치 흐름에 있기 때문입니다. 모바일보다 데스크톱에서 생산성 차이가 더 분명하게 드러난다고 판단했습니다.
|
||||
|
||||
## 모바일 웹은 어디서 볼 수 있나요
|
||||
|
||||
[vstalk.phy.kr](https://vstalk.phy.kr)에서 현재 공개 중인 모바일 웹 진입점을 확인할 수 있습니다.
|
||||
|
||||
## 특정 메신저를 그대로 복제하려는 프로젝트인가요
|
||||
|
||||
아닙니다. 익숙한 구조는 참고하지만, 목표는 더 낮은 피로도와 더 짧은 복귀 흐름입니다. 복제보다 정제에 가깝습니다.
|
||||
|
||||
## 지금 실제로 되는 것은 무엇인가요
|
||||
|
||||
- Windows 빌드
|
||||
- 모바일 웹 라이브 진입
|
||||
- 기본 계정 생성과 세션 유지
|
||||
- 대화 목록, 메시지 전송, 읽기 반영
|
||||
- 검색/보관/빈 상태 1차 UX
|
||||
|
||||
정확한 범위는 [PROJECT_STATUS.md](PROJECT_STATUS.md)에서 확인할 수 있습니다.
|
||||
|
||||
## 가입 방식은 어떻게 가나요
|
||||
|
||||
현재 알파 단계에서는 통제된 접근 정책을 쓰고 있지만, 공개 기획 기준은 `초대코드 중심`보다 `이메일 또는 휴대폰 기반 1회성 인증` 쪽으로 이동하고 있습니다. 자세한 배경은 [문서/10-signup-onboarding-and-auth-policy.md](문서/10-signup-onboarding-and-auth-policy.md)에 정리했습니다.
|
||||
|
||||
## 다운로드는 어디서 받나요
|
||||
|
||||
공식 미러와 저장소 릴리즈를 함께 제공합니다.
|
||||
|
||||
- 공식 미러: [download-vstalk.phy.kr](https://download-vstalk.phy.kr)
|
||||
- 제2 공개 레포: [physia.kr/open-source/projects/public/kotalk](https://physia.kr/open-source/projects/public/kotalk)
|
||||
- Forge releases: [git.physia.kr/ian/vs-messanger/releases](https://git.physia.kr/ian/vs-messanger/releases)
|
||||
- GitHub releases: [github.com/werther24601/kotalk/releases](https://github.com/werther24601/kotalk/releases)
|
||||
|
||||
현재 미러 정합성 상태는 [PROJECT_STATUS.md](PROJECT_STATUS.md), 릴리즈 규칙은 [RELEASING.md](RELEASING.md)에 기록합니다.
|
||||
|
||||
## 공식 서비스와 오픈소스 저장소는 같은가요
|
||||
|
||||
같지 않습니다. 저장소는 오픈소스 코어와 공개 문서를 다루고, 운영 서비스는 별도 표면으로 관리합니다. 이 경계는 [BUSINESS_MODEL.md](BUSINESS_MODEL.md)와 [DEPLOYMENT_MODES.md](DEPLOYMENT_MODES.md)에 설명해 두었습니다.
|
||||
|
||||
## 라이선스는 무엇인가요
|
||||
|
||||
현재 저장소는 [Apache License 2.0](LICENSE)을 사용합니다. 상표는 별도 정책을 따르므로 [TRADEMARKS.md](TRADEMARKS.md)도 함께 봐야 합니다.
|
||||
|
||||
## 기여는 어떻게 시작하나요
|
||||
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md), [FIRST_CONTRIBUTION.md](FIRST_CONTRIBUTION.md), [COMMUNITY.md](COMMUNITY.md)를 순서대로 보면 가장 빠릅니다.
|
||||
|
||||
## 보안 이슈는 어디로 알려야 하나요
|
||||
|
||||
공개 이슈보다 먼저 [SECURITY.md](SECURITY.md)에 적힌 비공개 경로를 사용해 주세요.
|
||||
45
FIRST_CONTRIBUTION.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# First Contribution
|
||||
|
||||
처음 기여한다면 이 문서만 먼저 보면 됩니다.
|
||||
|
||||
## 1. 현재 상태 읽기
|
||||
|
||||
아래 순서로 읽으면 충돌 없이 맥락을 잡을 수 있습니다.
|
||||
|
||||
1. [README.md](README.md)
|
||||
2. [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
3. [ROADMAP.md](ROADMAP.md)
|
||||
|
||||
## 2. 작업 영역 고르기
|
||||
|
||||
- 문서/정합성: README, 상태표, 문서 링크, 스크린샷
|
||||
- UX 개선: 모바일 웹, 데스크톱 사용 흐름
|
||||
- 인프라/릴리즈: 배포, 다운로드, 릴리즈 메타데이터
|
||||
|
||||
## 3. 난이도 가이드
|
||||
|
||||
- 쉬움: 오탈자, 링크 정리, 상태표 보강, 스크린샷 설명 개선
|
||||
- 중간: 모바일 웹 UI, 문서-구현 정합성 보강, 배포 스크립트 개선
|
||||
- 어려움: 인증/세션, 릴리즈 파이프라인, 채널 구조 변경
|
||||
|
||||
## 4. 브랜치 이름
|
||||
|
||||
- `docs/<topic>`
|
||||
- `feat/<topic>`
|
||||
- `hotfix/<topic>`
|
||||
|
||||
상세 규칙은 [BRANCHING_STRATEGY.md](BRANCHING_STRATEGY.md)를 참고하세요.
|
||||
|
||||
## 5. PR 전 마지막 확인
|
||||
|
||||
- README 또는 상태표 갱신이 필요한가
|
||||
- CHANGELOG 반영이 필요한가
|
||||
- 스크린샷이 현재 상태와 맞는가
|
||||
- 릴리즈/배포 경로에 영향이 있는가
|
||||
|
||||
## 6. 도움되는 문서
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
- [COMMUNITY.md](COMMUNITY.md)
|
||||
- [MAINTAINERS.md](MAINTAINERS.md)
|
||||
- [RELEASING.md](RELEASING.md)
|
||||
30
GOVERNANCE.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Governance
|
||||
|
||||
KoTalk는 현재 단일 메인테이너 중심의 공개 알파 프로젝트입니다.
|
||||
의사결정은 빠르게 하되, 공개 문서가 실제 상태보다 앞서 나가지 않도록 운영합니다.
|
||||
|
||||
## Decision Order
|
||||
|
||||
1. 사용자가 더 빨리 읽고, 답하고, 복귀할 수 있는가
|
||||
2. 한국어 UI와 Windows 중심 흐름에 맞는가
|
||||
3. 문서와 구현이 같은 방향을 유지하는가
|
||||
4. 릴리즈와 배포 표면이 더 단순해지는가
|
||||
5. 보안과 운영 리스크를 키우지 않는가
|
||||
|
||||
## What Needs Review
|
||||
|
||||
- 가입/인증 정책 변경
|
||||
- 보안 모델 변경
|
||||
- 다운로드 경로 변경
|
||||
- UI 정보구조의 큰 이동
|
||||
- 공개 상태 문서와 어긋날 수 있는 기능 추가
|
||||
|
||||
## Public Rules
|
||||
|
||||
- README는 과장 대신 검증된 상태만 적습니다.
|
||||
- 스크린샷, 릴리즈, CHANGELOG는 가능한 한 같은 시점 감각으로 갱신합니다.
|
||||
- 보안과 운영은 “현재 적용 범위”와 “남은 과제”를 분리해 씁니다.
|
||||
|
||||
## Maintainer
|
||||
|
||||
현재 운영과 최종 승인 책임은 [MAINTAINERS.md](MAINTAINERS.md)에 적힌 메인테이너가 맡습니다.
|
||||
201
LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
22
LICENSE-FAQ.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# License FAQ
|
||||
|
||||
## 현재 라이선스는 무엇인가요
|
||||
|
||||
이 저장소의 코드와 문서는 [Apache License 2.0](LICENSE)을 따릅니다.
|
||||
|
||||
## 왜 Apache-2.0인가요
|
||||
|
||||
오픈소스 채택 장벽을 낮추면서도, 특허와 기여 범위를 더 분명하게 설명할 수 있기 때문입니다.
|
||||
|
||||
## 상업적으로 써도 되나요
|
||||
|
||||
코드 라이선스 범위 안에서는 가능합니다. 다만 브랜드, 공식 서비스, 지원 계약은 별도 표면입니다.
|
||||
|
||||
## 기여에 별도 CLA가 필요한가요
|
||||
|
||||
일반적인 코드와 문서 기여에는 별도 CLA를 요구하지 않습니다.
|
||||
상표, 로고, 별도 권리 정리가 필요한 자료는 추가 합의가 필요할 수 있습니다.
|
||||
|
||||
## 상표도 같은 라이선스인가요
|
||||
|
||||
아닙니다. 상표는 [TRADEMARKS.md](TRADEMARKS.md)를 따릅니다.
|
||||
24
MAINTAINERS.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Maintainers
|
||||
|
||||
## Current Maintainer
|
||||
|
||||
- Name: `이재협`
|
||||
- Public handle: `Ian Werther`
|
||||
- GitHub: [werther24601](https://github.com/werther24601)
|
||||
- Organization: `PHYSIA`
|
||||
- Primary contact: [ian@physia.kr](mailto:ian@physia.kr)
|
||||
|
||||
## Maintainer Scope
|
||||
|
||||
- 제품 방향과 공개 저장소 표면
|
||||
- 릴리즈와 상태 문서 정합성
|
||||
- 보안/배포 정책의 최종 승인
|
||||
- Windows, 웹, Android 우선순위 조정
|
||||
|
||||
## Working Style
|
||||
|
||||
- 실제 사용 흐름 개선을 우선합니다.
|
||||
- 과장된 약속보다 상태와 근거를 먼저 맞춥니다.
|
||||
- 큰 방향 변경은 문서와 함께 남깁니다.
|
||||
|
||||
일반 문의는 [SUPPORT.md](SUPPORT.md), 운영 원칙은 [GOVERNANCE.md](GOVERNANCE.md)를 참고하세요.
|
||||
19
PORTFOLIO_CAPABILITIES.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# PHYSIA Portfolio Capabilities
|
||||
|
||||
이 문서는 KoTalk 저장소가 보여 주는 PHYSIA의 엔지니어링 역량을 정리합니다.
|
||||
|
||||
## What This Repository Demonstrates
|
||||
|
||||
| Capability | Evidence in this repository |
|
||||
|---|---|
|
||||
| Product design | 한국어 UI, 낮은 피로도, 데스크톱 생산성 중심의 제품 방향 |
|
||||
| Multi-platform planning | Windows, 모바일 웹, Android 병렬 전략 |
|
||||
| Backend implementation | `ASP.NET Core`, 인증, 세션, 실시간 연결, 데이터 저장 |
|
||||
| Desktop client delivery | `.NET` 기반 데스크톱 셸과 멀티 윈도우 구조 |
|
||||
| Release operations | 스크린샷, CHANGELOG, 릴리즈 메타데이터, 다운로드 표면 정합성 |
|
||||
| Security posture | 보안 정책, 제보 경로, 데이터 경계, 릴리즈 무결성 문서화 |
|
||||
| Self-hosting readiness | 공개 배포 골격, 운영 경계, 배포 모드 설명 |
|
||||
|
||||
## Public Position
|
||||
|
||||
KoTalk는 단순한 데모가 아니라, 제품 기획과 구현, 운영 표면, 보안 설명 가능성을 함께 보여 주는 저장소를 목표로 합니다.
|
||||
26
PRIVACY_AND_DATA_HANDLING.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Privacy And Data Handling
|
||||
|
||||
이 문서는 현재 구현 기준에서 어떤 데이터가 어떤 경계에 놓이는지 설명합니다.
|
||||
|
||||
## Current Data Categories
|
||||
|
||||
| Category | Examples | Current purpose |
|
||||
|---|---|---|
|
||||
| Account identity | 표시 이름, 사용자 ID | 계정 식별 |
|
||||
| Device identity | install ID, device name, app version | 세션 구분 |
|
||||
| Session data | session ID, token family, refresh token hash | 인증과 세션 회전 |
|
||||
| Conversation data | 대화 제목, 멤버, 메시지 본문, 읽기 커서 | 메시징 기능 |
|
||||
| Operational logs | 오류, 상태, 제한적 진단 정보 | 장애 분석과 보안 대응 |
|
||||
|
||||
## Handling Principles
|
||||
|
||||
- refresh token 원문은 서버 저장소에 평문으로 남기지 않습니다.
|
||||
- 메시지 본문과 민감정보는 로그 기본값으로 남기지 않습니다.
|
||||
- 운영자가 접근할 수 있는 데이터 범위는 최소화하는 방향으로 설계합니다.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- 정식 개인정보 처리방침 수준의 법률 문서화는 아직 아닙니다.
|
||||
- 보존 기간, 삭제 절차, 접근 감사는 더 구체화가 필요합니다.
|
||||
|
||||
문의: [help@physia.kr](mailto:help@physia.kr)
|
||||
24
PROCUREMENT_READINESS.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Procurement Readiness
|
||||
|
||||
이 문서는 KoTalk가 규제 환경과 기관 검토에서 어떤 준비 상태에 있는지 과장 없이 정리합니다.
|
||||
|
||||
## Current Position
|
||||
|
||||
현재 단계는 `공개 검토 가능` 수준의 알파입니다. 즉시 조달 가능 제품으로 주장하지 않으며, 준비된 항목과 남은 항목을 구분합니다.
|
||||
|
||||
## Snapshot
|
||||
|
||||
| Area | Current read |
|
||||
|---|---|
|
||||
| 공개 문서와 상태표 | 부분 준비 |
|
||||
| 보안 정책과 제보 경로 | 부분 준비 |
|
||||
| 자체 호스팅 방향 | 부분 준비 |
|
||||
| 폐쇄망 설치 검증 | 미검증 |
|
||||
| 감사 로그와 운영 증빙 | 미완성 |
|
||||
| 백업/복구 절차 | 미완성 |
|
||||
|
||||
## Principle
|
||||
|
||||
- 지원 가능성과 현재 제공 중인 범위를 분리해서 씁니다.
|
||||
- 규제 환경 대응은 마케팅 문구보다 운영 증빙이 먼저입니다.
|
||||
- 검증 전 항목은 준비 방향으로만 기록합니다.
|
||||
114
PROJECT_STATUS.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Project Status
|
||||
|
||||
마지막 검증일: `2026-04-16`
|
||||
|
||||
## Status Dashboard
|
||||
|
||||
| Signal | Current read |
|
||||
|---|---|
|
||||
| Public brand | `KoTalk` |
|
||||
| Stage | `Alpha` |
|
||||
| Most usable surface | Mobile web live + Windows build |
|
||||
| Biggest current gap | Android 실빌드와 다운로드 미러 정합성 |
|
||||
| Signup direction | 공개형 1회성 인증 중심으로 재설계 중 |
|
||||
| Tone of this repo | 현재 동작 범위와 남은 갭을 함께 적는 제품형 저장소 |
|
||||
|
||||
## What Exists Right Now
|
||||
|
||||
KoTalk는 아직 모든 플랫폼이 완성된 상태는 아니지만, “문서만 있는 프로젝트” 단계는 이미 지났습니다. 현재 저장소 기준으로 아래 항목을 실제로 확인할 수 있습니다.
|
||||
|
||||
- Windows 데스크톱 클라이언트 빌드
|
||||
- 모바일 웹 실서비스 채널
|
||||
- 기본 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 복구 루프
|
||||
- 최신 기준 스크린샷 세트
|
||||
- 릴리즈 경로와 다운로드 경로 문서
|
||||
|
||||
## Channel Status
|
||||
|
||||
| Channel | Surface | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| 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) | Normalizing | 2026-04-16 기준 DNS/HTTPS 정합성 확인 필요 |
|
||||
|
||||
## Verified Now
|
||||
|
||||
현재 기준으로 확인된 사실만 적습니다.
|
||||
|
||||
- Windows 클라이언트는 저장소 기준으로 빌드 가능한 상태입니다.
|
||||
- 모바일 웹은 [vstalk.phy.kr](https://vstalk.phy.kr)에서 공개 중입니다.
|
||||
- 기본 메시징 루프와 세션 복구 흐름은 구현돼 있습니다.
|
||||
- 검색, 보관, 빈 상태 UX는 1차 개편이 반영돼 있습니다.
|
||||
- 최신 스크린샷은 저장소에 함께 보관됩니다.
|
||||
|
||||
## Visual Proof
|
||||
|
||||
| Surface | Proof |
|
||||
|---|---|
|
||||
| Desktop shell | [hero-shell.png](docs/assets/latest/hero-shell.png) |
|
||||
| Desktop onboarding | [onboarding.png](docs/assets/latest/onboarding.png) |
|
||||
| Desktop conversation | [conversation.png](docs/assets/latest/conversation.png) |
|
||||
| Mobile web onboarding | [vstalk-web-onboarding.png](docs/assets/latest/vstalk-web-onboarding.png) |
|
||||
| Mobile web inbox | [vstalk-web-list.png](docs/assets/latest/vstalk-web-list.png) |
|
||||
| Mobile web search | [vstalk-web-search.png](docs/assets/latest/vstalk-web-search.png) |
|
||||
| Mobile web saved | [vstalk-web-saved.png](docs/assets/latest/vstalk-web-saved.png) |
|
||||
| Mobile web chat | [vstalk-web-chat.png](docs/assets/latest/vstalk-web-chat.png) |
|
||||
|
||||
## Product Direction That Is Already Visible
|
||||
|
||||
현재 화면만 봐도 읽히는 제품 방향은 아래와 같습니다.
|
||||
|
||||
- 메시징을 중심에 두고, 피드형 잡음을 덜어내려는 구조
|
||||
- 텍스트를 장황하게 읽게 하기보다 구조와 위치로 이해시키는 UI
|
||||
- 한국어 데스크톱 사용성, 특히 반복적인 읽기와 답장 흐름을 중시하는 설계
|
||||
- 단순한 “보여주기용 스크린샷”이 아니라 실제 릴리즈와 상태 문서에 연결된 표면
|
||||
|
||||
## In Progress
|
||||
|
||||
- Android 첫 실사용 빌드
|
||||
- 공개 다운로드 미러 정합성
|
||||
- 릴리즈 페이지와 미러 간 latest 라우트 통합
|
||||
- 검색 범위 확장
|
||||
- 파일 전송
|
||||
- 데스크톱 멀티 윈도우 생산성 강화
|
||||
|
||||
## Current Limits
|
||||
|
||||
아직 부족한 부분도 그대로 남깁니다.
|
||||
|
||||
- Android 실사용 빌드는 아직 제공되지 않습니다.
|
||||
- 파일 전송은 미구현입니다.
|
||||
- 검색은 전역 파일/링크/사람 범위까지 확장되지 않았습니다.
|
||||
- 공식 다운로드 미러는 DNS/HTTPS 정상화가 끝나야 안정 채널로 표기할 수 있습니다.
|
||||
- 데스크톱 멀티 윈도우는 방향은 잡혀 있지만, 실제 생산성 흐름은 더 다듬어야 합니다.
|
||||
|
||||
## Download And Release Paths
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | 공식 다운로드 미러 주소 |
|
||||
| [download-vstalk.phy.kr/windows/latest](https://download-vstalk.phy.kr/windows/latest) | Windows latest |
|
||||
| [download-vstalk.phy.kr/android/latest](https://download-vstalk.phy.kr/android/latest) | Android latest |
|
||||
| [download-vstalk.phy.kr/latest/version.json](https://download-vstalk.phy.kr/latest/version.json) | 버전 메타데이터 |
|
||||
| [physia.kr/open-source/projects/public/kotalk](https://physia.kr/open-source/projects/public/kotalk) | 제2 공개 레포 |
|
||||
| [Forge releases](https://git.physia.kr/ian/vs-messanger/releases) | 저장소 릴리즈 채널 |
|
||||
| [GitHub releases](https://github.com/werther24601/kotalk/releases) | 공개 릴리즈 채널 |
|
||||
|
||||
## Why This Repo May Feel Denser Again
|
||||
|
||||
최근 공개 표면은 리스크를 줄이려는 과정에서 지나치게 건조해졌습니다. 현재는 다시 아래 균형을 맞추는 방향으로 조정 중입니다.
|
||||
|
||||
- 화면과 스크린샷은 충분히 보여 주되, 과장된 약속은 줄이기
|
||||
- 제품 배경과 문제의식은 다시 설명하되, 감정적인 공격은 피하기
|
||||
- 상태 문서는 짧게 유지하되, 이 저장소가 왜 존재하는지는 읽히게 만들기
|
||||
|
||||
배경 문서는 [BACKGROUND.md](BACKGROUND.md), 더 긴 맥락은 [문서/14-project-background-and-market-context.md](문서/14-project-background-and-market-context.md)에서 확인할 수 있습니다.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- 사용자 관점 리뷰: [문서/31-user-review-log-and-experience-scorecard.md](문서/31-user-review-log-and-experience-scorecard.md)
|
||||
- 현재 모바일 웹 리뷰: [문서/89-current-product-mobile-web-review-2026-04.md](문서/89-current-product-mobile-web-review-2026-04.md)
|
||||
- 라이브 우선순위: [문서/35-live-user-review-and-priority-backlog.md](문서/35-live-user-review-and-priority-backlog.md)
|
||||
- 가입/온보딩 정책: [문서/10-signup-onboarding-and-auth-policy.md](문서/10-signup-onboarding-and-auth-policy.md)
|
||||
- 신뢰와 보안 표면: [TRUST_CENTER.md](TRUST_CENTER.md), [SECURITY.md](SECURITY.md)
|
||||
78
PhysOn.sln
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BD1AEF6A-5B59-4DAC-82C9-1983916200FB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Api", "src\PhysOn.Api\PhysOn.Api.csproj", "{DD8A5885-9647-4248-A0B2-C8695BBFB54E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Domain", "src\PhysOn.Domain\PhysOn.Domain.csproj", "{53C0FE48-1759-46FC-B61D-56AD0B60941E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Contracts", "src\PhysOn.Contracts\PhysOn.Contracts.csproj", "{737314E6-E8E6-43BD-8806-06B82C565A85}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Infrastructure", "src\PhysOn.Infrastructure\PhysOn.Infrastructure.csproj", "{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Application", "src\PhysOn.Application\PhysOn.Application.csproj", "{A3D77E81-11B9-4C51-872B-28DABEB6F665}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Worker", "src\PhysOn.Worker\PhysOn.Worker.csproj", "{925D92EC-965D-44D9-A3FF-011E1815C9EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Desktop", "src\PhysOn.Desktop\PhysOn.Desktop.csproj", "{90EF510F-E338-45C4-9EF4-0A4916725EF0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{04EBE1E2-D374-4F58-A0D5-5062BB4674FA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysOn.Api.IntegrationTests", "tests\PhysOn.Api.IntegrationTests\PhysOn.Api.IntegrationTests.csproj", "{38821CD9-915B-4C96-8A35-11BDE991C16E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{53C0FE48-1759-46FC-B61D-56AD0B60941E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{53C0FE48-1759-46FC-B61D-56AD0B60941E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{53C0FE48-1759-46FC-B61D-56AD0B60941E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{53C0FE48-1759-46FC-B61D-56AD0B60941E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{737314E6-E8E6-43BD-8806-06B82C565A85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{737314E6-E8E6-43BD-8806-06B82C565A85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{737314E6-E8E6-43BD-8806-06B82C565A85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{737314E6-E8E6-43BD-8806-06B82C565A85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A3D77E81-11B9-4C51-872B-28DABEB6F665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A3D77E81-11B9-4C51-872B-28DABEB6F665}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A3D77E81-11B9-4C51-872B-28DABEB6F665}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A3D77E81-11B9-4C51-872B-28DABEB6F665}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{38821CD9-915B-4C96-8A35-11BDE991C16E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{DD8A5885-9647-4248-A0B2-C8695BBFB54E} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{53C0FE48-1759-46FC-B61D-56AD0B60941E} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{737314E6-E8E6-43BD-8806-06B82C565A85} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{F13AF8BF-92CF-444B-B410-1BFA38DAD0CB} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{A3D77E81-11B9-4C51-872B-28DABEB6F665} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{925D92EC-965D-44D9-A3FF-011E1815C9EE} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{90EF510F-E338-45C4-9EF4-0A4916725EF0} = {BD1AEF6A-5B59-4DAC-82C9-1983916200FB}
|
||||
{38821CD9-915B-4C96-8A35-11BDE991C16E} = {04EBE1E2-D374-4F58-A0D5-5062BB4674FA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
190
README.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# KoTalk
|
||||
|
||||
<p align="center">
|
||||
<strong>한국어 중심의 차분한 메시징 경험을 다시 설계하는 오픈소스 프로젝트.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Windows 데스크톱을 중심에 두고, 모바일 웹과 Android를 병렬 확장하는 메신저 저장소입니다.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
짧은 답장, 빠른 복귀, 설명 가능한 제품 표면, 그리고 한국어 사용 습관에 맞는 조용한 UI를 핵심 기준으로 삼습니다.
|
||||
</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>
|
||||
<a href="문서/README.md"><img alt="docs" src="https://img.shields.io/badge/docs-master%20plan%20%2B%20atlas-111827"></a>
|
||||
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-Apache--2.0-1F2937"></a>
|
||||
<a href="https://vstalk.phy.kr"><img alt="web" src="https://img.shields.io/badge/live-vstalk.phy.kr-0F766E"></a>
|
||||
<a href="https://download-vstalk.phy.kr"><img alt="download" src="https://img.shields.io/badge/download-mirror%20reserved-F59E0B"></a>
|
||||
<a href="PROJECT_STATUS.md"><img alt="verified" src="https://img.shields.io/badge/verified-2026--04--16-6B7280"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="PROJECT_STATUS.md">Project Status</a> ·
|
||||
<a href="SHOWCASE.md">Showcase</a> ·
|
||||
<a href="BACKGROUND.md">Background</a> ·
|
||||
<a href="FAQ.md">FAQ</a> ·
|
||||
<a href="RELEASING.md">Releases</a> ·
|
||||
<a href="TRUST_CENTER.md">Trust Center</a> ·
|
||||
<a href="ARCHITECTURE.md">Architecture</a> ·
|
||||
<a href="문서/README.md">Master Plan</a> ·
|
||||
<a href="CONTRIBUTING.md">Contributing</a>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://vstalk.phy.kr"><strong>Try Web</strong></a><br><sub>모바일 웹 진입점</sub></td>
|
||||
<td align="center"><a href="SHOWCASE.md"><strong>See Screens</strong></a><br><sub>최신 화면 묶음</sub></td>
|
||||
<td align="center"><a href="PROJECT_STATUS.md"><strong>Read Status</strong></a><br><sub>현재 동작 범위</sub></td>
|
||||
<td align="center"><a href="RELEASING.md"><strong>Get Builds</strong></a><br><sub>미러와 릴리즈 경로</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/latest/hero-shell.png" alt="KoTalk desktop shell" width="920">
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>플랫한 화이트 톤과 컴팩트한 창 밀도를 기준으로 정리한 현재 데스크톱 셸</em>
|
||||
</p>
|
||||
|
||||
## Snapshot
|
||||
|
||||
| Layer | Current read |
|
||||
|---|---|
|
||||
| What this is | 한국어 UI, 낮은 피로도, 업무형 메시징 복귀 흐름을 중심에 둔 Windows-first 오픈소스 메신저 |
|
||||
| What works now | Windows 빌드, 모바일 웹 라이브, 기본 인증/대화/세션 루프 |
|
||||
| What is still moving | Android 첫 실빌드, 파일 전송, 검색 확장, 공개 다운로드 미러 정합성 |
|
||||
| What this repo tries to show | 화면, 빌드 산출물, 릴리즈 경로, 상태 문서, 배경 문서를 한 눈에 읽히게 정리한 제품형 저장소 |
|
||||
|
||||
## Why Now
|
||||
|
||||
KoTalk는 단순히 새로운 메신저를 하나 더 만드는 시도가 아닙니다. 최근 몇 년간 국내 메신저 여론에서 반복적으로 드러난 피로는 대체로 비슷했습니다. 대화보다 콘텐츠가 먼저 보이는 구조, 설명이 늦는 운영정책, 신뢰를 깎는 개인정보 이슈, 반복 장애에 대한 피로감이 그것입니다.
|
||||
|
||||
이 저장소는 그 불만을 과격하게 소비하려는 프로젝트가 아니라, 더 조용하고 더 짧고 더 예측 가능한 한국어 메시징 경험을 다시 설계해 보려는 제안입니다. 익숙한 메신저 문법은 존중하되, 반복해서 확인하는 목록, 답장을 미루지 않게 돕는 흐름, 업무적 대화와 사적 대화가 서로 피로를 만들지 않는 구조를 우선합니다.
|
||||
|
||||
특히 최근 공개 기사와 여론에서 반복적으로 읽히는 네 가지 신호를 중요한 배경으로 봅니다.
|
||||
|
||||
- 친구 탭 피드화, 숏폼, 광고 노출 확대에 대한 강한 UI 피로
|
||||
- 개인정보와 프라이버시 이슈 이후 높아진 기본 보안 기대
|
||||
- 운영정책 강화가 있을 때 더 크게 요구되는 설명 책임
|
||||
- PC 로그인과 메시지 전송 장애가 반복될 때 누적되는 불신
|
||||
|
||||
KoTalk는 이 배경을 리스크 문구로 숨기지 않고, 왜 이 프로젝트가 필요한지 설명하는 핵심 맥락으로 다룹니다. 배경 요약은 [BACKGROUND.md](BACKGROUND.md), 더 긴 맥락은 [문서/14-project-background-and-market-context.md](문서/14-project-background-and-market-context.md)에 정리돼 있습니다.
|
||||
|
||||
## What Makes KoTalk Different
|
||||
|
||||
| Focus | KoTalk approach |
|
||||
|---|---|
|
||||
| Desktop priority | Windows를 중심축으로 두고, 넓은 화면에서 짧은 클릭 수와 멀티 윈도우 흐름을 우선합니다. |
|
||||
| UI mood | 각진 패널, 얇은 보더, 플랫한 화이트 톤, 설명보다 구조가 먼저 보이는 화면을 지향합니다. |
|
||||
| Work communication | 검색, 보관, 후속조치, 복귀 시간을 줄이는 흐름을 제품 핵심으로 둡니다. |
|
||||
| Transparency | 상태 문서, 릴리즈 경로, 스크린샷, 현재 한계를 함께 적습니다. |
|
||||
| Deployment choice | 공개 서비스와 자체 호스팅 가능한 오픈소스 코어를 구분해 설명합니다. |
|
||||
|
||||
## Current Experience Shelf
|
||||
|
||||
현재 저장소에서 바로 볼 수 있는 화면과 산출물은 아래와 같습니다.
|
||||
|
||||
| Surface | What to look at | Visual |
|
||||
|---|---|---|
|
||||
| Desktop shell | 레일 + 목록 + 대화 중심의 3단 구조, 플랫한 보더, 멀티 윈도우 전제 | [hero-shell.png](docs/assets/latest/hero-shell.png) |
|
||||
| Desktop onboarding | 첫 실행 시 서버 주소보다 사용자 흐름을 먼저 보여주는 가벼운 진입 | [onboarding.png](docs/assets/latest/onboarding.png) |
|
||||
| Desktop conversation | 메시지 흐름, 읽기 상태, 입력 패널의 조밀한 배치 | [conversation.png](docs/assets/latest/conversation.png) |
|
||||
| Mobile web onboarding | 빠른 진입과 한국어 중심의 간결한 가입 흐름 | [vstalk-web-onboarding.png](docs/assets/latest/vstalk-web-onboarding.png) |
|
||||
| Mobile web inbox | 최근 대화, 필터, 검색 진입의 기본 구조 | [vstalk-web-list.png](docs/assets/latest/vstalk-web-list.png) |
|
||||
| Mobile web search | 대화 재발견과 보관 흐름의 1차 구현 | [vstalk-web-search.png](docs/assets/latest/vstalk-web-search.png) |
|
||||
| Mobile web saved | 나중에 답장, 중요 대화, 다시 열기 허브 | [vstalk-web-saved.png](docs/assets/latest/vstalk-web-saved.png) |
|
||||
| Mobile web chat | 모바일 입력창, 상단 정보 밀도, 복귀 동선 | [vstalk-web-chat.png](docs/assets/latest/vstalk-web-chat.png) |
|
||||
|
||||
전체 화면 묶음은 [SHOWCASE.md](SHOWCASE.md)에서 더 자세히 볼 수 있습니다.
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | Surface | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| 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) | Normalizing | 2026-04-16 기준 DNS/HTTPS 정합성 점검 진행 중 |
|
||||
|
||||
## Architecture Snapshot
|
||||
|
||||
KoTalk의 현재 구조는 지나치게 복잡한 플랫폼보다, 작은 조각을 조합해 실서비스와 로컬 빌드를 함께 검증하는 쪽에 가깝습니다.
|
||||
|
||||
- 클라이언트: Windows 데스크톱 + 모바일 웹 + Android 예정
|
||||
- API: 인증, 최근 대화, 메시지 전송, 읽기 커서, 세션 루프
|
||||
- 배포: VPS 기반 same-origin 웹앱과 API 운영
|
||||
- 공개 증거: 저장소 스크린샷, 빌드 산출물, 상태 문서, 릴리즈 경로
|
||||
|
||||
자세한 구성은 [ARCHITECTURE.md](ARCHITECTURE.md), 배포 모드는 [DEPLOYMENT_MODES.md](DEPLOYMENT_MODES.md), 로드맵은 [ROADMAP.md](ROADMAP.md)에서 확인할 수 있습니다.
|
||||
|
||||
## Download Paths
|
||||
|
||||
공식 링크 하나에만 의존하지 않고, 저장소 릴리즈 경로를 함께 제공합니다.
|
||||
|
||||
| Path | Link |
|
||||
|---|---|
|
||||
| Official mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) |
|
||||
| Windows latest | [download-vstalk.phy.kr/windows/latest](https://download-vstalk.phy.kr/windows/latest) |
|
||||
| Android latest | [download-vstalk.phy.kr/android/latest](https://download-vstalk.phy.kr/android/latest) |
|
||||
| Version manifest | [download-vstalk.phy.kr/latest/version.json](https://download-vstalk.phy.kr/latest/version.json) |
|
||||
| Public stage repo | [physia.kr/open-source/projects/public/kotalk](https://physia.kr/open-source/projects/public/kotalk) |
|
||||
| Forge releases | [git.physia.kr/ian/vs-messanger/releases](https://git.physia.kr/ian/vs-messanger/releases) |
|
||||
| GitHub releases | [github.com/werther24601/kotalk/releases](https://github.com/werther24601/kotalk/releases) |
|
||||
|
||||
릴리즈 정책과 현재 미러 상태는 [RELEASING.md](RELEASING.md), [PROJECT_STATUS.md](PROJECT_STATUS.md)에 함께 기록합니다.
|
||||
|
||||
## Principles
|
||||
|
||||
- 한국어 UI는 번역체보다 실제 사용 흐름을 우선합니다.
|
||||
- 둥글고 장식적인 UI보다 각진 구조, 플랫한 깊이, 짧은 텍스트, 높은 밀도를 택합니다.
|
||||
- 개인 대화와 업무형 소통 모두에서 검색, 복귀, 정리, 후속조치가 더 짧아져야 합니다.
|
||||
- 공식 서비스와 오픈소스 코어, 저장소 공개 표면은 같은 문장으로 뭉개지지 않게 설명합니다.
|
||||
- 보안은 과장된 문구보다 현재 적용 범위와 남은 과제를 함께 적는 방식으로 다룹니다.
|
||||
|
||||
## Reading Paths
|
||||
|
||||
처음 보는 독자라면 아래 순서가 가장 빠릅니다.
|
||||
|
||||
1. [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
2. [SHOWCASE.md](SHOWCASE.md)
|
||||
3. [BACKGROUND.md](BACKGROUND.md)
|
||||
4. [FAQ.md](FAQ.md)
|
||||
5. [문서/README.md](문서/README.md)
|
||||
|
||||
기여 관점이라면 [CONTRIBUTING.md](CONTRIBUTING.md), [COMMUNITY.md](COMMUNITY.md), [DEVELOPMENT.md](DEVELOPMENT.md)를 먼저 보면 됩니다.
|
||||
|
||||
제품 방향을 더 길게 읽고 싶다면 아래 문서를 권합니다.
|
||||
|
||||
- [ROADMAP.md](ROADMAP.md)
|
||||
- [BUSINESS_MODEL.md](BUSINESS_MODEL.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)
|
||||
|
||||
## Security And Trust
|
||||
|
||||
공개 저장소에서 약속하는 범위는 아래 문서에 나눠 적습니다.
|
||||
|
||||
- [TRUST_CENTER.md](TRUST_CENTER.md): 현재 통제, 남은 갭, 표현 원칙
|
||||
- [SECURITY.md](SECURITY.md): 제보 경로와 기본 보안 정책
|
||||
- [SECURITY_RESPONSE.md](SECURITY_RESPONSE.md): 접수와 공개 절차
|
||||
- [PRIVACY_AND_DATA_HANDLING.md](PRIVACY_AND_DATA_HANDLING.md): 현재 데이터 경계
|
||||
- [DEPLOYMENT_MODES.md](DEPLOYMENT_MODES.md): 공식 서비스, 셀프호스팅, 규제 환경 배포 구분
|
||||
|
||||
## Open-Source And Service Boundary
|
||||
|
||||
KoTalk는 저장소 공개 코드와 운영 서비스의 경계를 분명히 둡니다.
|
||||
|
||||
- 이 저장소는 Apache-2.0 기반의 오픈소스 코어와 공개 문서를 다룹니다.
|
||||
- 운영 서비스와 배포 지원은 별도 표면으로 설명합니다.
|
||||
- 자체 호스팅과 향후 사설망/폐쇄망 배포 가능성은 중요한 방향이지만, 검증 전 항목은 검증 완료처럼 쓰지 않습니다.
|
||||
|
||||
자세한 구조는 [BUSINESS_MODEL.md](BUSINESS_MODEL.md), [DEPLOYMENT_MODES.md](DEPLOYMENT_MODES.md), [PROCUREMENT_READINESS.md](PROCUREMENT_READINESS.md)에 정리해 두었습니다.
|
||||
|
||||
## Maintained By
|
||||
|
||||
KoTalk는 현재 PHYSIA가 유지하고 있습니다. 실무 연락은 [SUPPORT.md](SUPPORT.md), 운영 모델은 [GOVERNANCE.md](GOVERNANCE.md), 메인테이너 정보는 [MAINTAINERS.md](MAINTAINERS.md)를 참고하세요.
|
||||
38
RELEASING.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Releasing
|
||||
|
||||
KoTalk의 릴리즈는 단순한 파일 업로드가 아니라, 산출물과 공개 문서가 같은 상태를 가리키도록 맞추는 작업입니다.
|
||||
|
||||
## Release Surfaces
|
||||
|
||||
- 공식 다운로드 미러: [download-vstalk.phy.kr](https://download-vstalk.phy.kr)
|
||||
- Windows latest: [download-vstalk.phy.kr/windows/latest](https://download-vstalk.phy.kr/windows/latest)
|
||||
- Android latest: [download-vstalk.phy.kr/android/latest](https://download-vstalk.phy.kr/android/latest)
|
||||
- 버전 메타데이터: [download-vstalk.phy.kr/latest/version.json](https://download-vstalk.phy.kr/latest/version.json)
|
||||
- 제2 공개 레포: [physia.kr/open-source/projects/public/kotalk](https://physia.kr/open-source/projects/public/kotalk)
|
||||
- Forge releases: [git.physia.kr/ian/vs-messanger/releases](https://git.physia.kr/ian/vs-messanger/releases)
|
||||
- GitHub releases: [github.com/werther24601/kotalk/releases](https://github.com/werther24601/kotalk/releases)
|
||||
|
||||
## Current Note
|
||||
|
||||
2026-04-16 기준 [download-vstalk.phy.kr](https://download-vstalk.phy.kr)의 DNS/HTTPS 정합성은 재점검이 필요합니다.
|
||||
그래서 현재는 저장소 릴리즈 경로를 함께 유지하는 것을 원칙으로 둡니다.
|
||||
|
||||
## Minimum Release Contract
|
||||
|
||||
1. 실제로 실행 가능한 산출물이 있어야 합니다.
|
||||
2. [README.md](README.md)와 [PROJECT_STATUS.md](PROJECT_STATUS.md)가 같은 상태를 가리켜야 합니다.
|
||||
3. [CHANGELOG.md](CHANGELOG.md)에 의미 있는 변경이 기록돼야 합니다.
|
||||
4. 최신 스크린샷이 현재 UI를 대표해야 합니다.
|
||||
5. 다운로드 경로와 릴리즈 링크가 함께 갱신돼야 합니다.
|
||||
|
||||
## Platform Policy
|
||||
|
||||
- Windows: 빌드 산출물, 스크린샷, 체크섬을 함께 남깁니다.
|
||||
- Mobile web: 라이브 반영이 있으면 스크린샷과 상태 문서를 함께 갱신합니다.
|
||||
- Android: APK 공개 시 공식 미러와 저장소 릴리즈를 함께 맞춥니다.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- 배포 골격: [deploy/README.md](deploy/README.md)
|
||||
- 릴리즈 메타데이터: [release-assets/README.md](release-assets/README.md)
|
||||
- 상태표: [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
41
REPOSITORY_LAYOUT.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Repository Layout
|
||||
|
||||
이 저장소는 코드, 공개 보조 문서, 상세 기획 문서를 역할별로 나눠 관리합니다.
|
||||
|
||||
## Top Level
|
||||
|
||||
- `src/`: 제품 코드
|
||||
- `tests/`: 자동 테스트
|
||||
- `docs/`: 공개 저장소 보조 문서와 시각 자산
|
||||
- `문서/`: 제품 마스터 플랜과 UX 아틀라스
|
||||
- `deploy/`: 범용 배포 골격
|
||||
- `release-assets/`: 릴리즈 메타데이터와 스테이징 자산
|
||||
- `scripts/release/`: 릴리즈 준비와 게시 스크립트
|
||||
- `scripts/deploy/`: 서버와 웹앱 배포 스크립트
|
||||
- `scripts/ci/`: 캡처와 검증 보조 스크립트
|
||||
- `.workspace-*`: 워크스페이스 전용 비공개 정책과 시크릿
|
||||
|
||||
## Public Naming
|
||||
|
||||
- 제품 노출명: `KoTalk`
|
||||
- 한글 표기: `코톡`
|
||||
- 웹 진입점: `vstalk.phy.kr`
|
||||
- 다운로드 미러: `download-vstalk.phy.kr`
|
||||
|
||||
## Technical Note
|
||||
|
||||
현재 코드 네임스페이스와 프로젝트 파일은 여전히 `PhysOn.*`를 사용합니다.
|
||||
공개 브랜드와 소스 네이밍은 단계적으로 정렬합니다.
|
||||
|
||||
## Documentation Roles
|
||||
|
||||
- `docs/assets/`: README와 공개 표면에 직접 쓰는 이미지와 SVG
|
||||
- `docs/archive/`: 보관용 초안
|
||||
- `docs/repository-surfaces.md`: 공개 표면 규칙
|
||||
- `문서/`: 제품 전략, UX 기준, 실행 계획
|
||||
|
||||
## House Rules
|
||||
|
||||
- 공개 문서에는 비밀값, 운영 힌트, 내부 메모를 남기지 않습니다.
|
||||
- 공개 자산과 보관용 초안은 같은 폴더에 섞지 않습니다.
|
||||
- 공식 스크립트 경로는 `scripts/release`, `scripts/deploy`, `scripts/ci`입니다.
|
||||
94
ROADMAP.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Roadmap
|
||||
|
||||
## 현재 상태
|
||||
|
||||
- Alpha 가입, 대화 목록, 대화창, 텍스트 전송이 동작하는 첫 사용 가능 프로토타입 확보
|
||||
- Windows x64 portable build 생성 가능
|
||||
- `vstalk.phy.kr` 모바일 웹앱과 API를 VPS에 실제 배포
|
||||
- 원격 저장소에 최신 기준 스크린샷 포함 시작
|
||||
- `vstalk.phy.kr` 모바일 웹앱 MVP 빌드 및 same-origin API 검증 완료
|
||||
|
||||
## v0.1 Alpha
|
||||
|
||||
- [x] 초대코드 기반 가입
|
||||
- [x] 최근 대화 로드
|
||||
- [x] 메시지 전송
|
||||
- [x] 읽기 커서 갱신
|
||||
- [x] Windows portable zip 생성
|
||||
- [x] 모바일 웹앱 PWA 셸/가입/대화/전송
|
||||
- [x] VPS 공개 API 상시 구동
|
||||
- [x] `vstalk.phy.kr` same-origin 웹앱 배포
|
||||
- [ ] 데스크톱 WebSocket 실시간 반영
|
||||
|
||||
## v0.2 Collaboration Basics
|
||||
|
||||
- [ ] 파일 전송
|
||||
- [ ] 메시지 검색
|
||||
- [ ] 고정 / 읽지 않음 / 보관 UX
|
||||
- [ ] 알림/트레이 고도화
|
||||
- [ ] 공개 다운로드 채널 개통
|
||||
|
||||
## v0.2 Android First-class
|
||||
|
||||
- [ ] Android 셸/네비게이션 골격
|
||||
- [ ] Android 로그인/대화 목록/대화 진입 MVP
|
||||
- [ ] APK 서명 및 산출물 규칙 확정
|
||||
- [ ] Windows/Android 동시 릴리즈 메타데이터 검증
|
||||
- [ ] Forge Releases + VPS 미러 동시 게시
|
||||
|
||||
## v0.2 Mobile Web Entry
|
||||
|
||||
- [x] `vstalk.phy.kr` 모바일 웹 IA와 핵심 사용자 흐름 확정
|
||||
- [x] 링크 진입 화면과 초간단 가입/로그인 구조 정리
|
||||
- [x] 최근 대화 목록과 대화 화면 모바일 MVP 구현
|
||||
- [x] 업무/친근 소통 공존 규칙과 활동 화면 우선순위 문서화
|
||||
- [x] PWA 도입 전제와 비포함 범위 문서화
|
||||
- [x] 실제 VPS 배포 및 same-origin API 검증
|
||||
- [ ] 웹앱 전용 최신 스크린샷 갱신 자동화
|
||||
|
||||
## v0.3 Reliability
|
||||
|
||||
- [ ] 재연결 UX 고도화
|
||||
- [ ] 로컬 Draft/캐시 안정화
|
||||
- [ ] 자동 업데이트 전략 수립
|
||||
- [ ] 배포 자동화와 릴리즈 검증 강화
|
||||
- [ ] 최신 기준 스크린샷 갱신 자동 체크리스트 정착
|
||||
|
||||
## v1.0 Preview
|
||||
|
||||
- [ ] 다중 기기 세션 관리
|
||||
- [ ] Passkey 또는 더 강한 로그인 흐름
|
||||
- [ ] 업무/개인 맥락 전환 UX
|
||||
- [ ] 장기 보관과 검색 품질 개선
|
||||
|
||||
## 사업화와 조달 대응 방향
|
||||
|
||||
- [ ] `오픈소스 코어 / 공식 플랫폼 / 기관 대응 기능` 경계 문서화
|
||||
- [ ] 감사 로그, 관리자 정책, 조직 계정 모델의 최소 골격 확보
|
||||
- [ ] 셀프호스팅 배포 문서와 관리형 플랫폼 운영 문서 분리
|
||||
- [ ] 접근성, 릴리즈 증빙, 배포 선택권을 공공/기관 대응 기준으로 축적
|
||||
- [ ] 크라우드 펀딩용 공개 표면과 릴리즈/상태표 정합성 유지
|
||||
|
||||
## 보류 또는 실험 항목
|
||||
|
||||
- 음성/영상 통화
|
||||
- 공개 커뮤니티
|
||||
- 결제/송금
|
||||
- 피드형 콘텐츠
|
||||
- 과도한 자동 보조 기능 탑재
|
||||
|
||||
## 기여 우선순위
|
||||
|
||||
1. Android 최소 런칭 패스 구축
|
||||
2. 데스크톱 실시간 동기화
|
||||
3. 파일 전송과 검색
|
||||
4. Forge Releases와 다운로드 미러 정합성
|
||||
5. Android 첫 배포 이후 릴리즈 자동화
|
||||
|
||||
## 멀티 OS 릴리즈 운영 규칙
|
||||
|
||||
- 하나의 태그는 하나의 릴리즈 레코드를 뜻합니다.
|
||||
- 같은 버전 번호 아래에 Windows와 Android 자산을 함께 게시합니다.
|
||||
- 원격 저장소에는 최신 기준 스크린샷도 함께 포함합니다.
|
||||
- 다운로드 미러와 원격 Releases는 같은 자산 이름과 같은 노트를 기준으로 맞춥니다.
|
||||
- 모바일 웹은 설치형 산출물 대신 `https://vstalk.phy.kr`를 기준 진입점으로 관리합니다.
|
||||
40
SECURITY.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Security Policy
|
||||
|
||||
KoTalk는 알파 단계의 오픈소스 프로젝트입니다.
|
||||
완전무결함을 약속하지는 않지만, 공개 문서에서는 현재 통제와 남은 과제를 구분해 설명하는 방식을 유지합니다.
|
||||
|
||||
## Current Principles
|
||||
|
||||
- 기본 비밀값과 임시 접근값은 공개 문서에 실어두지 않습니다.
|
||||
- 메시지 본문과 민감정보를 로그 기본값으로 남기지 않습니다.
|
||||
- 세션과 인증 경로는 짧은 수명, 회전, 원격 폐기를 전제로 설계합니다.
|
||||
- 공식 릴리즈 경로와 무결성 정보는 함께 제공합니다.
|
||||
|
||||
## What This Policy Covers
|
||||
|
||||
- 취약점 제보 경로
|
||||
- 현재 적용 중인 기본 통제
|
||||
- 아직 남아 있는 보강 과제
|
||||
|
||||
자세한 신뢰 표면은 [TRUST_CENTER.md](TRUST_CENTER.md), 제보 처리 방식은 [SECURITY_RESPONSE.md](SECURITY_RESPONSE.md)에서 확인할 수 있습니다.
|
||||
|
||||
## Reporting A Vulnerability
|
||||
|
||||
보안 이슈는 공개 이슈 대신 아래 경로로 먼저 알려 주세요.
|
||||
|
||||
- [ian@physia.kr](mailto:ian@physia.kr)
|
||||
- [contact@physia.kr](mailto:contact@physia.kr)
|
||||
|
||||
가능하면 아래 정보를 포함해 주세요.
|
||||
|
||||
- 영향 범위
|
||||
- 재현 절차
|
||||
- 예상 시나리오
|
||||
- 임시 완화책
|
||||
|
||||
## Current Areas Still In Progress
|
||||
|
||||
- 브라우저 세션 저장 경계 강화
|
||||
- 취약점 advisory 공개 체계
|
||||
- 키 회전, 백업/복구, 공급망 보안 증빙
|
||||
- 규제 환경용 설치/운영 검증
|
||||
26
SECURITY_RESPONSE.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Security Response
|
||||
|
||||
이 문서는 보안 이슈 제보를 받았을 때의 기본 대응 방식을 설명합니다.
|
||||
|
||||
## Contact
|
||||
|
||||
- 보안 제보: [ian@physia.kr](mailto:ian@physia.kr)
|
||||
- 운영 연계: [contact@physia.kr](mailto:contact@physia.kr)
|
||||
|
||||
공개 이슈에는 취약점 세부 내용을 남기지 않는 것을 권장합니다.
|
||||
|
||||
## Response Targets
|
||||
|
||||
| Step | Target |
|
||||
|---|---|
|
||||
| Receipt acknowledgement | 영업일 기준 3일 이내 |
|
||||
| Initial triage | 영업일 기준 5일 이내 |
|
||||
| Severity update | 재현 여부 확인 후 가능한 한 빠르게 |
|
||||
| Fix or mitigation guidance | 심각도와 재현 범위에 따라 별도 판단 |
|
||||
|
||||
## Severity Guide
|
||||
|
||||
- `Critical`: 계정 탈취, 원격 실행, 대규모 데이터 노출
|
||||
- `High`: 인증 우회, 장기 세션 탈취, 권한 상승, 릴리즈 변조
|
||||
- `Medium`: 제한적 정보 노출, 우회 가능한 보호장치
|
||||
- `Low`: 설명 부족, 완화 가능한 설정 실수
|
||||
77
SHOWCASE.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Showcase
|
||||
|
||||
KoTalk의 현재 공개 표면을 짧게 훑어보는 문서가 아니라, 지금 저장소에서 실제로 확인할 수 있는 화면과 동작 범위를 빠르게 따라가는 문서입니다.
|
||||
|
||||
## Live Surfaces
|
||||
|
||||
| Surface | Link | What it shows |
|
||||
|---|---|---|
|
||||
| Mobile web | [vstalk.phy.kr](https://vstalk.phy.kr) | 실제 공개 중인 모바일 웹 흐름 |
|
||||
| Official download mirror | [download-vstalk.phy.kr](https://download-vstalk.phy.kr) | 공식 다운로드 미러 경로 |
|
||||
| Public stage repo | [physia.kr/open-source/projects/public/kotalk](https://physia.kr/open-source/projects/public/kotalk) | 제2 공개 레포 |
|
||||
| Forge releases | [git.physia.kr/ian/vs-messanger/releases](https://git.physia.kr/ian/vs-messanger/releases) | 내부 기준 릴리즈 채널 |
|
||||
| GitHub releases | [github.com/werther24601/kotalk/releases](https://github.com/werther24601/kotalk/releases) | 공개 릴리즈 채널 |
|
||||
|
||||
## Desktop Walkthrough
|
||||
|
||||
데스크톱은 화려한 장식보다 `레일 + 목록 + 대화`에 집중합니다. 넓은 화면을 “정보를 더 많이 보여 주는 곳”이 아니라 “답장과 전환을 덜 피곤하게 만드는 곳”으로 해석하는 쪽에 가깝습니다.
|
||||
|
||||
### What To Notice
|
||||
|
||||
- 좌측 레일은 목적지 전환을 맡고, 설명 텍스트는 최소화합니다.
|
||||
- 가운데 목록은 최근성, 읽지 않음, 고정 흐름을 조밀하게 처리합니다.
|
||||
- 오른쪽 대화 패널은 입력과 복귀 흐름이 끊기지 않게 밀도를 높입니다.
|
||||
- 멀티 윈도우 전제 설계라서 대화를 분리해 보는 흐름을 계속 강화하고 있습니다.
|
||||
|
||||
### Desktop Screens
|
||||
|
||||
| Screen | Why it matters |
|
||||
|---|---|
|
||||
| [hero-shell.png](docs/assets/latest/hero-shell.png) | 현재 데스크톱 전체 셸의 구조와 밀도 |
|
||||
| [onboarding.png](docs/assets/latest/onboarding.png) | 첫 진입에서 어떤 정보를 먼저 보여주는지 |
|
||||
| [conversation.png](docs/assets/latest/conversation.png) | 실제 대화 화면의 읽기 흐름과 입력 밀도 |
|
||||
|
||||
## Mobile Web Walkthrough
|
||||
|
||||
모바일 웹은 빠른 진입과 짧은 복귀가 핵심입니다. 설명을 길게 읽게 하기보다, 대화 목록과 검색, 보관, 다시 열기 흐름을 짧은 탭 구조로 분리합니다.
|
||||
|
||||
### What To Notice
|
||||
|
||||
- 온보딩은 길고 복잡한 가입보다 빠른 진입을 우선합니다.
|
||||
- 목록은 최근 대화와 필터, 검색 진입을 한 화면에서 해결합니다.
|
||||
- 검색은 단순 텍스트 필터가 아니라 다시 찾아야 하는 대화를 더 빨리 여는 방향으로 확장 중입니다.
|
||||
- 보관 화면은 “나중에 다시 답장해야 할 것”을 모아 보는 허브 역할을 맡습니다.
|
||||
|
||||
### Mobile Web Screens
|
||||
|
||||
| Screen | Why it matters |
|
||||
|---|---|
|
||||
| [vstalk-web-onboarding.png](docs/assets/latest/vstalk-web-onboarding.png) | 초기 진입과 가입 흐름 |
|
||||
| [vstalk-web-list.png](docs/assets/latest/vstalk-web-list.png) | 현재 받은함 구조 |
|
||||
| [vstalk-web-search.png](docs/assets/latest/vstalk-web-search.png) | 검색과 재발견 흐름 |
|
||||
| [vstalk-web-saved.png](docs/assets/latest/vstalk-web-saved.png) | 보관과 후속조치 허브 |
|
||||
| [vstalk-web-chat.png](docs/assets/latest/vstalk-web-chat.png) | 모바일 대화 화면의 현재 밀도 |
|
||||
|
||||
## Build And Artifact Shelf
|
||||
|
||||
저장소 안에는 화면만 있는 것이 아니라, 현재 기준의 빌드 산출물과 최신 스크린샷도 함께 남깁니다.
|
||||
|
||||
- 릴리즈 정책: [RELEASING.md](RELEASING.md)
|
||||
- 현재 상태 요약: [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
- 최신 화면 자산: [docs/assets/latest/README.md](docs/assets/latest/README.md)
|
||||
- 제품 방향 전체: [문서/README.md](문서/README.md)
|
||||
|
||||
## What This Showcase Is Trying To Prove
|
||||
|
||||
이 문서는 “멋있어 보이는 이미지 모음”보다 아래 세 가지를 보여주려 합니다.
|
||||
|
||||
- KoTalk가 단순한 기획 문서 저장소가 아니라 실제 화면과 빌드 결과를 가진 프로젝트라는 점
|
||||
- 한국어 데스크톱 메시징 경험을 다시 다듬는다는 방향이 UI와 정보 구조에 실제 반영되고 있다는 점
|
||||
- 현재 부족한 부분도 숨기지 않고, 상태 문서와 다음 작업 우선순위가 함께 공개돼 있다는 점
|
||||
|
||||
## Read Next
|
||||
|
||||
- 현재 상태: [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
- 배경과 문제의식: [BACKGROUND.md](BACKGROUND.md)
|
||||
- 공개 규칙: [RELEASING.md](RELEASING.md)
|
||||
- 제품 기획: [문서/README.md](문서/README.md)
|
||||
33
SUPPORT.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Support
|
||||
|
||||
## Contact Channels
|
||||
|
||||
- 일반 도움: [help@physia.kr](mailto:help@physia.kr)
|
||||
- 제휴 및 운영 문의: [contact@physia.kr](mailto:contact@physia.kr)
|
||||
- 보안 이슈: [ian@physia.kr](mailto:ian@physia.kr)
|
||||
|
||||
## Best Way To Report A Problem
|
||||
|
||||
가능하면 공개 이슈로 남겨 주세요. 아래 정보가 있으면 처리 속도가 높아집니다.
|
||||
|
||||
- 운영체제와 버전
|
||||
- 실행 방법
|
||||
- 재현 절차
|
||||
- 기대 결과와 실제 결과
|
||||
- 스크린샷이나 로그
|
||||
|
||||
## Priority Areas
|
||||
|
||||
- 메시지 전송 실패 또는 중복 전송
|
||||
- 세션 복구 문제
|
||||
- 공개 릴리즈 파일 무결성 이슈
|
||||
- [vstalk.phy.kr](https://vstalk.phy.kr) 또는 다운로드 경로 장애
|
||||
|
||||
## Before You Send A Message
|
||||
|
||||
먼저 아래 문서를 보면 빠르게 답을 찾을 수 있습니다.
|
||||
|
||||
- [FAQ.md](FAQ.md)
|
||||
- [PROJECT_STATUS.md](PROJECT_STATUS.md)
|
||||
- [RELEASING.md](RELEASING.md)
|
||||
- [COMMUNITY.md](COMMUNITY.md)
|
||||
26
TRADEMARKS.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Trademarks
|
||||
|
||||
코드 라이선스와 상표 사용 권한은 다릅니다.
|
||||
|
||||
## Marks
|
||||
|
||||
아래 명칭과 관련 로고는 PHYSIA의 브랜드 자산으로 취급합니다.
|
||||
|
||||
- `KoTalk`
|
||||
- `코톡`
|
||||
- `kotalk`
|
||||
- `ko-talk`
|
||||
- `PHYSIA`
|
||||
|
||||
## What The Open-Source License Does Not Grant
|
||||
|
||||
- 공식 서비스로 오인되는 브랜딩
|
||||
- 동일하거나 혼동 가능한 제품명 사용
|
||||
- PHYSIA의 공식 후원이나 인증을 암시하는 표시
|
||||
|
||||
## Forks And Self-Hosted Deployments
|
||||
|
||||
코드 포크와 자체 호스팅은 라이선스 범위 안에서 가능합니다.
|
||||
다만 공식 서비스와 혼동될 수 있는 브랜드 사용은 허용되지 않습니다.
|
||||
|
||||
문의: [contact@physia.kr](mailto:contact@physia.kr)
|
||||
27
TRUST_CENTER.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Trust Center
|
||||
|
||||
KoTalk의 신뢰 표면은 과장된 보안 문구보다 현재 통제와 남은 과제를 함께 적는 방식으로 관리합니다.
|
||||
|
||||
## Current Trust Baseline
|
||||
|
||||
| Area | Current read |
|
||||
|---|---|
|
||||
| Authentication | 기본 토큰/세션 구조와 만료·회전 정책을 사용 |
|
||||
| Realtime | 실시간 연결은 별도 보호 경계를 둠 |
|
||||
| Storage | 민감값 장기 저장 최소화 방향 |
|
||||
| Release integrity | 릴리즈 경로와 스크린샷, CHANGELOG를 함께 맞춤 |
|
||||
| Public posture | 적용 범위와 미완료 범위를 분리해 설명 |
|
||||
|
||||
## What Is Still In Progress
|
||||
|
||||
- 브라우저 세션 경계 강화
|
||||
- advisory 공개 체계
|
||||
- 키 회전과 백업/복구 증빙
|
||||
- 규제 환경 설치 검증
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [SECURITY.md](SECURITY.md)
|
||||
- [SECURITY_RESPONSE.md](SECURITY_RESPONSE.md)
|
||||
- [PRIVACY_AND_DATA_HANDLING.md](PRIVACY_AND_DATA_HANDLING.md)
|
||||
- [DEPLOYMENT_MODES.md](DEPLOYMENT_MODES.md)
|
||||
11
deploy/.env.example
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
COMPOSE_PROJECT_NAME=kotalk
|
||||
ACME_EMAIL=admin@example.com
|
||||
API_HOST=api.example.com
|
||||
DOWNLOAD_HOST=download-vstalk.phy.kr
|
||||
DOWNLOAD_ROOT=/srv/kotalk/download
|
||||
WEBAPP_HOST=vstalk.phy.kr
|
||||
WEBAPP_RELEASE_ROOT=/srv/kotalk/webapp/current
|
||||
BOOTSTRAP_INVITE_CODE=change-me-in-private-env
|
||||
JWT_ISSUER=KoTalk
|
||||
JWT_AUDIENCE=KoTalk.Client
|
||||
JWT_SIGNING_KEY=change-me-to-a-long-random-value
|
||||
49
deploy/Caddyfile
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$DOWNLOAD_HOST} {
|
||||
encode zstd gzip
|
||||
root * /srv/download
|
||||
redir /windows /windows/latest 302
|
||||
redir /android /android/latest 302
|
||||
redir /windows/latest /windows/latest/VsMessenger-win-x64.zip 302
|
||||
redir /android/latest /android/latest/VsMessenger-android-universal.apk 302
|
||||
redir /latest /latest/version.json 302
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
file_server
|
||||
}
|
||||
|
||||
{$WEBAPP_HOST:vstalk.phy.kr} {
|
||||
encode zstd gzip
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
handle /v1/* {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
handle /health {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
handle {
|
||||
reverse_proxy webapp:80
|
||||
}
|
||||
}
|
||||
|
||||
{$API_HOST} {
|
||||
encode zstd gzip
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
29
deploy/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Deployment Guide
|
||||
|
||||
이 디렉터리는 KoTalk의 범용 배포 골격을 담습니다.
|
||||
|
||||
## Public Endpoints
|
||||
|
||||
- 모바일 웹: [vstalk.phy.kr](https://vstalk.phy.kr)
|
||||
- 공식 다운로드 미러: [download-vstalk.phy.kr](https://download-vstalk.phy.kr)
|
||||
- 버전 메타데이터: [download-vstalk.phy.kr/latest/version.json](https://download-vstalk.phy.kr/latest/version.json)
|
||||
|
||||
## Intended Shape
|
||||
|
||||
- `Caddyfile`: 웹 진입점, 다운로드 미러, API 프록시
|
||||
- `compose*.yml`: API, 정적 웹, 보조 서비스 구성
|
||||
- `docker/`: 이미지 빌드 정의
|
||||
|
||||
## Public Rules
|
||||
|
||||
- 실서비스 호스트 주소, 관리자 계정, 비밀값은 공개 문서에 적지 않습니다.
|
||||
- 운영 중인 컨테이너명과 네트워크명은 공개 표면의 필수 정보가 아닙니다.
|
||||
- 배포 예시는 범용 구조 중심으로 유지합니다.
|
||||
|
||||
## Download Layout
|
||||
|
||||
- `/windows/latest`
|
||||
- `/android/latest`
|
||||
- `/latest/version.json`
|
||||
|
||||
실제 공개 릴리즈 경로는 [RELEASING.md](../RELEASING.md)와 함께 봐야 합니다.
|
||||
42
deploy/compose.mvp.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
services:
|
||||
caddy:
|
||||
image: caddy:2.9-alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
ACME_EMAIL: ${ACME_EMAIL}
|
||||
API_HOST: ${API_HOST}
|
||||
DOWNLOAD_HOST: ${DOWNLOAD_HOST}
|
||||
WEBAPP_HOST: ${WEBAPP_HOST:-vstalk.phy.kr}
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
- ${DOWNLOAD_ROOT}:/srv/download:ro
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/docker/api.Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://0.0.0.0:8080
|
||||
ConnectionStrings__Main: Data Source=/data/vs-messenger.db
|
||||
Auth__Jwt__Issuer: ${JWT_ISSUER}
|
||||
Auth__Jwt__Audience: ${JWT_AUDIENCE}
|
||||
Auth__Jwt__SigningKey: ${JWT_SIGNING_KEY}
|
||||
Bootstrap__InviteCodes__0: ${BOOTSTRAP_INVITE_CODE}
|
||||
volumes:
|
||||
- api-data:/data
|
||||
expose:
|
||||
- "8080"
|
||||
|
||||
volumes:
|
||||
caddy-config:
|
||||
caddy-data:
|
||||
api-data:
|
||||
9
deploy/compose.webapp.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
services:
|
||||
webapp:
|
||||
image: nginx:1.27-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${WEBAPP_RELEASE_ROOT:-/srv/vs-messanger/webapp/current}:/usr/share/nginx/html:ro
|
||||
- ./docker/webapp.nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
expose:
|
||||
- "80"
|
||||
25
deploy/docker/api.Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["PhysOn.sln", "./"]
|
||||
COPY ["global.json", "./"]
|
||||
COPY ["src/PhysOn.Api/PhysOn.Api.csproj", "src/PhysOn.Api/"]
|
||||
COPY ["src/PhysOn.Application/PhysOn.Application.csproj", "src/PhysOn.Application/"]
|
||||
COPY ["src/PhysOn.Contracts/PhysOn.Contracts.csproj", "src/PhysOn.Contracts/"]
|
||||
COPY ["src/PhysOn.Domain/PhysOn.Domain.csproj", "src/PhysOn.Domain/"]
|
||||
COPY ["src/PhysOn.Infrastructure/PhysOn.Infrastructure.csproj", "src/PhysOn.Infrastructure/"]
|
||||
|
||||
RUN dotnet restore "src/PhysOn.Api/PhysOn.Api.csproj"
|
||||
|
||||
COPY . .
|
||||
RUN dotnet publish "src/PhysOn.Api/PhysOn.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
EXPOSE 8080
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
ENTRYPOINT ["dotnet", "PhysOn.Api.dll"]
|
||||
23
deploy/docker/webapp.nginx.conf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
22
deploy/docker/worker.Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["PhysOn.sln", "./"]
|
||||
COPY ["global.json", "./"]
|
||||
COPY ["src/PhysOn.Worker/PhysOn.Worker.csproj", "src/PhysOn.Worker/"]
|
||||
COPY ["src/PhysOn.Application/PhysOn.Application.csproj", "src/PhysOn.Application/"]
|
||||
COPY ["src/PhysOn.Contracts/PhysOn.Contracts.csproj", "src/PhysOn.Contracts/"]
|
||||
COPY ["src/PhysOn.Domain/PhysOn.Domain.csproj", "src/PhysOn.Domain/"]
|
||||
COPY ["src/PhysOn.Infrastructure/PhysOn.Infrastructure.csproj", "src/PhysOn.Infrastructure/"]
|
||||
|
||||
RUN dotnet restore "src/PhysOn.Worker/PhysOn.Worker.csproj"
|
||||
|
||||
COPY . .
|
||||
RUN dotnet publish "src/PhysOn.Worker/PhysOn.Worker.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
ENTRYPOINT ["dotnet", "PhysOn.Worker.dll"]
|
||||
16
deploy/systemd/vs-messanger-mvp.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=vs-messanger MVP docker compose stack
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/srv/vs-messanger/app
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/docker compose --project-name vs-messanger --env-file deploy/.env -f deploy/compose.mvp.yml up -d --build --remove-orphans
|
||||
ExecStop=/usr/bin/docker compose --project-name vs-messanger --env-file deploy/.env -f deploy/compose.mvp.yml down
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
16
deploy/systemd/vs-messanger-webapp.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=vs-messanger mobile webapp compose stack
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/srv/vs-messanger/app
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/docker compose --project-name vs-messanger --env-file deploy/.env -f deploy/compose.mvp.yml -f deploy/compose.webapp.yml up -d webapp caddy
|
||||
ExecStop=/usr/bin/docker compose --project-name vs-messanger --env-file deploy/.env -f deploy/compose.mvp.yml -f deploy/compose.webapp.yml stop webapp
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
11
docs/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Docs Index
|
||||
|
||||
`docs/`는 공개 저장소 표면을 보조하는 문서 루트입니다. 제품 마스터 플랜 자체는 `문서/`를 기준으로 유지하고, `docs/`는 공개 자산, 저장소 운영 규칙, 보관용 초안을 맡습니다.
|
||||
|
||||
## Included Areas
|
||||
|
||||
- [assets/](assets/): README, Showcase, 최신 스크린샷, 공개 시각 자산
|
||||
- [archive/](archive/): 초기 기획 초안과 구조 탐색 문서
|
||||
- [repository-surfaces.md](repository-surfaces.md): 내부 기준선, 공개 큐레이션 브랜치, 비공개 워크스페이스 규칙
|
||||
|
||||
현재 기준의 최종 기획 본문과 UX 아틀라스는 [../문서/README.md](../문서/README.md)를 우선합니다.
|
||||
217
docs/archive/01-product-strategy-definition.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# 01. 제품 전략 및 정의
|
||||
|
||||
## 합의 관점
|
||||
|
||||
이 문서는 아래 6개 전문 관점의 공통 합의안으로 정리한다.
|
||||
|
||||
- 제품 전략: 출시 가능성과 포지셔닝을 우선한다.
|
||||
- UX 설계: 익숙함은 유지하되 더 정돈된 정보 밀도와 반응성을 만든다.
|
||||
- Windows 데스크톱: 키보드 중심 흐름과 멀티패널 효율을 핵심으로 본다.
|
||||
- 채팅 플랫폼: 실시간성보다 "안정적 전달, 빠른 재진입, 파일 전송 신뢰성"을 우선한다.
|
||||
- 브랜드/법무: 카카오톡과 혼동될 수 있는 요소는 배제한다.
|
||||
- 수익/성장: 소규모 개인 프로젝트로도 운영 가능한 구조를 택한다.
|
||||
|
||||
## 제품 비전
|
||||
|
||||
- 목표는 "Windows에서 가장 빠르고 정돈된 개인용 메신저"를 만드는 것이다.
|
||||
- 출발점은 카카오톡 PC의 익숙한 사용 패턴이지만, 최종 제품은 더 세련된 데스크톱 경험과 개인 생산성 보조 기능을 갖춘 독립 서비스여야 한다.
|
||||
- 핵심 가치 제안:
|
||||
- 빠른 대화 접근: 실행, 로그인, 방 전환, 검색이 가볍고 즉각적이어야 한다.
|
||||
- 높은 작업 효율: 키보드 단축키, 멀티패널, 파일/링크/이미지 관리가 우수해야 한다.
|
||||
- 개인 사이드 프로젝트다운 완성도: 과한 범용 플랫폼이 아니라 "한 명이 끝까지 유지 가능한 범위"에서 품질을 높인다.
|
||||
|
||||
## 타깃 사용자
|
||||
|
||||
- 1차 타깃: Windows PC를 오래 켜두고 일하거나 공부하는 20~40대 개인 사용자
|
||||
- 1차 타깃 특성:
|
||||
- 모바일보다 PC에서 대화와 파일 공유를 더 많이 처리한다.
|
||||
- 오픈채팅/커뮤니티보다 소수 관계, 개인 네트워크, 협업성 대화에 가치를 둔다.
|
||||
- 메신저 자체보다 "업무/일상 컨텍스트 전환 비용"을 줄이고 싶어 한다.
|
||||
- 2차 타깃: 개인 프로젝트 팀, 지인 소모임, 스터디 그룹, 소규모 프리랜서 협업 그룹
|
||||
|
||||
## 핵심 JTBD
|
||||
|
||||
- "PC에서 일하는 동안, 휴대폰을 들지 않고도 대화를 빠르게 처리하고 싶다."
|
||||
- "자주 대화하는 사람, 중요한 파일, 링크, 공지를 메신저 안에서 쉽게 다시 찾고 싶다."
|
||||
- "메신저가 가볍고 안정적이어서 계속 켜 두어도 부담이 없었으면 좋겠다."
|
||||
- "읽음 상태, 미확인 메시지, 첨부파일, 핀 고정 정보를 한눈에 관리하고 싶다."
|
||||
- "사적인 메신저라도 디자인이 투박하지 않고, 데스크톱 앱다운 완성도가 있었으면 좋겠다."
|
||||
|
||||
## MVP 범위
|
||||
|
||||
MVP는 "사람들이 실제로 매일 켜 둘 수 있는 1:1 및 소규모 그룹 메신저"에 집중한다.
|
||||
|
||||
- 필수 계정 기능:
|
||||
- 이메일 또는 휴대폰 기반 가입/로그인
|
||||
- Windows 디바이스 기준 세션 유지
|
||||
- 프로필 이미지, 상태 메시지, 표시 이름
|
||||
- 필수 대화 기능:
|
||||
- 1:1 채팅
|
||||
- 그룹 채팅
|
||||
- 텍스트, 이모지, 이미지, 파일 전송
|
||||
- 읽음 상태, 전송 실패 재시도, 날짜 구분선, 미확인 배지
|
||||
- 대화방 고정, 알림 음소거, 검색
|
||||
- 필수 데스크톱 UX:
|
||||
- 좌측 채팅 리스트 + 중앙 대화 패널 + 선택형 우측 정보 패널
|
||||
- 글로벌 검색
|
||||
- 드래그 앤 드롭 파일 전송
|
||||
- 단축키 중심 이동
|
||||
- 시스템 트레이 상주, 알림센터 연동
|
||||
- 필수 운영 기능:
|
||||
- 신고/차단
|
||||
- 관리자용 기본 운영 콘솔
|
||||
- 장애/로그 모니터링
|
||||
|
||||
## MVP에서 제외할 것
|
||||
|
||||
- 음성/영상 통화
|
||||
- 공개 커뮤니티, 대형 오픈채팅
|
||||
- 스토리, 피드, 쇼츠형 콘텐츠
|
||||
- 결제, 송금, 선물하기
|
||||
- AI 비서 전면 탑재
|
||||
- 과한 커스터마이징 테마 마켓
|
||||
- 멀티플랫폼 동시 최적화
|
||||
|
||||
제외 이유:
|
||||
- 개인 사이드 프로젝트가 첫 출시까지 가는 데 가장 큰 장애물은 범위 과잉이다.
|
||||
- 통화/결제/콘텐츠 기능은 법률, 인프라, 운영 부담이 급증한다.
|
||||
|
||||
## 기능 우선순위
|
||||
|
||||
### P0
|
||||
|
||||
- 가입/로그인
|
||||
- 친구 또는 사용자 검색
|
||||
- 1:1 채팅
|
||||
- 그룹 채팅
|
||||
- 메시지 영속 저장
|
||||
- 이미지/파일 전송
|
||||
- 읽음 상태
|
||||
- 푸시/데스크톱 알림
|
||||
- 채팅 리스트 정렬과 미확인 배지
|
||||
- 대화 검색
|
||||
|
||||
### P1
|
||||
|
||||
- 답장, 전달, 메시지 고정
|
||||
- 링크/파일/미디어 모아보기
|
||||
- 멀티 디바이스 로그인 관리
|
||||
- 차단/신고
|
||||
- 관리 도구와 운영 로그
|
||||
|
||||
### P2
|
||||
|
||||
- 예약 전송
|
||||
- 나에게 보내기
|
||||
- 읽지 않은 대화 일괄 정리
|
||||
- 생산성형 미니 기능: 체크리스트, 빠른 메모, 링크 컬렉션
|
||||
|
||||
## 단계별 출시 플랜
|
||||
|
||||
### Phase 0. 정의 및 검증
|
||||
|
||||
- 기준 제품 경험을 분해한다.
|
||||
- "왜 카카오톡 PC가 편한가"를 기능 복제가 아니라 흐름 단위로 정리한다.
|
||||
- 5~10명의 잠재 사용자 인터뷰로 Windows 메신저 사용 패턴을 검증한다.
|
||||
- 브랜드 방향, 시각 언어, 법적 가드레일을 먼저 확정한다.
|
||||
|
||||
### Phase 1. Private Alpha
|
||||
|
||||
- 본인 + 지인 소수 그룹이 실제로 쓰는 수준까지 구현한다.
|
||||
- 안정성과 기본 채팅 품질에만 집중한다.
|
||||
- 서버는 본 VPS에 단일 리전으로 구축하되, 백업과 로그는 반드시 분리한다.
|
||||
- 성공 기준은 "매일 열어두는가"와 "메시지 손실이 없는가"다.
|
||||
|
||||
### Phase 2. Closed Beta
|
||||
|
||||
- 50~200명 수준으로 확장한다.
|
||||
- 검색, 파일, 알림, 운영 도구를 강화한다.
|
||||
- 설치/업데이트 경험과 계정 복구 흐름을 다듬는다.
|
||||
- 이 시점부터 지표 기반 개선 루프를 시작한다.
|
||||
|
||||
### Phase 3. Public Launch
|
||||
|
||||
- 브랜딩과 온보딩을 정식화한다.
|
||||
- 유료 옵션이 있다면 이 단계에서만 소프트 런칭한다.
|
||||
- Windows 앱 완성도를 우선 홍보 포인트로 삼는다.
|
||||
|
||||
## 차별화 방향
|
||||
|
||||
- "카카오톡 대체재"보다 "Windows에서 더 잘 맞는 메신저"로 포지셔닝한다.
|
||||
- 차별화 포인트:
|
||||
- 정돈된 데스크톱 레이아웃과 높은 정보 밀도
|
||||
- 키보드 우선 UX와 빠른 탐색
|
||||
- 링크/파일/이미지 재발견 경험 강화
|
||||
- 가볍고 신뢰 가능한 앱 성능
|
||||
- 개인 사용자와 소규모 그룹에 맞는 프라이빗한 톤
|
||||
|
||||
- 피해야 할 차별화:
|
||||
- 기능 수를 늘려 거대 플랫폼처럼 보이게 하는 것
|
||||
- AI, 커뮤니티, 콘텐츠 허브를 한 번에 붙이는 것
|
||||
|
||||
## 수익화 옵션
|
||||
|
||||
- 기본 원칙:
|
||||
- 초기에는 무료가 맞다.
|
||||
- 수익화는 제품이 일상 사용 습관에 들어온 뒤 붙여야 한다.
|
||||
|
||||
- 가능한 옵션:
|
||||
- Pro 개인 요금제
|
||||
- 대용량 파일 업로드
|
||||
- 긴 메시지/미디어 보관 기간
|
||||
- 향상된 검색과 기록 내보내기
|
||||
- 테마/레이아웃 커스터마이징
|
||||
- 소규모 팀 요금제
|
||||
- 관리자 기능
|
||||
- 공유 드라이브형 파일 관리
|
||||
- 간단한 공지/권한 관리
|
||||
|
||||
- 추천 방향:
|
||||
- 첫 유료 기능은 광고가 아니라 "보관/검색/파일 한도" 기반이 가장 현실적이다.
|
||||
|
||||
## 브랜드 및 법적 주의사항
|
||||
|
||||
- 카카오톡의 이름, 색상 조합, 아이콘 메타포, 말풍선 스타일, 사운드, 카피 문구를 그대로 차용하지 않는다.
|
||||
- "카카오톡 PC 클론"처럼 보이는 표현은 내부 참고용으로만 쓰고 외부 문서나 마케팅에는 쓰지 않는다.
|
||||
- 프로토콜 역공학, 기존 네트워크에 붙는 브리지, 상표 혼동 가능성이 있는 UX 복제는 피한다.
|
||||
- 참고 대상은 "사용 흐름"과 "데스크톱 생산성 원칙"이어야 하며, 결과물은 별도 브랜드 아이덴티티를 가져야 한다.
|
||||
- 출시 전 최소 점검 항목:
|
||||
- 서비스명 상표 검색
|
||||
- 앱 아이콘과 UI 주요 요소의 혼동 가능성 리뷰
|
||||
- 이용약관/개인정보처리방침 초안
|
||||
|
||||
## 성공 지표
|
||||
|
||||
### 제품 핵심 지표
|
||||
|
||||
- 주간 활성 사용자 비율
|
||||
- 7일, 30일 리텐션
|
||||
- DAU/WAU
|
||||
- 1인당 일평균 세션 수
|
||||
- 1인당 일평균 메시지 송수신량
|
||||
|
||||
### 경험 품질 지표
|
||||
|
||||
- 앱 실행 후 첫 화면 표시 시간
|
||||
- 메시지 전송 성공률
|
||||
- 이미지/파일 업로드 성공률
|
||||
- 알림 수신 성공률
|
||||
- 검색 성공률 또는 검색 후 클릭률
|
||||
|
||||
### 사업성 보조 지표
|
||||
|
||||
- 초대 전환율
|
||||
- 그룹 생성 비율
|
||||
- 1명 이상의 재초대 발생 비율
|
||||
- 유료 기능 대기자 등록률
|
||||
|
||||
## 최종 권고
|
||||
|
||||
- 이 프로젝트는 "카카오톡과 비슷한 것"을 목표로 두면 늦어진다.
|
||||
- 더 좋은 방향은 "Windows에서 쓰기 가장 좋은 개인 메신저"를 만드는 것이다.
|
||||
- 첫 출시의 기준:
|
||||
- 매일 켜 두고 쓸 만큼 빠를 것
|
||||
- 메시지와 파일이 불안하지 않을 것
|
||||
- 기존 대형 메신저보다 데스크톱 경험이 더 정돈되어 있을 것
|
||||
|
||||
이 기준을 지키면 개인 사이드 프로젝트로도 실제 출시 가능한 수준까지 갈 수 있다.
|
||||
9
docs/archive/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Archive Docs
|
||||
|
||||
이 디렉터리는 초기에 작성된 기획 초안과 구조 탐색 문서를 보관합니다.
|
||||
|
||||
원칙:
|
||||
|
||||
- 현재 기준의 최종 기획 본문은 `문서/`를 우선합니다.
|
||||
- 여기 있는 문서는 참고용 아카이브이며, 현재 제품 기준과 다를 수 있습니다.
|
||||
- 공개 브랜치에서는 이 경로를 포함하지 않거나 축약할 수 있습니다.
|
||||
515
docs/archive/planning/01-backend-platform-architecture.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# 개인 메신저 프로젝트 백엔드/플랫폼 아키텍처 합의안
|
||||
|
||||
이 문서는 기존 Rocky Linux VPS 위에 Docker로 채팅 서버를 구축하는 것을 전제로 한 구현 지향형 백엔드/플랫폼 설계안이다. 목표는 카카오톡 PC 버전 수준의 빠른 체감 응답성과 안정적인 실시간 동기화 경험을 내는 것이다.
|
||||
|
||||
합의에 참여한 전문 관점:
|
||||
|
||||
- 리얼타임 백엔드 아키텍트
|
||||
- 플랫폼 엔지니어
|
||||
- PostgreSQL 데이터 엔지니어
|
||||
- SRE/관측성 엔지니어
|
||||
- 보안 엔지니어
|
||||
- 스토리지/백업 엔지니어
|
||||
|
||||
## 1. 전제와 설계 원칙
|
||||
|
||||
전제:
|
||||
|
||||
- 서버는 기존 Rocky Linux VPS에 배포한다.
|
||||
- Docker 사용 가능하다.
|
||||
- 초기 개발 대상은 Windows PC 클라이언트다.
|
||||
- 초기는 개인 사이드 프로젝트지만, 구조는 다중 디바이스와 사용자 증가를 막지 않아야 한다.
|
||||
|
||||
설계 원칙:
|
||||
|
||||
- 초기에는 단일 VPS에 맞는 단순성을 우선한다.
|
||||
- 그러나 프로토콜, 데이터 모델, 메시지 ID, 스토리지 인터페이스는 이후 수평 확장을 막지 않게 설계한다.
|
||||
- 채팅 메시지의 원본 저장소는 처음부터 PostgreSQL로 통일한다.
|
||||
- 실시간 연결과 영속 저장을 분리해 생각한다.
|
||||
- MinIO는 첨부파일 저장소로는 괜찮지만 백업 저장소로 간주하지 않는다.
|
||||
- 운영 편의보다 복구 가능성을 우선한다.
|
||||
|
||||
## 2. 최종 권장 아키텍처 한 줄 요약
|
||||
|
||||
초기 최적안은 `단일 애플리케이션 계열 + PostgreSQL + Redis + MinIO + Caddy` 조합이다.
|
||||
|
||||
- 클라이언트 외부 프로토콜은 `HTTPS REST + WSS(WebSocket over TLS)`를 사용한다.
|
||||
- 서버 애플리케이션은 논리적으로 `API/Realtime/Worker`로 분리하되, 초기에는 같은 코드베이스와 같은 이미지에서 역할만 나눠 배포한다.
|
||||
- 영속 데이터는 PostgreSQL 하나를 기준축으로 삼는다.
|
||||
- Redis는 presence, 세션 인덱스, 단기 캐시, fan-out 보조 용도로만 사용하고, 진실의 원천으로 쓰지 않는다.
|
||||
- 첨부파일과 프로필 이미지는 MinIO에 저장하되 S3 호환 계층으로 추상화해서 이후 외부 오브젝트 스토리지로 쉽게 옮길 수 있게 한다.
|
||||
|
||||
## 3. 왜 이 아키텍처가 현재 VPS에 가장 적합한가
|
||||
|
||||
이 VPS는 단일 머신이다. 따라서 초기에 Kafka, 다수의 마이크로서비스, 복잡한 서비스 메시를 올리면 운영 부담이 가치보다 커진다. 반대로 단일 앱 컨테이너 하나에 모든 역할을 몰아넣고 Redis 없이 메모리만 사용하면, 나중에 멀티 인스턴스나 재시작 복구, presence 일관성, fan-out 제어에서 다시 뜯어고쳐야 한다.
|
||||
|
||||
따라서 가장 균형이 좋은 선택은 아래와 같다.
|
||||
|
||||
- 앱은 단순하게 유지한다.
|
||||
- 상태 저장은 PostgreSQL로 고정한다.
|
||||
- 연결 상태와 단기 이벤트 전달은 Redis에 맡긴다.
|
||||
- 첨부파일은 애플리케이션 로컬 디스크가 아니라 S3 호환 저장소로 보낸다.
|
||||
- 프록시는 Caddy로 단순화한다.
|
||||
|
||||
이렇게 하면 MVP에서는 운영 난이도가 낮고, 이후 앱 복제본을 늘릴 때도 방향이 유지된다.
|
||||
|
||||
## 4. 외부 프로토콜 권장안
|
||||
|
||||
### 4.1 클라이언트 통신
|
||||
|
||||
권장:
|
||||
|
||||
- 인증, 초기 동기화, 대화방 목록, 과거 메시지 조회, 첨부 업로드 시작/완료: `HTTPS REST`
|
||||
- 실시간 메시지 수신, 읽음 상태, 타이핑 상태, presence, 서버 이벤트 푸시: `WSS`
|
||||
|
||||
선택 이유:
|
||||
|
||||
- Windows 데스크톱에서 구현과 디버깅이 쉽다.
|
||||
- 프록시/Caddy/TLS 환경에서 안정적이다.
|
||||
- 모바일/웹 클라이언트로 확장해도 동일 패턴을 유지하기 쉽다.
|
||||
- gRPC streaming보다 도입 장벽이 낮고, 브라우저/데스크톱 지원이 직관적이다.
|
||||
|
||||
권장하지 않는 초기 선택:
|
||||
|
||||
- 순수 TCP 커스텀 프로토콜: 성능 이점보다 운영/보안/프록시 복잡성이 커진다.
|
||||
- MQTT: 메신저 도메인 모델과 인증/권한/히스토리 조회가 결국 별도 API를 필요로 하므로 단순해지지 않는다.
|
||||
- WebRTC data channel 중심 설계: P2P는 NAT, 오프라인 저장, 멀티 디바이스 동기화에 불리하다.
|
||||
|
||||
### 4.2 앱 레벨 메시지 프레임
|
||||
|
||||
초기부터 다음 개념은 반드시 포함한다.
|
||||
|
||||
- `message_id`: ULID 또는 UUIDv7
|
||||
- `conversation_id`
|
||||
- `sender_user_id`
|
||||
- `client_request_id`: 중복 전송 방지용
|
||||
- `server_sequence`: 대화방 단위 또는 사용자 inbox 단위 순서값
|
||||
- `sent_at`, `delivered_at`, `read_at`
|
||||
- `event_version`
|
||||
- `device_id`
|
||||
|
||||
이 필드가 있으면 이후 재전송, 읽음 동기화, 멀티 디바이스, 중복 ACK 처리, 서버 재시작 복구가 쉬워진다.
|
||||
|
||||
## 5. 서비스 구성 권장안
|
||||
|
||||
초기 컨테이너 구성:
|
||||
|
||||
- `messenger-caddy`
|
||||
외부 80/443 수신, TLS 종료, WebSocket 업그레이드 처리, 정적 헬스 라우팅
|
||||
- `messenger-app`
|
||||
REST API와 WebSocket 게이트웨이 역할 수행
|
||||
- `messenger-worker`
|
||||
outbox 처리, 알림 발행, 썸네일/미디어 후처리, 정리 배치 작업
|
||||
- `messenger-postgres`
|
||||
핵심 영속 데이터 저장소
|
||||
- `messenger-redis`
|
||||
presence, session map, short-lived cache, publish/subscribe 보조
|
||||
- `messenger-minio`
|
||||
첨부파일, 프로필 이미지, 미디어 저장
|
||||
- `messenger-backup`
|
||||
PostgreSQL 백업과 MinIO 메타/버킷 동기화 작업
|
||||
- `messenger-prometheus`
|
||||
메트릭 수집
|
||||
- `messenger-loki`
|
||||
로그 저장
|
||||
- `messenger-grafana`
|
||||
대시보드와 알림 뷰
|
||||
- `messenger-node-exporter` 또는 유사 호스트 메트릭 수집기
|
||||
VPS CPU, 메모리, 디스크, 네트워크 관측
|
||||
|
||||
중요한 점:
|
||||
|
||||
- `messenger-app`과 `messenger-worker`는 같은 코드베이스를 쓰는 것이 좋다.
|
||||
- 초기에는 마이크로서비스로 쪼개지 않는다.
|
||||
- 역할만 분리해 컨테이너를 나누면, 장애면을 분리하고 추후 스케일 아웃하기 쉬워진다.
|
||||
|
||||
## 6. 데이터 저장 전략
|
||||
|
||||
### 6.1 PostgreSQL을 중심으로 두는 이유
|
||||
|
||||
채팅 서비스는 결국 다음을 안정적으로 다뤄야 한다.
|
||||
|
||||
- 대화방 생성/멤버십
|
||||
- 메시지 영속화
|
||||
- 읽음 상태
|
||||
- 첨부 메타데이터
|
||||
- 차단/숨김/핀/알림 설정
|
||||
- 운영 감사 로그
|
||||
|
||||
이 영역은 관계형 정합성이 중요하다. PostgreSQL은 트랜잭션, 인덱스, JSONB, 전문 검색, WAL 기반 백업, 추후 복제까지 한 번에 가져갈 수 있다.
|
||||
|
||||
초기에는 NoSQL보다 PostgreSQL이 훨씬 안전하다.
|
||||
|
||||
### 6.2 핵심 테이블 범주
|
||||
|
||||
초기부터 분리해서 설계할 범주:
|
||||
|
||||
- 사용자/프로필
|
||||
- 디바이스/세션
|
||||
- 친구 관계 또는 연락처 매핑
|
||||
- 대화방
|
||||
- 대화방 멤버
|
||||
- 메시지
|
||||
- 메시지 첨부 메타
|
||||
- 메시지 전달 상태
|
||||
- 읽음 커서
|
||||
- 리액션
|
||||
- 차단/뮤트/보관
|
||||
- 감사 로그
|
||||
- 아웃박스 이벤트
|
||||
|
||||
핵심 원칙:
|
||||
|
||||
- 메시지 본문은 PostgreSQL에 저장한다.
|
||||
- 첨부파일 바이너리는 MinIO에 저장하고 DB에는 메타와 경로만 둔다.
|
||||
- 읽음 상태는 사용자별 커서 모델을 우선한다. 메시지별 읽음 row를 무분별하게 만들면 커진다.
|
||||
|
||||
### 6.3 Redis 역할 제한
|
||||
|
||||
Redis는 아래만 맡기는 것이 맞다.
|
||||
|
||||
- 온라인 사용자 presence TTL
|
||||
- 웹소켓 세션 라우팅 인덱스
|
||||
- 단기 rate limit 카운터
|
||||
- 최근 조회 캐시
|
||||
- 멀티 인스턴스 fan-out 보조 pub/sub
|
||||
|
||||
Redis를 진실의 원천으로 두면 복구와 디버깅이 어려워진다. 메시지 원본이나 유일한 unread 상태를 Redis에 넣는 것은 피한다.
|
||||
|
||||
## 7. 실시간 메시징 처리 흐름 권장안
|
||||
|
||||
메시지 전송 처리 흐름:
|
||||
|
||||
1. 클라이언트가 `client_request_id` 포함 메시지 전송
|
||||
2. 서버가 권한과 대화방 멤버십 확인
|
||||
3. PostgreSQL 트랜잭션에서 메시지 row 저장
|
||||
4. 같은 트랜잭션에서 outbox 이벤트 저장
|
||||
5. 커밋 성공 후 송신자에게 서버 확정 ACK 반환
|
||||
6. worker가 outbox를 읽고 수신자 online 세션으로 fan-out
|
||||
7. 수신 클라이언트 ACK 수신 후 전달 상태 갱신
|
||||
8. 읽음 이벤트는 별도 커서 업데이트로 처리
|
||||
|
||||
이 구조의 장점:
|
||||
|
||||
- DB 커밋 전송 실패와 푸시 실패를 분리할 수 있다.
|
||||
- 서버가 내려갔다가 올라와도 outbox 기준으로 재처리 가능하다.
|
||||
- 이후 Redis pub/sub에서 NATS JetStream 같은 것으로 바꿔도 모델이 유지된다.
|
||||
|
||||
## 8. 첨부파일/미디어 저장 전략
|
||||
|
||||
초기 권장:
|
||||
|
||||
- 원본 파일은 MinIO에 저장
|
||||
- 업로드는 서버에서 발급한 제한적 업로드 정책 또는 서버 경유 업로드 중 하나를 선택
|
||||
- MVP에서는 서버 경유 업로드가 구현은 쉽지만, 대용량에서 병목이 된다
|
||||
- 가능하면 초반부터 S3 호환 업로드 패턴으로 설계하는 것이 낫다
|
||||
|
||||
권장 메타데이터:
|
||||
|
||||
- object_key
|
||||
- original_filename
|
||||
- mime_type
|
||||
- byte_size
|
||||
- image_width, image_height
|
||||
- checksum
|
||||
- upload_status
|
||||
- virus_scan_status 또는 reserve 필드
|
||||
|
||||
확장 포인트:
|
||||
|
||||
- MinIO를 나중에 Cloudflare R2, AWS S3, Backblaze B2로 옮기기 쉽게 추상화
|
||||
- 썸네일과 프리뷰는 worker에서 비동기 생성
|
||||
|
||||
## 9. 인증/세션 전략
|
||||
|
||||
Windows PC만 먼저 개발하더라도 처음부터 아래를 고려한다.
|
||||
|
||||
- 사용자 계정과 디바이스 세션 분리
|
||||
- `device_id`를 가진 장치별 refresh session 유지
|
||||
- access token은 짧게, refresh token은 서버 추적 가능하게
|
||||
- 강제 로그아웃, 특정 디바이스 로그아웃, 세션 폐기 가능해야 함
|
||||
|
||||
권장:
|
||||
|
||||
- 앱 로그인 후 디바이스 세션 생성
|
||||
- WebSocket 연결 시 access token 검증
|
||||
- 세션 폐기 시 WebSocket 강제 끊기
|
||||
|
||||
이렇게 해야 추후 모바일 앱을 붙여도 모델을 바꾸지 않는다.
|
||||
|
||||
## 10. VPS 배포 전략
|
||||
|
||||
### 10.1 배포 방식
|
||||
|
||||
권장:
|
||||
|
||||
- Docker Compose 기반 단일 프로젝트 스택
|
||||
- systemd에서 compose 스택을 부팅 시 자동 기동
|
||||
- 상태 저장 볼륨은 명확히 분리
|
||||
|
||||
권장 볼륨 범주:
|
||||
|
||||
- PostgreSQL data
|
||||
- Redis data
|
||||
- MinIO data
|
||||
- Grafana data
|
||||
- Prometheus data
|
||||
- Loki data
|
||||
- 앱 업로드 임시 디렉터리
|
||||
|
||||
주의:
|
||||
|
||||
- 기존 VPS에 다른 서비스가 있다면 네트워크와 도메인을 분리해야 한다.
|
||||
- 외부 80/443을 이미 다른 Caddy가 쓰고 있다면, 새 메신저 스택은 기존 프록시에 역방향 프록시로 붙이는 방식이 가장 안전하다.
|
||||
- 메신저 전용 서브도메인을 분리한다. 예: `msg.example.com`, `api.msg.example.com`, `ws.msg.example.com`, `media.msg.example.com`
|
||||
|
||||
### 10.2 네트워크/포트
|
||||
|
||||
외부 공개는 최소화한다.
|
||||
|
||||
- 외부 공개: `80`, `443`, `22`
|
||||
- 내부 전용: PostgreSQL, Redis, MinIO 관리 포트, Grafana, Prometheus, Loki
|
||||
|
||||
원칙:
|
||||
|
||||
- DB/Redis/MinIO는 외부에 직접 열지 않는다.
|
||||
- 관리자 UI가 필요하면 VPN 또는 SSH 터널 기반 접근만 허용한다.
|
||||
|
||||
### 10.3 리소스 운영 방침
|
||||
|
||||
초기에는 다음 우선순위로 리소스를 배정한다.
|
||||
|
||||
1. PostgreSQL 메모리 보장
|
||||
2. 앱/WebSocket 연결 처리 여유 확보
|
||||
3. MinIO 디스크 여유 확보
|
||||
4. 관측성 스택은 과하지 않게 운영
|
||||
|
||||
관측 스택이 너무 무거우면 다음 순서로 경량화한다.
|
||||
|
||||
- Grafana 유지
|
||||
- Prometheus retention 축소
|
||||
- Loki retention 축소
|
||||
- 고해상도 scrape interval 완화
|
||||
|
||||
## 11. 관측성 설계
|
||||
|
||||
최소한 아래는 반드시 수집한다.
|
||||
|
||||
애플리케이션 메트릭:
|
||||
|
||||
- 활성 WebSocket 연결 수
|
||||
- 인증 성공/실패 수
|
||||
- 메시지 송신 TPS
|
||||
- 메시지 저장 지연
|
||||
- outbox 적체 수
|
||||
- fan-out 지연
|
||||
- 읽음 이벤트 처리량
|
||||
- 업로드 성공/실패 수
|
||||
- 대화방별 초당 메시지 폭주 탐지
|
||||
|
||||
인프라 메트릭:
|
||||
|
||||
- CPU
|
||||
- 메모리
|
||||
- 디스크 사용량
|
||||
- 디스크 IOPS
|
||||
- 네트워크 in/out
|
||||
- 컨테이너 재시작 횟수
|
||||
- PostgreSQL connections, replication lag 향후 대비
|
||||
- Redis memory eviction 여부
|
||||
|
||||
로그:
|
||||
|
||||
- JSON 구조화 로그
|
||||
- request_id, user_id, device_id, conversation_id, message_id 등 상관 키 포함
|
||||
- 인증 실패, 권한 오류, 메시지 저장 실패는 별도 레벨 관리
|
||||
|
||||
대시보드 우선순위:
|
||||
|
||||
1. 서비스 헬스
|
||||
2. 메시지 송수신 지연
|
||||
3. DB 상태
|
||||
4. 첨부 업로드 상태
|
||||
5. 에러 로그 상위 유형
|
||||
|
||||
## 12. 백업 및 복구 전략
|
||||
|
||||
가장 중요한 원칙:
|
||||
|
||||
- 같은 VPS 안의 다른 디렉터리에 저장한 사본은 백업이 아니다.
|
||||
- MinIO에 저장한 DB 덤프도 MinIO가 같은 머신에 있으면 재해 복구가 아니다.
|
||||
|
||||
권장 백업 구조:
|
||||
|
||||
- PostgreSQL: 일간 전체 백업 + 지속적 WAL 아카이빙
|
||||
- 백업 저장 위치: 외부 오브젝트 스토리지
|
||||
- MinIO 버킷 데이터: 주기적 외부 스토리지 동기화
|
||||
- 설정 파일과 compose 파일: Git 또는 암호화된 설정 저장소 별도 보관
|
||||
|
||||
복구 목표:
|
||||
|
||||
- RPO 목표: 5분에서 15분
|
||||
- RTO 목표: 1시간 이내
|
||||
|
||||
권장 운영 습관:
|
||||
|
||||
- 월 1회 복구 리허설
|
||||
- 최소 스테이징 환경으로 복원 테스트
|
||||
- 백업 성공 여부만 보지 말고 실제 복구 성공 여부 확인
|
||||
|
||||
## 13. 보안 권장안
|
||||
|
||||
초기 사이드 프로젝트라도 아래는 반드시 한다.
|
||||
|
||||
- root 비밀번호 SSH 로그인을 끄고 키 기반 인증으로 전환
|
||||
- fail2ban 또는 동등한 차단 장치 적용
|
||||
- firewalld/ufw로 외부 포트 최소화
|
||||
- Docker secrets 또는 최소한 권한 제한된 env 파일 사용
|
||||
- DB/Redis/MinIO 비밀번호 분리
|
||||
- TLS 강제
|
||||
- 관리자 계정 MFA 고려
|
||||
- 민감 로그 마스킹
|
||||
|
||||
애플리케이션 보안 체크포인트:
|
||||
|
||||
- rate limiting
|
||||
- 대화방 멤버십 권한 검사
|
||||
- 첨부파일 MIME 및 확장자 검증
|
||||
- 업로드 크기 제한
|
||||
- 악성 파일 검사 훅 준비
|
||||
- 세션 폐기와 토큰 로테이션
|
||||
|
||||
## 14. 스케일링 경로
|
||||
|
||||
### 단계 1. MVP
|
||||
|
||||
구성:
|
||||
|
||||
- 단일 VPS
|
||||
- 단일 `messenger-app`
|
||||
- 단일 `messenger-worker`
|
||||
- PostgreSQL 단일 인스턴스
|
||||
- Redis 단일 인스턴스
|
||||
- MinIO 단일 인스턴스
|
||||
|
||||
적합한 상황:
|
||||
|
||||
- 개인 프로젝트 초기
|
||||
- 소수 사용자
|
||||
- 운영자가 혼자일 때
|
||||
|
||||
### 단계 2. 같은 아키텍처로 성능 확장
|
||||
|
||||
구성 변화:
|
||||
|
||||
- 더 큰 VPS 또는 별도 앱 서버
|
||||
- `messenger-app` 복수 인스턴스
|
||||
- Redis 기반 세션 공유와 fan-out
|
||||
- PostgreSQL 튜닝 및 읽기 부하 분산 준비
|
||||
|
||||
필수 전제:
|
||||
|
||||
- WebSocket 세션 상태가 로컬 메모리에만 있지 않아야 함
|
||||
- outbox 기반 비동기 처리 구조가 이미 있어야 함
|
||||
|
||||
### 단계 3. 상태 저장 계층 분리
|
||||
|
||||
구성 변화:
|
||||
|
||||
- PostgreSQL을 관리형 또는 별도 전용 서버로 이동
|
||||
- MinIO를 외부 오브젝트 스토리지로 이동
|
||||
- Redis를 별도 인스턴스로 분리
|
||||
- 앱은 여러 대로 수평 확장
|
||||
|
||||
이 단계에서 바뀌면 좋은 것:
|
||||
|
||||
- 로드밸런서 도입
|
||||
- 무중단 배포
|
||||
- 관측성 경보 체계 정교화
|
||||
|
||||
### 단계 4. 더 큰 사용량
|
||||
|
||||
구성 변화:
|
||||
|
||||
- 내부 이벤트 버스를 Redis pub/sub에서 `NATS JetStream`으로 교체 또는 병행
|
||||
- PostgreSQL 읽기 복제본 도입
|
||||
- 검색을 외부 검색엔진으로 분리
|
||||
- 대화방 파티셔닝 또는 사용자 샤딩 검토
|
||||
|
||||
Kafka는 이 프로젝트 규모에서 너무 이른 선택일 가능성이 높다. NATS JetStream이 운영 난이도와 기능 균형이 더 좋다.
|
||||
|
||||
## 15. 마이그레이션 경로를 부드럽게 만드는 설계 포인트
|
||||
|
||||
초기부터 반드시 지켜야 하는 항목:
|
||||
|
||||
- 메시지 ID는 시간 정렬 가능한 전역 고유값 사용
|
||||
- DB 마이그레이션 체계 도입
|
||||
- 아웃박스 패턴 도입
|
||||
- 첨부 저장소는 S3 호환 인터페이스 뒤에 숨기기
|
||||
- 세션/프레즌스는 Redis 기반으로 외부화 가능하게 만들기
|
||||
- API 응답과 WebSocket 이벤트에 버전 필드 두기
|
||||
- 기능 플래그 도입
|
||||
|
||||
이것만 지켜도 단일 VPS에서 시작한 시스템을 크게 깨지 않고 다음 단계로 옮길 수 있다.
|
||||
|
||||
## 16. 자가 구현 서버 vs Matrix/기타 프로토콜 비교
|
||||
|
||||
### 16.1 이번 프로젝트의 권장 선택
|
||||
|
||||
이번 프로젝트는 `자가 구현 서버`가 더 적합하다.
|
||||
|
||||
이유:
|
||||
|
||||
- 카카오톡 PC 스타일 UX를 맞추려면 제품 흐름과 프로토콜을 세밀하게 제어해야 한다.
|
||||
- Matrix는 강력하지만 연합, 브리지, 규격 준수, 이벤트 모델 이해 비용이 크다.
|
||||
- 단일 VPS에서 개인 프로젝트로 운영하기에는 Synapse 계열은 무겁고 운영 난이도가 높다.
|
||||
- Element류 생태계와 맞물리는 장점보다, 제품 개성과 속도에서 제약이 더 크게 느껴질 가능성이 높다.
|
||||
|
||||
### 16.2 Matrix가 적합한 경우
|
||||
|
||||
- 오픈 프로토콜과 연합이 중요할 때
|
||||
- 외부 클라이언트 호환성이 중요할 때
|
||||
- 자체 프로토콜 설계보다 표준 기반 확장을 원할 때
|
||||
|
||||
### 16.3 기타 선택지
|
||||
|
||||
- XMPP: 성숙했지만 현대적인 제품 UX에 맞춘 구현에서는 다시 커스텀 계층이 많이 필요하다.
|
||||
- MQTT: IoT와 경량 pub/sub에는 좋지만 메신저 영속 모델과 권한 모델이 핵심이 아니므로 주축으로는 비권장이다.
|
||||
|
||||
결론:
|
||||
|
||||
- MVP와 제품 완성도 우선이라면 자가 구현
|
||||
- 개방형 생태계 우선이라면 Matrix
|
||||
|
||||
이번 케이스에서는 자가 구현이 맞다.
|
||||
|
||||
## 17. 실제 구축 순서 권장안
|
||||
|
||||
1. 도메인과 서브도메인 구조 확정
|
||||
2. Docker Compose 스택 초안 구성
|
||||
3. PostgreSQL 스키마와 마이그레이션 체계 확정
|
||||
4. 인증/세션 모델 확정
|
||||
5. REST + WSS 이벤트 계약서 작성
|
||||
6. outbox 기반 메시지 저장/전달 흐름 구현
|
||||
7. Redis presence 및 세션 인덱스 도입
|
||||
8. MinIO 첨부 업로드 흐름 도입
|
||||
9. Prometheus/Loki/Grafana 기본 대시보드 구성
|
||||
10. 외부 백업 파이프라인 연결
|
||||
11. 장애 복구 리허설
|
||||
12. 부하 테스트 후 튜닝
|
||||
|
||||
## 18. 최종 합의안
|
||||
|
||||
가장 현실적이고 좋은 방향은 아래다.
|
||||
|
||||
- 프로토콜은 `REST + WebSocket`
|
||||
- 서버 구조는 `모놀리식 코드베이스 + app/worker 역할 분리`
|
||||
- DB는 `PostgreSQL`
|
||||
- 캐시/프레즌스는 `Redis`
|
||||
- 첨부 저장소는 `MinIO(S3 호환)`
|
||||
- 프록시는 `Caddy`
|
||||
- 관측성은 `Prometheus + Loki + Grafana`
|
||||
- 백업은 `외부 오브젝트 스토리지로 분리`
|
||||
- 확장 경로는 `단일 VPS -> 앱 복제본 -> 상태 저장 계층 외부화 -> NATS/복제/샤딩`
|
||||
|
||||
이 구조는 사이드 프로젝트의 실행 가능성과, 이후 실제 사용자가 붙었을 때의 확장 가능성 사이에서 가장 균형이 좋다.
|
||||
427
docs/archive/planning/06-quality-release-and-launch.md
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
# 품질 전략, 테스트 계획, 릴리즈 게이트, 텔레메트리, 베타 운영안
|
||||
|
||||
## 문서 목적
|
||||
|
||||
이 문서는 Windows PC 우선 메신저 사이드 프로젝트의 품질 확보 체계를 정의한다. 범위는 데스크톱 앱과 VPS 기반 채팅 백엔드를 함께 포함하며, 알파 단계부터 정식 런치 직전까지의 테스트, 관측성, 장애 대응, 승인 기준, 최종 마감 체크리스트를 다룬다.
|
||||
|
||||
이 제안은 최소 6개 관점의 합의안으로 본다.
|
||||
|
||||
- QA 리드: 회귀 방지와 출시 게이트 설계
|
||||
- Windows 데스크톱 엔지니어: 설치, 업데이트, 자원 사용량, 크래시 대응
|
||||
- 백엔드/SRE: VPS 안정성, 장애 복구, 로그와 모니터링
|
||||
- UX 리서처: 대화 흐름, 인지 부하, 사용성 검증
|
||||
- 보안/프라이버시 담당: 로그 최소 수집, 민감정보 처리 원칙
|
||||
- 릴리즈 매니저: 베타 롤아웃, 빌드 승격, 런치 준비
|
||||
|
||||
## 1. 품질 전략 원칙
|
||||
|
||||
### 1.1 제품 품질의 정의
|
||||
|
||||
이 프로젝트의 품질은 단순히 버그 수가 적은 상태가 아니라 아래 조건을 동시에 만족하는 상태로 정의한다.
|
||||
|
||||
- 메신저의 핵심 루프가 끊기지 않는다.
|
||||
- 느린 네트워크와 일시적 장애에서도 대화 맥락이 유지된다.
|
||||
- Windows PC 환경에서 설치, 실행, 업데이트, 복구가 자연스럽다.
|
||||
- UI가 카카오톡 PC 사용 경험을 참고하되, 더 세련되고 명확한 피드백을 제공한다.
|
||||
- 텔레메트리와 크래시 리포트가 문제를 재현 가능한 수준까지 설명한다.
|
||||
- 정식 출시 직전에는 치명적 버그를 막는 게이트가 자동화와 수동 검수 양쪽에 존재한다.
|
||||
|
||||
### 1.2 품질 우선순위
|
||||
|
||||
우선순위는 아래 순서로 둔다.
|
||||
|
||||
1. 메시지 손실 방지
|
||||
2. 로그인/세션 안정성
|
||||
3. 실시간 연결 복구
|
||||
4. UI 응답성과 스크롤/입력 안정성
|
||||
5. 설치, 자동 업데이트, 재실행 복구
|
||||
6. 미세한 시각 완성도와 인터랙션 폴리시
|
||||
|
||||
### 1.3 품질 목표
|
||||
|
||||
초기 런치 기준 목표치는 아래처럼 잡는다.
|
||||
|
||||
- 앱 크래시 없는 세션 비율: 99.5% 이상
|
||||
- 메시지 전송 성공률: 99.9% 이상
|
||||
- 네트워크 일시 단절 후 자동 재연결 성공률: 95% 이상
|
||||
- 앱 콜드 스타트에서 대화 목록 표시까지: 일반 환경 기준 3초 이내
|
||||
- 기본 채팅 입력 반응 시간: 체감상 지연 없이 100ms 이하 목표
|
||||
- 중대한 회귀 버그가 있는 빌드는 외부 베타로 승격 금지
|
||||
|
||||
## 2. 테스트 전략 전체 구조
|
||||
|
||||
테스트는 아래 4층으로 설계한다.
|
||||
|
||||
- Unit Test: 메시지 상태 전이, 입력 검증, 포맷터, 상태 관리, 동기화 로직
|
||||
- Integration Test: 데스크톱 앱 모듈 간 연계, 로컬 저장소, 인증, 소켓/HTTP 결합
|
||||
- End-to-End Test: 실제 사용자 시나리오를 앱 + VPS 환경에서 검증
|
||||
- Manual UX Check: 시각 완성도, 감정선, 읽기 흐름, 미세한 피드백 점검
|
||||
|
||||
자동화만으로는 메신저 품질이 충분히 보장되지 않으므로, UI와 상호작용의 감성적 완성도는 반드시 수동 검수를 포함한다.
|
||||
|
||||
## 3. 단계별 테스트 계획
|
||||
|
||||
## 3.1 Unit Test 범위
|
||||
|
||||
가장 먼저 안정화해야 할 단위는 아래와 같다.
|
||||
|
||||
- 메시지 모델: 생성, 수정, 삭제, 전달 상태, 읽음 상태, 실패 상태 전이
|
||||
- 대화방 정렬 로직: 최신 메시지 기준 정렬, 고정 대화방 우선 노출
|
||||
- 검색 로직: 채팅방 이름, 메시지 텍스트, 사용자 이름 검색 결과 일관성
|
||||
- 입력창 로직: 멀티라인 입력, 엔터 전송/줄바꿈 규칙, IME 조합 중 입력 처리
|
||||
- 파일 첨부 로직: 허용 포맷, 크기 제한, 업로드 대기/실패 상태
|
||||
- 세션 상태: 로그인 유지, 토큰 만료, 재인증 트리거
|
||||
- 재연결 전략: backoff, 중복 연결 방지, 연결 상태 배지 표시 조건
|
||||
- 알림 로직: 포그라운드/백그라운드 조건별 알림 발송 정책
|
||||
- 로컬 캐시: 최근 대화, 프로필, 읽음 위치 복원
|
||||
|
||||
Unit Test의 기준은 단순 커버리지 수치보다, 상태 전이가 많은 로직을 빠짐없이 명세하는 데 둔다.
|
||||
|
||||
## 3.2 Integration Test 범위
|
||||
|
||||
데스크톱 앱과 백엔드 경계, 또는 앱 내부 모듈 경계에서 아래를 검증한다.
|
||||
|
||||
- 로그인 후 사용자 정보, 대화 목록, 최근 메시지 초기 로드
|
||||
- WebSocket 연결 수립 전후의 REST fallback 동작
|
||||
- 네트워크 재연결 이후 누락 메시지 동기화
|
||||
- 이미지/파일 업로드 후 메시지 항목과 미디어 URL 연결
|
||||
- 읽음 처리 전송 후 상대/서버 상태 반영
|
||||
- 알림 클릭 시 정확한 대화방으로 포커스 이동
|
||||
- 로컬 저장소 손상 또는 오래된 캐시 버전에서 안전한 복구
|
||||
- 앱 업데이트 후 기존 세션과 캐시 유지 여부
|
||||
- 서버 응답 지연, 중복 응답, 순서 뒤집힘 상황에서 UI 일관성 유지
|
||||
|
||||
## 3.3 End-to-End Test 시나리오
|
||||
|
||||
필수 E2E 시나리오는 아래를 포함한다.
|
||||
|
||||
- 신규 사용자 로그인 후 첫 채팅 시작
|
||||
- 기존 사용자 재실행 후 대화 목록 복원
|
||||
- 1:1 채팅 메시지 송수신
|
||||
- 다자간 채팅방 입장, 메시지 수신, 읽음 상태 변화
|
||||
- 이미지 첨부, 업로드 완료, 실패 후 재시도
|
||||
- 앱 재실행 중 미전송 메시지 복구
|
||||
- 네트워크 끊김 중 입력/전송 시도 후 복구
|
||||
- 중복 로그인 또는 세션 만료 상황 처리
|
||||
- 검색에서 채팅방 진입 후 뒤로 이동
|
||||
- 알림 클릭으로 특정 메시지 문맥 진입
|
||||
- 오래된 메시지 스크롤 페이징
|
||||
- 매우 긴 대화방 이름, 긴 메시지, 이모지, 한글 IME 혼합 입력
|
||||
- 서버 재시작 중 클라이언트 복구
|
||||
|
||||
E2E는 로컬 스텁 환경만으로 끝내지 말고, 실제 VPS 스테이징 환경에서도 주기적으로 돌려야 한다.
|
||||
|
||||
## 4. 수동 UX 검수 계획
|
||||
|
||||
자동 테스트가 통과해도 아래 항목은 사람이 직접 판단해야 한다.
|
||||
|
||||
- 첫 실행 시 정보 구조가 즉시 이해되는가
|
||||
- 좌측 대화 목록, 중앙 대화 영역, 상단 상태 정보의 시선 흐름이 자연스러운가
|
||||
- 선택 상태, hover, unread, muted, pinned 상태가 한눈에 구분되는가
|
||||
- 메시지 입력창의 높이 변화와 스크롤 이동이 거슬리지 않는가
|
||||
- 새 메시지 도착 시 시각적 피드백이 과하지 않으면서 놓치지 않게 설계됐는가
|
||||
- 읽음 표시, 전송 중, 실패 상태의 의미가 직관적인가
|
||||
- 설정 화면이 과도하게 복잡하지 않은가
|
||||
- 오류 문구가 사용자 책임으로 들리지 않고, 해결 가능성을 제시하는가
|
||||
- 폰트 렌더링, 한글 자간, line-height, 아이콘 선 굵기가 일관적인가
|
||||
- 다크/라이트 모드가 있다면 대비와 시선 분리가 안정적인가
|
||||
|
||||
수동 UX 검수는 최소 3종 장비 조합으로 수행한다.
|
||||
|
||||
- 일반적인 FHD 노트북
|
||||
- 고배율 디스플레이가 적용된 고해상도 Windows PC
|
||||
- 저사양 또는 메모리 제약이 있는 테스트 장비
|
||||
|
||||
## 5. 네트워크 복원력 시나리오
|
||||
|
||||
메신저는 정상 네트워크보다 비정상 네트워크에서 품질 차이가 크게 드러난다. 아래 시나리오는 별도 회복력 테스트 묶음으로 관리한다.
|
||||
|
||||
### 5.1 연결 품질 저하
|
||||
|
||||
- 고지연 환경
|
||||
- 패킷 손실 환경
|
||||
- 업로드만 느린 환경
|
||||
- 순간적인 DNS 실패
|
||||
- TLS 핸드셰이크 지연
|
||||
|
||||
### 5.2 연결 중단과 복구
|
||||
|
||||
- Wi-Fi 꺼짐 후 재연결
|
||||
- 노트북 절전 진입 후 복귀
|
||||
- VPN on/off 전환
|
||||
- 사내망/공용망 전환
|
||||
- 백엔드 WebSocket 프로세스 재시작
|
||||
- VPS 재부팅
|
||||
|
||||
### 5.3 동기화 이상
|
||||
|
||||
- 메시지 ACK 지연
|
||||
- 서버는 수신했지만 클라이언트가 ACK를 못 받은 상태
|
||||
- 동일 메시지 중복 수신
|
||||
- 오래된 이벤트가 늦게 도착
|
||||
- 읽음 이벤트가 메시지보다 먼저 도착
|
||||
|
||||
### 5.4 기대 동작
|
||||
|
||||
각 네트워크 이상 시나리오에서 앱은 아래 동작을 만족해야 한다.
|
||||
|
||||
- 연결 상태를 숨기지 않고 명확하게 알려준다.
|
||||
- 전송 실패 메시지는 사라지지 않고 재시도 가능해야 한다.
|
||||
- 재연결 이후 중복 메시지를 만들지 않아야 한다.
|
||||
- 사용자 입력은 가능한 한 보존되어야 한다.
|
||||
- 회복 후에는 최신 상태와의 차이를 자동 동기화해야 한다.
|
||||
|
||||
## 6. 텔레메트리 전략
|
||||
|
||||
텔레메트리는 문제를 빨리 발견하고 우선순위를 정하기 위한 최소 수집 원칙으로 설계한다. 메시지 본문, 파일 내용, 개인식별 가능 정보는 수집하지 않는다.
|
||||
|
||||
### 6.1 핵심 이벤트
|
||||
|
||||
- 앱 실행, 종료, 비정상 종료
|
||||
- 로그인 성공/실패
|
||||
- 대화 목록 로드 성공/실패/지연
|
||||
- 채팅방 진입
|
||||
- 메시지 전송 시도/성공/실패
|
||||
- 파일 업로드 시도/성공/실패
|
||||
- WebSocket 연결, 끊김, 재연결
|
||||
- API 오류 코드 집계
|
||||
- 업데이트 다운로드/적용 성공 여부
|
||||
|
||||
### 6.2 핵심 지표
|
||||
|
||||
- DAU/WAU
|
||||
- 세션 길이
|
||||
- 대화방 진입 대비 실제 메시지 전송 비율
|
||||
- 메시지 전송 실패율
|
||||
- reconnect 횟수와 성공률
|
||||
- 앱 버전별 크래시율
|
||||
- API 엔드포인트별 실패율과 p95 응답 시간
|
||||
- 특정 릴리즈 이후 회귀 지표 변화
|
||||
|
||||
### 6.3 대시보드 구성
|
||||
|
||||
릴리즈 직후 가장 먼저 보는 대시보드는 아래 4개다.
|
||||
|
||||
- 안정성 대시보드: 크래시율, 비정상 종료, 재실행 루프
|
||||
- 메시징 대시보드: 전송 성공률, ACK 지연, 중복 메시지 감지
|
||||
- 연결성 대시보드: WebSocket 단절, 재연결 성공률, 서버 에러율
|
||||
- 릴리즈 대시보드: 버전별 설치 성공률, 업데이트 적용 성공률, 롤백 필요 신호
|
||||
|
||||
## 7. 크래시 핸들링 전략
|
||||
|
||||
### 7.1 클라이언트
|
||||
|
||||
- 비정상 종료 시 다음 실행에서 안전 복구 모드 진입 여부를 판단한다.
|
||||
- 크래시 리포트에는 앱 버전, OS 버전, 메모리 상태, 직전 화면, 최근 오류 범주를 포함한다.
|
||||
- 민감정보와 메시지 본문은 제외한다.
|
||||
- 크래시 직전 사용자가 작성 중이던 미전송 텍스트는 가능한 범위에서 복구한다.
|
||||
- 반복 크래시가 특정 화면 진입에서 발생하면 해당 기능을 임시 비활성화할 수 있어야 한다.
|
||||
|
||||
### 7.2 서버
|
||||
|
||||
- 프로세스 재시작 정책을 설정한다.
|
||||
- 앱 서버, DB, 스토리지, 리버스 프록시 각각의 헬스 체크를 분리한다.
|
||||
- 장애 시 최근 배포 이력과 리소스 사용량을 즉시 연동 확인 가능해야 한다.
|
||||
- 치명 장애 발생 시 운영 알림 채널로 즉시 통지한다.
|
||||
|
||||
### 7.3 크래시 triage 우선순위
|
||||
|
||||
- P0: 앱 실행 불가, 로그인 불가, 메시지 손실 가능성
|
||||
- P1: 반복 크래시, 재연결 실패, 파일 업로드 핵심 기능 불가
|
||||
- P2: 특정 화면 진입 시 크래시, 우회 가능하지만 경험 훼손 큼
|
||||
- P3: 드문 환경에서의 비핵심 기능 크래시
|
||||
|
||||
## 8. 릴리즈 단계와 게이트
|
||||
|
||||
릴리즈는 `Alpha -> Closed Beta -> Open Beta -> RC -> Launch` 흐름으로 관리한다.
|
||||
|
||||
### 8.1 Alpha
|
||||
|
||||
목적:
|
||||
|
||||
- 핵심 채팅 루프가 성립하는지 확인
|
||||
- 구조적 결함 조기 발견
|
||||
|
||||
대상:
|
||||
|
||||
- 개발자 본인과 내부 소수 테스터
|
||||
|
||||
필수 기준:
|
||||
|
||||
- 로그인, 대화 목록, 메시지 송수신, 기본 재연결이 동작
|
||||
- 치명 크래시가 재현성 있게 남아있지 않음
|
||||
- 텔레메트리와 크래시 리포트 수집 가능
|
||||
|
||||
차단 조건:
|
||||
|
||||
- 메시지 유실 재현
|
||||
- 세션 꼬임
|
||||
- 앱 시작 불가
|
||||
|
||||
### 8.2 Closed Beta
|
||||
|
||||
목적:
|
||||
|
||||
- 다양한 Windows 환경에서 회귀와 설치/업데이트 문제 발견
|
||||
- 사용성 불만과 혼란 포인트 수집
|
||||
|
||||
대상:
|
||||
|
||||
- 신뢰 가능한 외부 사용자 20명에서 100명 규모
|
||||
|
||||
필수 기준:
|
||||
|
||||
- 자동 업데이트 또는 업데이트 유도 플로우 안정화
|
||||
- 주요 E2E와 네트워크 복원력 시나리오 통과
|
||||
- 고우선순위 이슈 대응 프로세스 마련
|
||||
|
||||
차단 조건:
|
||||
|
||||
- 버전 업 이후 캐시 손상
|
||||
- 특정 GPU/해상도 조합에서 UI unusable
|
||||
- 알림/포커스 이동이 신뢰할 수 없는 수준
|
||||
|
||||
### 8.3 Open Beta
|
||||
|
||||
목적:
|
||||
|
||||
- 실제 사용 패턴과 부하 기반 안정성 검증
|
||||
- VPS 백엔드의 운영 내구성 확인
|
||||
|
||||
대상:
|
||||
|
||||
- 초대 또는 신청 기반의 확장된 사용자군
|
||||
|
||||
필수 기준:
|
||||
|
||||
- 크래시율과 전송 실패율이 목표치 근처에서 안정화
|
||||
- 서버 모니터링, 알림, 백업, 장애 대응 문서화 완료
|
||||
- 주요 UX 불만의 상위 항목 정리와 개선 반영
|
||||
|
||||
차단 조건:
|
||||
|
||||
- 피크 시간대에서 서버 병목
|
||||
- 업로드/다운로드 기능의 잦은 실패
|
||||
- 보안/개인정보 위험 신호
|
||||
|
||||
### 8.4 RC
|
||||
|
||||
목적:
|
||||
|
||||
- 정식 출시 후보 빌드 확정
|
||||
|
||||
필수 기준:
|
||||
|
||||
- P0, P1 이슈 0건
|
||||
- 승인된 예외 목록만 남아있음
|
||||
- 회귀 테스트, 수동 UX 체크, 설치/업데이트 검증 완료
|
||||
- 릴리즈 노트, 알려진 이슈, 지원 대응 문안 준비 완료
|
||||
|
||||
### 8.5 Launch
|
||||
|
||||
목적:
|
||||
|
||||
- 안전한 공개 전환과 초기 운영 안정화
|
||||
|
||||
필수 기준:
|
||||
|
||||
- 모니터링 대시보드 활성화
|
||||
- 운영 온콜 또는 대응 시간대 확보
|
||||
- 롤백 경로와 이전 안정 버전 보관
|
||||
- 초기 72시간 관찰 계획 확정
|
||||
|
||||
## 9. 베타 롤아웃 전략
|
||||
|
||||
### 9.1 배포 원칙
|
||||
|
||||
- 한 번에 전체 공개하지 않고 점진적으로 확장한다.
|
||||
- 데스크톱 앱 버전과 서버 릴리즈를 명확히 매핑한다.
|
||||
- 서버 측 기능 플래그로 위험 기능을 단계적으로 연다.
|
||||
|
||||
### 9.2 추천 롤아웃 흐름
|
||||
|
||||
1. 내부 알파 100%
|
||||
2. 클로즈드 베타 10명
|
||||
3. 클로즈드 베타 30명
|
||||
4. 클로즈드 베타 100명
|
||||
5. 오픈 베타 제한 공개
|
||||
6. 정식 출시 전 RC 고정
|
||||
7. 런치 후 초기 사용자군 10% 수준 점진 확대
|
||||
|
||||
### 9.3 베타 피드백 수집
|
||||
|
||||
- 앱 내 피드백 진입점 제공
|
||||
- 이슈 제보 시 자동 첨부 가능한 진단 요약 제공
|
||||
- 설문은 짧게 유지하고, 채팅 사용 맥락 기반 질문으로 설계
|
||||
- 정량 지표와 정성 피드백을 따로 보지 말고 함께 해석
|
||||
|
||||
## 10. 승인 기준과 완료 정의
|
||||
|
||||
### 10.1 기능 승인 기준
|
||||
|
||||
한 기능은 아래를 만족해야 완료로 본다.
|
||||
|
||||
- 명세된 핵심 시나리오가 자동화 테스트 또는 재현 가능한 체크리스트로 검증됨
|
||||
- 실패 상태 UI와 문구가 존재함
|
||||
- 텔레메트리 이벤트가 연결됨
|
||||
- 접근성 기본 기준을 위반하지 않음
|
||||
- Windows 배율, 창 크기 변화, 포커스 이동에서 깨지지 않음
|
||||
|
||||
### 10.2 릴리즈 승인 기준
|
||||
|
||||
릴리즈는 아래를 모두 충족해야 승격한다.
|
||||
|
||||
- P0, P1 미해결 이슈 없음
|
||||
- 최근 변경 범위에 대한 회귀 테스트 완료
|
||||
- 설치, 실행, 로그인, 메시지 송수신, 파일 첨부, 재연결 수동 검수 완료
|
||||
- VPS 서버 상태, 디스크 사용량, DB 백업, 스토리지 상태 확인 완료
|
||||
- 모니터링과 알림이 실제로 동작함
|
||||
|
||||
## 11. 최종 폴리시와 마감 체크리스트
|
||||
|
||||
### 11.1 데스크톱 앱 최종 체크
|
||||
|
||||
- 설치/삭제가 정상 동작한다.
|
||||
- 첫 실행 경험이 매끄럽다.
|
||||
- 업데이트 후 설정, 세션, 캐시가 의도대로 유지된다.
|
||||
- 창 최소화, 복원, 다중 모니터 이동이 안정적이다.
|
||||
- 알림 클릭 시 정확한 대화방으로 이동한다.
|
||||
- 입력창, 스크롤, 붙여넣기, 드래그 앤 드롭이 예상대로 동작한다.
|
||||
- 고배율 디스플레이에서 흐릿한 요소가 없다.
|
||||
- 폰트, 아이콘, 간격, hover, selection이 일관적이다.
|
||||
|
||||
### 11.2 백엔드/VPS 최종 체크
|
||||
|
||||
- 프로세스 자동 시작과 재시작 정책이 확인됐다.
|
||||
- 리버스 프록시, 앱, DB, 스토리지 헬스 체크가 정상이다.
|
||||
- 백업과 복원 절차가 실제 검증됐다.
|
||||
- 로그 보존 정책과 디스크 사용량 경보가 설정됐다.
|
||||
- TLS, 도메인, 인증서 자동 갱신 상태가 확인됐다.
|
||||
- 서버 재기동 후 클라이언트 재연결이 정상 동작한다.
|
||||
|
||||
### 11.3 런치 72시간 체크
|
||||
|
||||
- 크래시율, 전송 실패율, 재연결 실패율을 2시간 단위로 본다.
|
||||
- 상위 오류 5개를 매일 triage 한다.
|
||||
- 사용자 피드백을 UX, 성능, 안정성으로 분류한다.
|
||||
- 심각한 회귀가 있으면 기능 플래그 off 또는 롤백을 즉시 검토한다.
|
||||
|
||||
## 12. 권장 운영 리듬
|
||||
|
||||
추천 리듬은 아래와 같다.
|
||||
|
||||
- 매일: 크래시/오류/전송 실패 지표 확인
|
||||
- 주 2회: 회귀 테스트와 베타 피드백 정리
|
||||
- 주 1회: 릴리즈 후보 점검 회의
|
||||
- 베타 기간 중: 매 릴리즈마다 수동 UX 검수 세션 수행
|
||||
|
||||
## 13. 최종 권고
|
||||
|
||||
이 프로젝트의 성공 여부는 UI를 얼마나 비슷하게 만들었는지보다, 사용자가 불안 없이 메시지를 보내고 다시 돌아왔을 때 맥락이 보존되는지에 달려 있다. 따라서 출시 게이트는 시각적 유사성보다 메시지 신뢰성, 재연결 복원력, 설치/업데이트 안정성, 관측성 완성도를 우선해야 한다.
|
||||
|
||||
정식 출시 직전에는 새로운 기능 추가를 멈추고 아래 4가지만 집중하는 것이 가장 낫다.
|
||||
|
||||
- 크래시 제거
|
||||
- 네트워크 복원력 보강
|
||||
- 텔레메트리와 운영 대시보드 정제
|
||||
- UI 폴리시와 마이크로 인터랙션 최종 다듬기
|
||||
14
docs/archive/planning/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Messenger Planning Set
|
||||
|
||||
이 폴더는 Windows PC 기준 개인 메신저 사이드 프로젝트의 기획 문서를 모아두는 planning set이다.
|
||||
|
||||
문서 목록:
|
||||
|
||||
1. [01-backend-platform-architecture.md](01-backend-platform-architecture.md)
|
||||
채팅 서버 아키텍처, VPS 배포 전략, 데이터/스토리지, 관측성, 백업, 확장 및 마이그레이션 전략
|
||||
2. [windows-desktop-client-architecture.md](windows-desktop-client-architecture.md)
|
||||
Windows-first 데스크톱 기술 스택 선정, WinUI 3 vs 대안 비교, 패키징, 오프라인 캐시, 알림, 보안 경계, 단계별 구현 전략
|
||||
3. [06-quality-release-and-launch.md](06-quality-release-and-launch.md)
|
||||
품질 전략, 테스트 계획, 릴리즈 게이트, 텔레메트리, 베타 운영안
|
||||
|
||||
이 폴더는 제품 기획 세트 중 실행/운영 관점 문서를 모아둔다. 백엔드, 플랫폼, 품질, 릴리즈, 운영 문서를 계속 추가한다.
|
||||
489
docs/archive/planning/windows-desktop-client-architecture.md
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
# Windows 데스크톱 클라이언트 기술/아키텍처 결정서
|
||||
|
||||
## 문서 목적
|
||||
|
||||
이 문서는 Windows PC 기준으로 먼저 개발하는 메신저 데스크톱 앱의 기술 스택과 애플리케이션 구조를 결정하기 위한 기준 문서다. 목표는 "카카오톡 PC에서 기대하는 속도, 상시 실행성, 익숙한 생산성"을 유지하면서도 더 세련된 UI와 안정적인 확장 구조를 확보하는 것이다.
|
||||
|
||||
범위는 데스크톱 클라이언트에 한정한다. 서버 프로토콜 상세, 운영 인프라, 모바일 클라이언트는 별도 문서에서 다룬다.
|
||||
|
||||
## 최종 권고안
|
||||
|
||||
Windows 1차 출시 기준 권고 조합은 아래와 같다.
|
||||
|
||||
- UI 프레임워크: `WinUI 3 + Windows App SDK`
|
||||
- 언어/런타임: `C# + .NET 8`
|
||||
- 아키텍처 패턴: `MVVM + 기능별 모듈 구조(feature-first)`
|
||||
- 상태 관리: `CommunityToolkit.Mvvm` 기반의 `ViewModel + Domain Store + Repository` 구조
|
||||
- 로컬 저장소: `SQLite` 중심의 오프라인 우선 캐시
|
||||
- 실시간 통신 추상화: `WebSocket` 기반 이벤트 스트림 + `HTTPS` 기반 명령/업로드 API
|
||||
- 패키징: `최종 배포는 MSIX 패키지드 앱`을 기본으로 하고, 내부 개발 루프에서는 필요 시 unpackaged 디버그 프로필 병행
|
||||
- 알림: `Windows toast notification + tray resident mode`
|
||||
- 자동 업데이트: `MSIX App Installer 업데이트 피드`
|
||||
- 비밀정보 저장: `Windows DPAPI/PasswordVault 계열 저장소`
|
||||
|
||||
핵심 판단은 단순하다. 이 프로젝트는 처음부터 Windows에서 가장 자연스럽게 느껴져야 하고, 메신저 특유의 "항상 켜져 있는 앱" 경험이 중요하다. 그 기준에서는 Electron보다 WinUI 3가 유리하고, WPF보다 미래 지향적이며, MAUI/Avalonia보다 Windows 통합 품질이 높다.
|
||||
|
||||
## WinUI 3 vs 대안 비교
|
||||
|
||||
### 1. WinUI 3
|
||||
|
||||
가장 균형이 좋다.
|
||||
|
||||
- 장점
|
||||
- Windows 11 감성에 가장 잘 맞는 기본 컨트롤, 타이포, 재질, 입력 체계를 바로 활용할 수 있다.
|
||||
- 알림, 앱 아이덴티티, 윈도우 관리, 테마 대응 같은 Windows 통합이 자연스럽다.
|
||||
- C#/.NET 생태계를 그대로 쓰면서 성능, 메모리, 유지보수 측면에서 Electron보다 유리하다.
|
||||
- 향후 Windows 전용 고급 기능을 붙일 때 우회가 적다.
|
||||
|
||||
- 단점
|
||||
- WPF보다 생태계와 레퍼런스가 덜 성숙했다.
|
||||
- 일부 고급 데스크톱 패턴, 특히 tray, title bar 세부 제어, 복잡한 virtualization 튜닝은 Win32 interop 이해가 필요하다.
|
||||
|
||||
### 2. WPF
|
||||
|
||||
실용성은 높지만 최종 권고안은 아니다.
|
||||
|
||||
- 장점
|
||||
- 데스크톱 안정성과 서드파티 생태계가 매우 강하다.
|
||||
- tray, 윈도우 제어, 업데이트, 커스텀 chrome 같은 데스크톱 전통 과제는 가장 익숙하게 풀 수 있다.
|
||||
|
||||
- 단점
|
||||
- 기본 인상이 오래되었고, "트렌디한 Windows 앱" 감성을 얻으려면 커스텀 비용이 커진다.
|
||||
- 지금 새로 시작하는 Windows 전용 메신저라면 장기 방향성에서 WinUI 3보다 매력이 약하다.
|
||||
|
||||
### 3. Electron
|
||||
|
||||
초기 생산성은 좋지만 이 프로젝트의 최적해는 아니다.
|
||||
|
||||
- 장점
|
||||
- 웹 인력 전환이 쉽고, 풍부한 UI 라이브러리를 바로 쓸 수 있다.
|
||||
- 크로스플랫폼 확장성이 좋다.
|
||||
|
||||
- 단점
|
||||
- 메신저처럼 항상 켜 두는 앱에서 메모리/배터리/네이티브 감성 측면 손해가 크다.
|
||||
- Windows 통합이 "가능"한 수준이지 "자연스러운 기본값"은 아니다.
|
||||
- 보안 경계 관리가 더 까다롭다.
|
||||
|
||||
### 4. Avalonia
|
||||
|
||||
기술적으로 괜찮지만 이번 목표와는 다르다.
|
||||
|
||||
- 장점
|
||||
- 크로스플랫폼을 염두에 둔 C# 선택지 중 가장 실전적이다.
|
||||
|
||||
- 단점
|
||||
- Windows 퍼스트 제품에서 필요한 OS 통합, 알림, 설치체험, 앱 정체성 측면은 WinUI 3보다 약하다.
|
||||
|
||||
### 5. .NET MAUI
|
||||
|
||||
권장하지 않는다.
|
||||
|
||||
- 장점
|
||||
- 모바일과 공유할 수 있는 발판은 있다.
|
||||
|
||||
- 단점
|
||||
- Windows 데스크톱 완성도가 핵심인 메신저에는 맞지 않는다.
|
||||
- 데스크톱 UX, 창 관리, 리스트 성능, 윈도우 느낌 모두 전용 데스크톱 프레임워크보다 불리하다.
|
||||
|
||||
## 패키징 선택
|
||||
|
||||
최종 결론은 `MSIX 패키지드 앱`이다.
|
||||
|
||||
이유는 아래와 같다.
|
||||
|
||||
- Windows 알림과 앱 아이덴티티가 안정적이다.
|
||||
- 설치/제거가 깔끔하고 잔여 파일 문제가 적다.
|
||||
- 시작 메뉴, 프로토콜 활성화, 알림 활성화, 권한 모델을 제품답게 다루기 좋다.
|
||||
- 장기적으로 베타/정식 배포 체계를 운영하기 쉽다.
|
||||
|
||||
다만 개발 단계에서는 두 가지 현실을 인정해야 한다.
|
||||
|
||||
- 내부 개발 속도만 보면 unpackaged 디버그가 더 편할 수 있다.
|
||||
- 비공개 테스트 배포에서 인증서/설치 체인이 번거로울 수 있다.
|
||||
|
||||
실행 전략은 이렇게 잡는다.
|
||||
|
||||
- 개발 초기: 개발기 로컬에서는 빠른 디버그 루프를 우선한다.
|
||||
- 비공개 알파 이후: 배포 검증은 반드시 MSIX 기준으로 한다.
|
||||
- 외부 배포 시점: MSIX + App Installer를 공식 릴리스 경로로 고정한다.
|
||||
|
||||
별도 exe 인스톨러 + 자체 업데이터 조합은 지금 단계에서는 피한다. 메신저 제품은 설치 체인보다 상시 안정성과 Windows 통합이 더 중요하다.
|
||||
|
||||
## 앱 셸 아키텍처
|
||||
|
||||
메신저는 일반 CRUD 앱이 아니다. 창이 곧 제품이다. 셸 설계가 곧 사용자 경험의 절반이다.
|
||||
|
||||
권장 구조는 `단일 메인 셸 + 필요 시 보조 창` 구조다.
|
||||
|
||||
- 메인 창
|
||||
- 좌측 고정 내비게이션
|
||||
- 채팅 목록
|
||||
- 선택된 대화 뷰
|
||||
- 우측 상세 패널은 필요 시 확장
|
||||
|
||||
- 보조 창
|
||||
- 이미지 뷰어
|
||||
- 파일 전송 상세
|
||||
- 설정
|
||||
- 로그인/계정 전환
|
||||
- 향후 통화 팝업
|
||||
|
||||
셸 원칙은 다음과 같다.
|
||||
|
||||
- 기본은 단일 창 경험으로 시작한다.
|
||||
- 대화창을 무제한 분리하는 기능은 초기 버전에 넣지 않는다.
|
||||
- 창을 여러 개 띄우는 순간 동기화, 포커스, 알림 중복, 메모리 문제가 같이 커진다.
|
||||
- 대신 "이미지 뷰어", "환경설정", "별도 팝업 업무창" 정도만 보조 창으로 분리한다.
|
||||
|
||||
탐색 방식은 페이지 네비게이션보다 `상태 전환형 셸`이 더 적합하다.
|
||||
|
||||
- 채팅 목록과 대화 뷰는 페이지 이동보다 같은 셸 내에서 컨텍스트만 바뀌는 구조가 낫다.
|
||||
- 좌측 메뉴는 `채팅`, `친구`, `오픈채널/서버형 공간(향후)`, `설정` 정도로 제한한다.
|
||||
- 메신저에서 과도한 딥 네비게이션은 UX를 해친다.
|
||||
|
||||
## 상태 관리 전략
|
||||
|
||||
권장 모델은 `가벼운 MVVM + 도메인 스토어`다.
|
||||
|
||||
전역 단일 상태 저장소 하나로 모든 것을 넣는 구조는 피한다. 메신저는 실시간 이벤트, 임시 입력 상태, 오프라인 큐, 동기화 상태가 얽혀서 거대 스토어가 금방 망가진다.
|
||||
|
||||
추천 계층은 아래와 같다.
|
||||
|
||||
- View
|
||||
- ViewModel
|
||||
- Feature Store
|
||||
- Repository
|
||||
- Transport/API Client
|
||||
- Local DB/Cache
|
||||
|
||||
각 계층의 책임은 다음과 같다.
|
||||
|
||||
- ViewModel
|
||||
- 화면 단위 상태만 가진다.
|
||||
- 선택된 채팅, 입력창 포커스, 필터 텍스트, 패널 열림 여부 같은 UI 상태를 담당한다.
|
||||
|
||||
- Feature Store
|
||||
- 기능 단위의 세션 상태를 가진다.
|
||||
- 예: `ChatListStore`, `ConversationStore`, `PresenceStore`, `NotificationStore`
|
||||
- 서버 이벤트와 로컬 변경을 합쳐 화면에 안정적으로 제공한다.
|
||||
|
||||
- Repository
|
||||
- 로컬 DB와 네트워크를 조합한다.
|
||||
- 예: "메시지 전송", "대화방 페이지 로드", "읽음 상태 반영", "첨부 업로드"를 트랜잭션처럼 묶는다.
|
||||
|
||||
전역 공유가 필요한 상태는 아래 정도로 제한한다.
|
||||
|
||||
- 현재 로그인 세션
|
||||
- 연결 상태
|
||||
- unread 총합
|
||||
- 활성 워크스페이스/계정
|
||||
- 공통 설정
|
||||
|
||||
## 로컬 캐시와 오프라인 동기화
|
||||
|
||||
메신저는 네트워크가 흔들려도 "쓸 수 있어 보이는 느낌"이 중요하다. 그래서 `SQLite를 단순 캐시가 아니라 로컬 소스 오브 트루스에 가깝게` 써야 한다.
|
||||
|
||||
권장 원칙은 아래와 같다.
|
||||
|
||||
- 앱 진입 시 채팅 목록은 로컬 DB에서 먼저 그린다.
|
||||
- 선택한 대화의 최근 메시지도 먼저 로컬에서 즉시 보여 준다.
|
||||
- 서버 응답은 그 뒤에 덮어쓰거나 이어 붙인다.
|
||||
- 전송 버튼을 누르면 서버 성공 전에도 로컬에 임시 메시지를 만들어 즉시 표시한다.
|
||||
- 서버 ack를 받으면 임시 ID를 서버 ID로 치환하고 상태를 정정한다.
|
||||
|
||||
로컬 DB에 최소한 있어야 하는 도메인은 아래와 같다.
|
||||
|
||||
- 계정/세션 메타데이터
|
||||
- 친구/프로필
|
||||
- 채팅방 목록
|
||||
- 채팅방 멤버십
|
||||
- 메시지 본문
|
||||
- 메시지 전송 상태
|
||||
- 읽음 상태
|
||||
- 첨부파일 메타데이터
|
||||
- 업로드/다운로드 작업 큐
|
||||
- 임시 저장 중인 draft
|
||||
- 사용자 설정
|
||||
|
||||
동기화는 `cursor 기반 점진 동기화`를 전제로 설계한다.
|
||||
|
||||
- 채팅 목록용 cursor
|
||||
- 대화방별 메시지 cursor
|
||||
- 읽음/반응/멤버 변경용 증분 이벤트
|
||||
|
||||
오프라인 큐에 들어갈 항목은 제한한다.
|
||||
|
||||
- 텍스트 전송
|
||||
- 읽음 상태 보고
|
||||
- 반응 추가/취소
|
||||
- 첨부 업로드 예약
|
||||
|
||||
계정 설정 변경이나 대규모 프로필 변경은 온라인 상태에서만 처리하는 편이 안전하다.
|
||||
|
||||
## 미디어 처리 전략
|
||||
|
||||
초기부터 "무거운 클라이언트"가 되지 않도록 범위를 통제해야 한다.
|
||||
|
||||
원칙은 서버가 할 수 있는 무거운 일은 서버에서 처리하고, 클라이언트는 표시와 경량 캐시에 집중하는 것이다.
|
||||
|
||||
권장 구성은 아래와 같다.
|
||||
|
||||
- 이미지
|
||||
- 원본과 썸네일을 분리 관리한다.
|
||||
- 리스트에서는 항상 썸네일/축소본만 사용한다.
|
||||
- 전체 보기 시에만 고해상도 리소스를 읽는다.
|
||||
|
||||
- 동영상
|
||||
- 채팅 리스트/대화창에는 포스터 프레임 중심으로 표시한다.
|
||||
- 초기 버전에서는 자동 재생을 금지한다.
|
||||
- 인라인 편집/트랜스코딩은 넣지 않는다.
|
||||
|
||||
- 파일
|
||||
- 다운로드 전에는 메타데이터만 유지한다.
|
||||
- 파일 열기는 OS 기본 연결 프로그램에 위임한다.
|
||||
- 악성 확장자/이중 확장자 표시는 UI에서 명확히 한다.
|
||||
|
||||
- 음성/녹음
|
||||
- 초기 MVP에는 제외하는 편이 낫다.
|
||||
- 넣더라도 녹음, 재생, 업로드를 별도 기능군으로 격리한다.
|
||||
|
||||
미디어 캐시는 계층화한다.
|
||||
|
||||
- 메모리 캐시: 현재 화면에 보이는 이미지
|
||||
- 디스크 캐시: 최근 본 이미지/썸네일
|
||||
- 영구 보관: 사용자가 저장한 다운로드 파일만
|
||||
|
||||
클라이언트에서 FFmpeg 같은 대형 의존성을 너무 일찍 넣지 않는다. 초기에는 서버 썸네일 생성 + 클라이언트 표시만으로 충분하다.
|
||||
|
||||
## 알림 전략
|
||||
|
||||
권장 조합은 `toast notification + 앱 내부 배너 + tray unread 표시`다.
|
||||
|
||||
필수 시나리오는 아래와 같다.
|
||||
|
||||
- 앱이 최소화되어 있거나 뒤에 있을 때 새 메시지 toast
|
||||
- 앱이 포그라운드일 때는 toast 대신 인앱 배너 또는 해당 대화 강조
|
||||
- 알림 클릭 시 해당 대화로 정확히 이동
|
||||
- 중복 toast 방지
|
||||
- mute된 방, 현재 보고 있는 방, 조용한 시간대 예외 처리
|
||||
|
||||
메신저는 "창 닫기 = 종료"보다 "창 닫기 = tray 상주"가 더 자연스럽다.
|
||||
|
||||
그래서 초기 설계부터 아래를 반영한다.
|
||||
|
||||
- 우상단 X 클릭 시 기본 동작을 `tray 최소화`로 둘지 정책 결정
|
||||
- 완전 종료는 tray 메뉴 또는 설정에서 명시적으로 실행
|
||||
- 1회성 안내를 통해 사용자가 동작을 오해하지 않게 한다
|
||||
|
||||
tray 기능은 보조 요소가 아니라 핵심 상시성 UX다.
|
||||
|
||||
## 자동 업데이트
|
||||
|
||||
정식 경로는 `MSIX App Installer 업데이트 피드`가 가장 낫다.
|
||||
|
||||
추천 이유는 아래와 같다.
|
||||
|
||||
- Windows 친화적인 업데이트 흐름을 유지할 수 있다.
|
||||
- 설치 파일과 업데이트 정책이 한 체계 안에 있다.
|
||||
- 알파, 베타, 스테이블 채널을 분리하기 좋다.
|
||||
|
||||
운영 원칙은 아래처럼 잡는다.
|
||||
|
||||
- `dev`, `beta`, `stable` 세 채널 분리
|
||||
- 앱 시작 시 무조건 업데이트하지 않고, 유휴 상태나 재시작 시점 반영
|
||||
- 강제 업데이트는 인증/프로토콜 호환성 깨질 때만 사용
|
||||
|
||||
업데이터를 별도 커스텀 프로세스로 처음부터 만들지 않는다. 메신저 본체가 안정화되기 전까지는 관리 포인트만 늘어난다.
|
||||
|
||||
## 보안 경계
|
||||
|
||||
메신저 데스크톱 앱은 생각보다 공격면이 넓다. 첨부파일, 링크 프리뷰, 알림 활성화 인자, 로컬 캐시, 토큰 저장이 다 경계다.
|
||||
|
||||
반드시 지킬 기준은 아래와 같다.
|
||||
|
||||
- 인증 토큰은 평문 파일로 저장하지 않는다.
|
||||
- refresh token이나 세션 비밀값은 OS 보호 저장소에 넣는다.
|
||||
- 로컬 SQLite에는 필요한 최소 데이터만 저장한다.
|
||||
- 서버에서 온 HTML/리치 콘텐츠를 앱 내부에서 임의 렌더링하지 않는다.
|
||||
- 첨부파일은 "열기"와 "미리보기"의 경계를 분리한다.
|
||||
- 로그에는 메시지 본문, 토큰, 첨부 URL 원문을 남기지 않는다.
|
||||
- 크래시 리포트에도 PII 최소화 규칙을 둔다.
|
||||
|
||||
프로세스 경계는 초기에 단순하게 가져간다.
|
||||
|
||||
- 기본은 단일 앱 프로세스
|
||||
- 정말 필요할 때만 보조 프로세스 분리
|
||||
|
||||
보조 프로세스를 고려할 만한 경우는 아래다.
|
||||
|
||||
- 대용량 미디어 전처리
|
||||
- 별도 업데이트/복구 헬퍼
|
||||
- 향후 화면 공유/통화 같은 고권한 기능
|
||||
|
||||
초기 MVP에서 브라우저 엔진 기반 렌더링, 스크립트 실행형 플러그인, 임의 확장 기능은 넣지 않는다.
|
||||
|
||||
## IPC와 백그라운드 작업
|
||||
|
||||
초기 구조에서 필요한 IPC는 제한적이다.
|
||||
|
||||
- 단일 인스턴스 유지
|
||||
- 두 번째 실행 요청이 오면 기존 인스턴스로 포커스 이동
|
||||
- 프로토콜 링크 또는 알림 활성화 인자를 기존 인스턴스로 전달
|
||||
|
||||
이 용도에는 `로컬 named pipe` 수준이면 충분하다.
|
||||
|
||||
백그라운드 작업은 "모바일 같은 진짜 background task"보다 `tray 상주 앱` 개념으로 접근하는 것이 맞다.
|
||||
|
||||
즉, 메신저 수신과 동기화는 아래를 기본 전제로 삼는다.
|
||||
|
||||
- 사용자가 로그아웃하지 않은 이상 앱은 백그라운드에서 살아 있다
|
||||
- 창만 닫혀도 프로세스는 유지된다
|
||||
- 연결이 끊기면 지수 백오프로 재연결한다
|
||||
|
||||
Windows의 백그라운드 태스크 모델을 과용하지 않는다. 데스크톱 메신저에서는 주 실행 프로세스 상주가 더 단순하고 신뢰성이 높다.
|
||||
|
||||
## 권장 폴더 구조
|
||||
|
||||
프로젝트는 `레이어만 예쁘게 나눈 구조`보다 `기능 단위로 이해되는 구조`가 유지보수에 낫다.
|
||||
|
||||
권장 예시는 아래와 같다.
|
||||
|
||||
- `src/App`
|
||||
- 앱 진입점, 부트스트랩, 셸, 테마, 리소스, 공통 네비게이션
|
||||
|
||||
- `src/Features/Auth`
|
||||
- 로그인, 세션 복구, 계정 전환
|
||||
|
||||
- `src/Features/ChatList`
|
||||
- 채팅 목록, unread, 고정, 검색 진입
|
||||
|
||||
- `src/Features/Conversation`
|
||||
- 메시지 목록, 입력창, 전송, 읽음 상태, 반응
|
||||
|
||||
- `src/Features/Contacts`
|
||||
- 친구 목록, 프로필, 차단/숨김
|
||||
|
||||
- `src/Features/Notifications`
|
||||
- toast, 배너, 알림 정책
|
||||
|
||||
- `src/Features/Settings`
|
||||
- 일반, 알림, 파일, 계정, 실험 기능
|
||||
|
||||
- `src/Core`
|
||||
- 공용 도메인 모델, 인터페이스, 결과 타입, 에러 규약
|
||||
|
||||
- `src/Infrastructure/Api`
|
||||
- HTTP/WebSocket 클라이언트, DTO, 직렬화
|
||||
|
||||
- `src/Infrastructure/Persistence`
|
||||
- SQLite, 마이그레이션, 리포지토리 구현
|
||||
|
||||
- `src/Infrastructure/Media`
|
||||
- 썸네일, 다운로드, 임시 파일, 프리뷰
|
||||
|
||||
- `src/Infrastructure/Security`
|
||||
- 토큰 저장, 암호화, 민감정보 정책
|
||||
|
||||
- `src/Infrastructure/Platform`
|
||||
- tray, 프로토콜 활성화, 파일 연결, OS 통합
|
||||
|
||||
- `tests/Unit`
|
||||
- ViewModel, Store, 도메인 규칙
|
||||
|
||||
- `tests/Integration`
|
||||
- 로컬 DB, 동기화, 업로드/다운로드, reconnect 시나리오
|
||||
|
||||
핵심은 `Features`와 `Infrastructure`를 명확히 분리하는 것이다. 채팅 기능 코드가 SQLite 세부 구현이나 WebSocket payload 형태를 직접 알게 만들면 나중에 반드시 꼬인다.
|
||||
|
||||
## 단계별 구현 전략
|
||||
|
||||
### Phase 0. 제품 골격 검증
|
||||
|
||||
목표는 "이 구조로 메신저 느낌이 나는가"만 확인하는 것이다.
|
||||
|
||||
- 로그인 화면
|
||||
- 메인 셸
|
||||
- 채팅 목록 mock
|
||||
- 대화창 mock
|
||||
- 다크/라이트 테마
|
||||
- 창 최소화, 리사이즈, 기본 반응형
|
||||
|
||||
이 단계에서는 서버 연결보다 셸 품질을 먼저 본다.
|
||||
|
||||
### Phase 1. 핵심 메시징 MVP
|
||||
|
||||
실사용 가능한 최소 메신저를 만든다.
|
||||
|
||||
- 계정 로그인/세션 복구
|
||||
- 채팅 목록 실데이터
|
||||
- 1:1 채팅
|
||||
- 텍스트 송수신
|
||||
- 읽음 상태
|
||||
- reconnect
|
||||
- 로컬 캐시 부팅
|
||||
|
||||
여기서 가장 중요한 성공 조건은 "앱 재실행 후 바로 이전 대화가 보여야 한다"다.
|
||||
|
||||
### Phase 2. 데스크톱다운 완성도 확보
|
||||
|
||||
카카오톡 PC 대체감을 만드는 구간이다.
|
||||
|
||||
- tray 상주
|
||||
- toast 알림
|
||||
- 파일 첨부/다운로드
|
||||
- 전송 실패 재시도
|
||||
- draft 저장
|
||||
- 검색 진입
|
||||
- 채팅방 정렬/고정
|
||||
|
||||
이 단계에서 제품 체감 품질이 크게 올라간다.
|
||||
|
||||
### Phase 3. 성능과 운영성 강화
|
||||
|
||||
- 대화방 수천 개, 메시지 수만 건에서도 부드러운 스크롤
|
||||
- 이미지 캐시 최적화
|
||||
- 크래시 리포트
|
||||
- 진단 로그
|
||||
- 업데이트 채널 운영
|
||||
- 설정 백업/복원 범위 정의
|
||||
|
||||
이 단계 없이 사용자 수를 늘리면 운영이 무너질 가능성이 높다.
|
||||
|
||||
### Phase 4. 고급 기능 확장
|
||||
|
||||
이 단계는 코어가 안정화된 뒤에만 들어간다.
|
||||
|
||||
- 멀티 계정
|
||||
- 그룹 채팅 고급 기능
|
||||
- 메시지 반응
|
||||
- 파일 히스토리
|
||||
- 통화/화면공유
|
||||
- 관리자/조직 기능
|
||||
|
||||
초기부터 이 기능을 욕심내면 아키텍처는 커 보이는데 제품은 느리고 불안정해진다.
|
||||
|
||||
## 반드시 미루는 항목
|
||||
|
||||
초기 버전에서 제외하는 편이 좋은 항목들이다.
|
||||
|
||||
- 플러그인 시스템
|
||||
- 내장 브라우저 기반 리치 콘텐츠 렌더링
|
||||
- 과한 애니메이션
|
||||
- 멀티 윈도우 대화창 자유 분리
|
||||
- 클라이언트 측 대형 미디어 트랜스코딩
|
||||
- 암호화 메신저 수준의 종단간암호화 복잡도
|
||||
|
||||
이런 요소는 제품 핵심이 아니라 복잡도 폭탄이 되기 쉽다.
|
||||
|
||||
## 최종 판단
|
||||
|
||||
이 프로젝트의 최적 출발점은 `WinUI 3 + .NET 8 + MVVM + SQLite 기반 오프라인 우선 구조 + MSIX 배포`다.
|
||||
|
||||
이 조합은 세련된 Windows UX, 메신저에 필요한 상시성, 실시간성, 설치 품질, 장기 유지보수의 균형이 가장 좋다.
|
||||
|
||||
정리하면 아래 네 가지를 흔들지 않는 것이 중요하다.
|
||||
|
||||
- Windows 전용 1차 제품답게 네이티브 경험을 우선한다.
|
||||
- 로컬 캐시는 옵션이 아니라 제품 핵심으로 본다.
|
||||
- tray, 알림, 재연결은 부가 기능이 아니라 메신저의 본체로 취급한다.
|
||||
- 초기에 단일 창과 단순 프로세스 구조를 유지해 제품을 먼저 안정화한다.
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
# Windows-first 메신저 시각/인터랙션 설계 방향
|
||||
|
||||
## 1. 디자인 테제
|
||||
|
||||
이 제품의 목표는 "카카오톡 PC의 즉시성, 익숙한 효율, 낮은 진입장벽"은 유지하면서, Windows 데스크톱 환경에서 더 차분하고 세련되며 오래 써도 피로하지 않은 메시징 경험을 제공하는 것이다.
|
||||
|
||||
핵심 방향은 다음과 같다.
|
||||
|
||||
- 한눈에 읽히는 정보 밀도: 화면을 넓게 쓰는 PC 환경에서 정보량은 충분히 담되 답답하거나 복잡해 보이지 않아야 한다.
|
||||
- 즉시 조작 가능한 구조: 클릭 한두 번 안에 이동, 검색, 파일 전송, 대화 전환, 알림 확인이 끝나야 한다.
|
||||
- 차분한 고급감: 과한 장식 대신 균형 잡힌 비율, 재질감, 미세한 움직임, 명확한 위계를 통해 프리미엄 감각을 만든다.
|
||||
- Windows 네이티브 감성: 타이틀 바, 창 상태, 포커스, 컨텍스트 메뉴, 단축키, 다중 창 사용 방식에서 데스크톱 소프트웨어다워야 한다.
|
||||
- 장시간 사용 최적화: 업무 중 하루 종일 켜두는 앱이라는 전제 하에 대비, 타이포, 상태 표현, 알림 피로도를 설계한다.
|
||||
|
||||
한 문장으로 정리하면 다음과 같다.
|
||||
|
||||
`친숙한 메신저 구조 위에, 생산성과 심미성이 정교하게 올라간 Windows용 프리미엄 데스크톱 메신저`
|
||||
|
||||
## 2. 데스크톱 정보구조
|
||||
|
||||
기본 정보구조는 3단 구성을 중심으로 설계한다.
|
||||
|
||||
- 1단: 좌측 글로벌 내비게이션
|
||||
- 2단: 리스트 영역
|
||||
- 3단: 메인 콘텐츠 영역
|
||||
|
||||
필요 시 우측 보조 패널이 열리는 4단 구조까지 확장한다.
|
||||
|
||||
상위 메뉴는 다음 정도로 제한한다.
|
||||
|
||||
- 채팅
|
||||
- 친구 또는 연락처
|
||||
- 알림
|
||||
- 보관함 또는 자료함
|
||||
- 설정
|
||||
|
||||
기본 진입점은 `채팅`이다. 메신저의 중심 과업이 대화라는 점을 흐리지 않는다. 친구 목록과 기타 기능은 중요하지만 1차 목적을 방해하지 않도록 한 단계 낮은 밀도로 둔다.
|
||||
|
||||
권장 구조는 다음과 같다.
|
||||
|
||||
- 좌측 아이콘 바: 전역 기능 전환
|
||||
- 중앙 리스트 패널: 채팅방, 친구, 알림, 검색 결과 등 현재 선택된 전역 기능의 목록
|
||||
- 우측 메인 패널: 대화 스레드, 친구 상세, 알림 상세, 설정 화면
|
||||
- 필요 시 우측 인스펙터 패널: 참여자 목록, 첨부 파일, 링크, 검색 내역, 채팅방 정보
|
||||
|
||||
이 구조의 장점은 카카오톡 PC 사용자의 정신 모델을 크게 벗어나지 않으면서, Slack이나 Discord류 데스크톱 협업 앱처럼 "맥락 패널"을 유연하게 붙일 수 있다는 점이다.
|
||||
|
||||
## 3. 셸과 내비게이션
|
||||
|
||||
### 3.1 앱 셸 원칙
|
||||
|
||||
- Windows 타이틀 바와 자연스럽게 어울리는 상단 영역을 설계한다.
|
||||
- 커스텀 크롬을 쓰더라도 창 이동, 스냅, 최대화, 최소화, 시스템 메뉴 등 기본 OS 동작을 해치지 않는다.
|
||||
- 상단은 과하게 비워두지 말고, 검색, 빠른 실행, 현재 상태 같은 핵심 기능을 밀도 있게 담는다.
|
||||
|
||||
권장 셸 구성은 다음과 같다.
|
||||
|
||||
- 상단 바: 앱명 또는 현재 컨텍스트, 통합 검색, 빠른 새 채팅, 사용자 상태/프로필, 창 컨트롤
|
||||
- 좌측 내비게이션 레일: 아이콘 중심, 선택 상태가 강하게 드러나는 구조
|
||||
- 중앙 작업 영역: 콘텐츠 중심
|
||||
|
||||
### 3.2 내비게이션 패턴
|
||||
|
||||
- 전역 전환은 좌측 레일에 고정한다.
|
||||
- 각 전역 섹션 안의 세부 이동은 중앙 리스트에서 해결한다.
|
||||
- 우측 패널은 탐색이 아니라 "맥락 확장" 용도로만 쓴다.
|
||||
- 브레드크럼은 사용하지 않는다. 메신저에서는 깊은 계층보다 현재 컨텍스트 표시가 중요하다.
|
||||
|
||||
### 3.3 검색 경험
|
||||
|
||||
검색은 데스크톱 메신저의 핵심 경쟁력이다. 상단 통합 검색은 다음을 지원해야 한다.
|
||||
|
||||
- 채팅방 검색
|
||||
- 사람 검색
|
||||
- 메시지 본문 검색
|
||||
- 파일/링크 검색
|
||||
- 최근 검색
|
||||
|
||||
검색 UI는 단순 입력창이 아니라 커맨드 팔레트와 리스트 검색의 중간 형태가 좋다. 입력 즉시 그룹화된 결과가 드롭다운으로 뜨고, 방향키와 엔터로 이동 가능해야 한다.
|
||||
|
||||
## 4. 리스트, 채팅, 디테일 패널 동작
|
||||
|
||||
## 4.1 리스트 패널
|
||||
|
||||
리스트는 단순 나열이 아니라 생산성 도구여야 한다.
|
||||
|
||||
필수 항목 구성
|
||||
- 아바타
|
||||
- 이름
|
||||
- 마지막 메시지 미리보기
|
||||
- 시간
|
||||
- 읽지 않음 배지
|
||||
- 고정 또는 알림 해제 상태
|
||||
|
||||
동작 원칙
|
||||
- 행 높이는 너무 좁지 않게 설정하되, 기본 밀도는 "업무용으로 충분히 빽빽한 수준"을 유지한다.
|
||||
- 마우스 호버 시 보조 액션이 은근히 드러난다.
|
||||
- 우클릭 컨텍스트 메뉴가 풍부해야 한다.
|
||||
- 드래그 앤 드롭으로 고정 순서 변경, 파일 전송, 창 분리 같은 데스크톱 행동을 적극 수용한다.
|
||||
|
||||
상태 표현
|
||||
- 선택 상태는 배경색 + 얇은 강조선 + 타이포 웨이트 조합으로 표현한다.
|
||||
- 읽지 않음은 색 하나에만 의존하지 말고 배지, 굵기, 프리뷰 톤까지 조합한다.
|
||||
- 음소거/보관/고정 상태는 아이콘 크기를 작게 유지해 시각적 소음을 줄인다.
|
||||
|
||||
### 4.2 채팅 패널
|
||||
|
||||
채팅 패널은 이 제품의 중심이다. "읽기 쉬움"과 "입력 흐름의 부드러움"이 최우선이다.
|
||||
|
||||
기본 구조
|
||||
- 상단 헤더: 채팅방 이름, 참여자 수, 상태, 검색, 통화/추가 액션
|
||||
- 메시지 영역: 타임라인
|
||||
- 하단 입력 영역: 입력창, 첨부, 이모지, 전송, 보조 액션
|
||||
|
||||
권장 동작
|
||||
- 새 메시지가 들어와도 사용자가 과거 메시지를 읽는 중이면 자동 하단 점프를 강요하지 않는다.
|
||||
- "새 메시지 n개" 점프 배너를 제공한다.
|
||||
- 날짜 구분선, 읽음 기준선, 시스템 메시지는 메시지 버블보다 한 단계 낮은 시각 강도로 표현한다.
|
||||
- 메시지 선택 모드, 멀티 선택, 복사/전달/삭제 등 PC다운 조작을 지원한다.
|
||||
- 긴 대화에서 스크롤 성능과 가상화 품질을 매우 높게 가져간다.
|
||||
|
||||
메시지 레이아웃 원칙
|
||||
- 내 메시지와 상대 메시지는 명확히 분리하되, 카카오톡처럼 지나치게 장난스럽지 않게 정제한다.
|
||||
- 버블 최대 폭은 넓은 모니터에서도 읽기 좋은 선에서 제한한다.
|
||||
- 텍스트, 이미지, 파일, 답장, 링크 프리뷰, 시스템 공지의 규칙이 일관돼야 한다.
|
||||
- 연속 메시지는 아바타와 여백을 절약해 리듬감을 준다.
|
||||
|
||||
입력창 원칙
|
||||
- 한 줄 입력을 기본으로 하되 여러 줄로 자연스럽게 확장된다.
|
||||
- 첨부, 캡처, 파일 끌어놓기, 클립보드 이미지 붙여넣기 등 PC 특화 행동을 막지 않는다.
|
||||
- 입력 보조 버튼이 너무 많아져 웹 툴바처럼 보이지 않게 한다.
|
||||
|
||||
### 4.3 디테일 패널
|
||||
|
||||
우측 디테일 패널은 선택적으로 열리고 닫혀야 한다.
|
||||
|
||||
용도
|
||||
- 채팅방 정보
|
||||
- 참여자
|
||||
- 공유 파일
|
||||
- 링크
|
||||
- 미디어
|
||||
- 대화 내 검색 결과
|
||||
|
||||
원칙
|
||||
- 항상 고정 노출하지 않는다.
|
||||
- 메인 채팅 공간을 잠식하지 않도록 기본 너비를 절제한다.
|
||||
- 탭이 많아지면 세그먼트드 컨트롤 또는 상단 탭으로 정리한다.
|
||||
|
||||
## 5. 타이포그래피, 컬러, 머티리얼
|
||||
|
||||
### 5.1 타이포그래피
|
||||
|
||||
Windows 데스크톱에서는 가독성과 밀도가 핵심이다. 기본 방향은 다음과 같다.
|
||||
|
||||
- 기본 UI 폰트는 Windows 환경과 잘 맞는 높은 가독성의 산세리프를 사용한다.
|
||||
- 채팅 본문과 리스트 본문은 숫자와 한글, 영문 혼합 시 안정적인 폰트를 우선한다.
|
||||
- 제목용 폰트와 본문용 폰트의 역할을 지나치게 벌리지 않는다.
|
||||
|
||||
타이포 위계 예시
|
||||
- 앱/섹션 타이틀: 강한 존재감, 그러나 과장되지 않음
|
||||
- 리스트 제목/채팅방명: 명확한 식별성
|
||||
- 본문: 장시간 읽기에 최적화
|
||||
- 메타 정보: 시간, 상태, 보조 정보는 한 단계 낮은 대비
|
||||
|
||||
디자인 느낌은 "날카로운 스타트업 툴"보다 "매끈한 프리미엄 커뮤니케이션 앱" 쪽이 적합하다.
|
||||
|
||||
### 5.2 컬러
|
||||
|
||||
카카오톡의 노란색 아이덴티티를 직접 복제하는 대신, 다음 방식이 더 좋다.
|
||||
|
||||
- 기본 베이스는 뉴트럴 톤 위주
|
||||
- 액센트 컬러는 한 가지 메인 색으로 통일
|
||||
- 읽지 않음, 성공, 주의, 오류는 명확히 분리
|
||||
|
||||
권장 컬러 전략
|
||||
- 배경: 완전한 흰색보다 미세하게 따뜻하거나 차가운 오프 화이트
|
||||
- 패널 분리: 얇은 구분선보다 톤 차와 표면 재질감 중심
|
||||
- 액센트: 채팅 전송, 선택 상태, 활성 탭, 주요 CTA에 일관되게 사용
|
||||
- 경고/오류: 과포화 붉은색 남용 금지
|
||||
|
||||
트렌디함은 강한 원색이 아니라, 절제된 중립 팔레트 위에 정확한 강조색을 올리는 방식에서 나온다.
|
||||
|
||||
### 5.3 머티리얼과 표면
|
||||
|
||||
웹 카드 UI처럼 모든 것을 박스로 자르지 않는다.
|
||||
|
||||
권장 원칙
|
||||
- 좌우 패널은 표면 레이어 차이로 구분
|
||||
- 모달, 팝오버, 컨텍스트 메뉴는 Windows 특유의 얇은 재질감과 깊이감 사용
|
||||
- 메시지 버블은 과도하게 둥글거나 젤리처럼 보이지 않게 조절
|
||||
- 반투명, 블러, 그림자는 최소한으로 정교하게 사용
|
||||
|
||||
느낌은 "플랫 + 아주 얕은 입체감"이 적합하다. 과한 유리 효과는 피하고, 정보가 또렷하게 보이는 것을 우선한다.
|
||||
|
||||
## 6. 모션 원칙
|
||||
|
||||
모션은 눈에 띄기보다 사용 맥락을 설명해야 한다.
|
||||
|
||||
핵심 원칙
|
||||
- 짧고 정확한 응답
|
||||
- 방향성이 있는 전환
|
||||
- 상태 변화 설명
|
||||
- 방해하지 않는 부드러움
|
||||
|
||||
권장 모션
|
||||
- 리스트 항목 선택: 짧은 배경 전이
|
||||
- 패널 열림/닫힘: 수평 슬라이드 + 페이드
|
||||
- 새 메시지 도착: 미세한 페이드/슬라이드
|
||||
- 토스트/배너: 아래 또는 우상단에서 짧게 등장
|
||||
- 검색 결과 표시: 즉시성 중심, 과한 애니메이션 금지
|
||||
|
||||
피해야 할 것
|
||||
- 모든 클릭에 바운스
|
||||
- 과장된 스프링
|
||||
- 모바일 앱 같은 통통 튀는 반응
|
||||
- 긴 페이드로 인한 느린 인상
|
||||
|
||||
## 7. 상태 설계
|
||||
|
||||
데스크톱 메신저는 상태 수가 많다. 상태 표현은 미세하지만 분명해야 한다.
|
||||
|
||||
필수 상태
|
||||
- 기본
|
||||
- 호버
|
||||
- 포커스
|
||||
- 활성
|
||||
- 선택
|
||||
- 비활성
|
||||
- 로딩
|
||||
- 동기화 중
|
||||
- 오프라인
|
||||
- 오류
|
||||
- 전송 중
|
||||
- 업로드 중
|
||||
- 읽음/안 읽음
|
||||
- 입력 중
|
||||
- 방해 금지
|
||||
|
||||
상태 표현 원칙
|
||||
- 색만으로 상태를 구분하지 않는다.
|
||||
- 리스트와 채팅, 버튼과 입력창 전반에서 상태 언어를 통일한다.
|
||||
- 로딩은 스켈레톤과 점진 표시를 기본으로 하고, 의미 없는 무한 스피너 남용을 피한다.
|
||||
|
||||
## 8. 온보딩
|
||||
|
||||
온보딩은 "설명"보다 "즉시 사용"을 목표로 설계한다.
|
||||
|
||||
권장 흐름
|
||||
1. 첫 실행
|
||||
2. 로그인 또는 계정 생성
|
||||
3. 프로필 기본 설정
|
||||
4. 연락처/친구 연결 또는 건너뛰기
|
||||
5. 첫 채팅 시작 유도
|
||||
6. 알림, 시작 프로그램, 트레이 최소화 여부 등 PC 친화 옵션 제안
|
||||
|
||||
온보딩 원칙
|
||||
- 초기 단계 수를 최소화한다.
|
||||
- 권한 요청은 필요한 순간에만 한다.
|
||||
- 데스크톱 메신저답게 "항상 켜두는 앱" 설정을 자연스럽게 안내한다.
|
||||
- 빈 상태 화면이 첫 대화를 열도록 적극적으로 돕는다.
|
||||
|
||||
빈 상태 화면에서 추천할 행동
|
||||
- 새 대화 시작
|
||||
- 연락처 가져오기
|
||||
- 테스트 채팅방 입장
|
||||
- 파일을 끌어 대화 시작하기
|
||||
|
||||
## 9. 접근성
|
||||
|
||||
이 프로젝트는 트렌디함보다 완성도를 우선해야 한다. 접근성은 부가 기능이 아니라 품질 기준이다.
|
||||
|
||||
필수 기준
|
||||
- 키보드만으로 주요 흐름 수행 가능
|
||||
- 포커스 링 명확
|
||||
- 충분한 명도 대비
|
||||
- 축소/확대 환경에서 레이아웃 붕괴 방지
|
||||
- 스크린 리더용 의미 구조 설계
|
||||
- 모션 축소 옵션 제공
|
||||
|
||||
특히 중요한 항목
|
||||
- 채팅 리스트와 메시지 타임라인의 키보드 탐색
|
||||
- 읽지 않음/전송 실패/입력 중 상태의 비색상 표현
|
||||
- 작은 배지, 시간, 상태 아이콘의 저시력 대응
|
||||
- 한글/영문/숫자 혼합 가독성
|
||||
|
||||
## 10. 카카오톡 PC에서 유지할 것
|
||||
|
||||
완전히 새롭게 만들기보다, 사용자가 무의식적으로 기대하는 패턴은 유지하는 편이 좋다.
|
||||
|
||||
- 좌측 중심의 익숙한 섹션 전환 구조
|
||||
- 채팅 리스트 우선 진입
|
||||
- 읽지 않은 채팅을 빠르게 훑는 흐름
|
||||
- PC다운 우클릭 중심 조작
|
||||
- 가벼운 창 크기 조절과 빠른 실행감
|
||||
- 과도한 학습 없이 바로 채팅 가능한 구조
|
||||
|
||||
즉, 사용자는 "낯설지 않다"고 느껴야 하고, 동시에 "왜 이게 더 좋지?"라고 느껴야 한다.
|
||||
|
||||
## 11. 현대화할 것
|
||||
|
||||
다음 요소는 확실히 현대화하는 것이 좋다.
|
||||
|
||||
- 더 정교한 정보 밀도와 위계
|
||||
- 우측 맥락 패널의 도입
|
||||
- 검색을 단순 필터가 아닌 핵심 기능으로 승격
|
||||
- 메시지 타입별 시각 시스템 정교화
|
||||
- 반응성 높은 다중 상태 설계
|
||||
- 디스플레이 스케일링과 고해상도 환경 대응
|
||||
- 다크 모드가 아니라, 먼저 "좋은 라이트 모드" 완성 후 테마 확장
|
||||
- 알림, 배지, 토스트를 세련되게 통합
|
||||
|
||||
추가로 현대화할 가치가 높은 부분
|
||||
- 멀티 윈도우 또는 팝아웃 채팅
|
||||
- 드래그 앤 드롭 UX
|
||||
- 최근 파일/링크/미디어 탐색
|
||||
- 검색 기반 이동
|
||||
- 키보드 단축키 체계
|
||||
|
||||
## 12. 피해야 할 안티패턴
|
||||
|
||||
- 웹 메신저를 데스크톱 창 안에 얹은 듯한 넓은 여백과 느린 반응
|
||||
- 모바일 UI를 단순 확대해 붙인 큰 버튼과 과도한 둥근 모서리
|
||||
- 카드 남용으로 인한 조각난 정보 구조
|
||||
- 색과 그림자 과다 사용
|
||||
- 기능은 많은데 기본 채팅 흐름이 무거워지는 구조
|
||||
- 입력창 툴바에 기능을 끝없이 누적하는 방식
|
||||
- 알림, 배지, 강조색이 동시에 소리치는 시각 언어
|
||||
- 다크 모드에서만 그럴듯하고 라이트 모드가 약한 설계
|
||||
- 윈도우 스냅, 다중 모니터, DPI 스케일링을 고려하지 않은 레이아웃
|
||||
- 상태 피드백이 늦어 사용자가 불안해지는 인터랙션
|
||||
|
||||
## 13. 최종 비주얼 톤 제안
|
||||
|
||||
가장 적합한 톤은 다음 세 가지 키워드로 정리된다.
|
||||
|
||||
- 정제됨
|
||||
- 민첩함
|
||||
- 오래 써도 질리지 않음
|
||||
|
||||
무드는 "밝은 생산성 툴"과 "프리미엄 개인 메신저"의 중간 지점이 좋다. 지나치게 기업 협업 도구처럼 딱딱하지도 않고, 반대로 캐주얼 메신저처럼 장난스럽지도 않아야 한다.
|
||||
|
||||
비주얼 방향을 한 줄로 요약하면 다음과 같다.
|
||||
|
||||
`카카오톡 PC의 익숙한 작업 흐름을 유지하되, Windows 11 시대의 고급 생산성 앱 수준으로 정밀하게 다듬은 메신저`
|
||||
|
||||
## 14. 디자인 의사결정 체크리스트
|
||||
|
||||
새 화면이나 컴포넌트를 만들 때마다 다음 질문으로 검증한다.
|
||||
|
||||
- 이 요소는 채팅이라는 핵심 과업을 더 빠르게 만드는가
|
||||
- Windows 데스크톱에서 자연스러운가
|
||||
- 장시간 사용 시 피로를 줄이는가
|
||||
- 색 없이도 상태를 이해할 수 있는가
|
||||
- 마우스와 키보드 모두에서 효율적인가
|
||||
- 익숙함을 해치지 않으면서도 더 세련된가
|
||||
- 웹 앱처럼 보이지 않는가
|
||||
|
||||
이 체크리스트를 통과하지 못하면 화려해 보여도 채택하지 않는다.
|
||||
11
docs/archive/windows-messenger-planning/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Windows Messenger Planning Set
|
||||
|
||||
이 폴더는 Windows PC 기준 개인 사이드 프로젝트 메신저의 기획 문서 세트를 모아두는 공간이다.
|
||||
|
||||
문서 목록
|
||||
- `01-visual-interaction-direction.md`: 카카오톡 PC 사용성을 참고하되 더 현대적인 Windows 데스크톱 메신저로 재해석한 시각/인터랙션 설계 방향
|
||||
|
||||
문서 작성 원칙
|
||||
- 웹 앱을 데스크톱 셸에 억지로 넣은 느낌이 아니라, Windows 데스크톱 소프트웨어다운 밀도와 반응성을 우선한다.
|
||||
- 익숙함과 차별화를 동시에 가져간다. 학습 비용은 낮추고, 완성도는 명확하게 높인다.
|
||||
- 설계 문서는 구현 이전의 의사결정 기준서 역할을 하며, 추후 제품/기술/브랜드 문서와 연결된다.
|
||||
14
docs/assets/latest/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Latest Screenshots
|
||||
|
||||
이 디렉터리는 원격 저장소에서도 바로 확인할 수 있는 `최신 기준 제품 스크린샷`을 보관합니다.
|
||||
|
||||
규칙:
|
||||
|
||||
- README는 이 경로의 이미지를 직접 참조합니다.
|
||||
- 새 릴리즈나 큰 UI 변경이 있으면 이 폴더의 스크린샷도 함께 갱신합니다.
|
||||
- 릴리즈 번들용 스크린샷과 별개로, 저장소 안의 최신 기준 화면을 유지합니다.
|
||||
- 모바일 웹 스크린샷은 `scripts/ci/capture-vstalk-web-screenshots.cjs`로 다시 생성할 수 있습니다.
|
||||
- 현재 포함:
|
||||
- Windows 데스크톱 셸
|
||||
- Windows 온보딩/대화 화면
|
||||
- `vstalk` 모바일 웹 온보딩/목록/검색/보관/대화 화면
|
||||
BIN
docs/assets/latest/conversation.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/assets/latest/hero-shell.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
docs/assets/latest/onboarding.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/assets/latest/vstalk-web-chat.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/assets/latest/vstalk-web-list.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/assets/latest/vstalk-web-onboarding.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/assets/latest/vstalk-web-saved.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/assets/latest/vstalk-web-search.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
19
docs/assets/readme/contribution-path.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<svg width="1200" height="320" viewBox="0 0 1200 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="320" fill="#F7F8FA"/>
|
||||
<rect x="40" y="44" width="1120" height="232" rx="28" fill="white" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="84" y="102" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="32" font-weight="700">Contribution Path</text>
|
||||
<text x="84" y="134" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="18">처음 방문자도 어디서 시작해야 하는지 바로 읽히도록, 기여 동선을 짧게 유지합니다.</text>
|
||||
|
||||
<rect x="84" y="176" width="220" height="64" rx="18" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="114" y="216" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="20" font-weight="700">1. Read Status</text>
|
||||
<rect x="336" y="176" width="220" height="64" rx="18" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="366" y="216" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="20" font-weight="700">2. Pick A Surface</text>
|
||||
<rect x="588" y="176" width="220" height="64" rx="18" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="618" y="216" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="20" font-weight="700">3. Open Issue / PR</text>
|
||||
<rect x="840" y="176" width="236" height="64" rx="18" fill="#111827"/>
|
||||
<text x="870" y="216" fill="white" font-family="Segoe UI, Arial, sans-serif" font-size="20" font-weight="700">4. Update Docs + Proof</text>
|
||||
|
||||
<path d="M304 208H336" stroke="#9CA3AF" stroke-width="2"/>
|
||||
<path d="M556 208H588" stroke="#9CA3AF" stroke-width="2"/>
|
||||
<path d="M808 208H840" stroke="#9CA3AF" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/assets/readme/conversation.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
21
docs/assets/readme/evaluation-paths.svg
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<svg width="1200" height="340" viewBox="0 0 1200 340" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="340" rx="28" fill="#F8F8F7"/>
|
||||
<rect x="32" y="32" width="1136" height="276" rx="24" fill="white" stroke="#E5E7EB"/>
|
||||
<text x="64" y="88" fill="#111827" font-family="Arial, sans-serif" font-size="32" font-weight="700">Evaluation Paths</text>
|
||||
<text x="64" y="120" fill="#6B7280" font-family="Arial, sans-serif" font-size="16">방문자, 기여자, 운영자가 각자 가장 빨리 들어오는 길</text>
|
||||
|
||||
<rect x="64" y="156" width="328" height="116" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="88" y="192" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Visitor</text>
|
||||
<text x="88" y="222" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">README → Project Status → Showcase</text>
|
||||
<text x="88" y="244" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">Live Web / Releases / Screenshots</text>
|
||||
|
||||
<rect x="436" y="156" width="328" height="116" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="460" y="192" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Contributor</text>
|
||||
<text x="460" y="222" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">First Contribution → Community</text>
|
||||
<text x="460" y="244" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">Contributing / UX Atlas / Roadmap</text>
|
||||
|
||||
<rect x="808" y="156" width="320" height="116" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="832" y="192" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Operator</text>
|
||||
<text x="832" y="222" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">Releasing → Deploy → Release Assets</text>
|
||||
<text x="832" y="244" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">Download Host / Forge Releases</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/assets/readme/hero-shell.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/assets/readme/onboarding.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
41
docs/assets/readme/open-source-surface.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<svg width="1200" height="520" viewBox="0 0 1200 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="520" fill="#F7F8FA"/>
|
||||
<rect x="48" y="48" width="1104" height="424" rx="28" fill="white" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="88" y="110" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="36" font-weight="700">Open Source Surface</text>
|
||||
<text x="88" y="144" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="18">코드만이 아니라 상태, 문서, 릴리즈, 라이브 채널까지 한 화면에서 읽히는 저장소를 목표로 합니다.</text>
|
||||
|
||||
<rect x="88" y="188" width="196" height="216" rx="22" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="112" y="232" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="24" font-weight="700">Code</text>
|
||||
<text x="112" y="268" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Desktop</text>
|
||||
<text x="112" y="294" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Mobile Web</text>
|
||||
<text x="112" y="320" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">API</text>
|
||||
<text x="112" y="368" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600">실제 구현의 중심</text>
|
||||
|
||||
<rect x="304" y="188" width="196" height="216" rx="22" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="328" y="232" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="24" font-weight="700">Docs</text>
|
||||
<text x="328" y="268" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">README</text>
|
||||
<text x="328" y="294" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Status</text>
|
||||
<text x="328" y="320" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Master Plan</text>
|
||||
<text x="328" y="368" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600">방향과 현실의 정합성</text>
|
||||
|
||||
<rect x="520" y="188" width="196" height="216" rx="22" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="544" y="232" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="24" font-weight="700">Releases</text>
|
||||
<text x="544" y="268" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Forge Releases</text>
|
||||
<text x="544" y="294" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Download Host</text>
|
||||
<text x="544" y="320" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Checksums</text>
|
||||
<text x="544" y="368" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600">받아볼 수 있는 결과물</text>
|
||||
|
||||
<rect x="736" y="188" width="196" height="216" rx="22" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="760" y="232" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="24" font-weight="700">Live</text>
|
||||
<text x="760" y="268" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">vstalk.phy.kr</text>
|
||||
<text x="760" y="294" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Health</text>
|
||||
<text x="760" y="320" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Screenshots</text>
|
||||
<text x="760" y="368" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600">지금 직접 확인 가능한 표면</text>
|
||||
|
||||
<rect x="952" y="188" width="152" height="216" rx="22" fill="#FFFFFF" stroke="#E5E7EB" stroke-width="2"/>
|
||||
<text x="976" y="232" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="24" font-weight="700">People</text>
|
||||
<text x="976" y="268" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Issues</text>
|
||||
<text x="976" y="294" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">PRs</text>
|
||||
<text x="976" y="320" fill="#6B7280" font-family="Segoe UI, Arial, sans-serif" font-size="16">Maintainers</text>
|
||||
<text x="976" y="368" fill="#111827" font-family="Segoe UI, Arial, sans-serif" font-size="16" font-weight="600">기여와 운영의 접점</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
49
docs/assets/readme/platform-journey.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<svg width="1200" height="620" viewBox="0 0 1200 620" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="40" y1="20" x2="1160" y2="600" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B1220"/>
|
||||
<stop offset="1" stop-color="#111827"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lineA" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#38BDF8"/>
|
||||
<stop offset="1" stop-color="#22C55E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lineB" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#F59E0B"/>
|
||||
<stop offset="1" stop-color="#FB7185"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="20" y="20" width="1160" height="580" rx="28" fill="url(#bg)" stroke="#1F2937"/>
|
||||
|
||||
<text x="64" y="86" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="34" font-weight="700">Platform Journey</text>
|
||||
<text x="64" y="122" fill="#94A3B8" font-family="Arial, sans-serif" font-size="18">Three product surfaces, one release story: live entrypoint, desktop focus, Android parallel rollout.</text>
|
||||
|
||||
<rect x="64" y="188" width="320" height="156" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<text x="96" y="234" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Mobile Web</text>
|
||||
<text x="96" y="270" fill="#86EFAC" font-family="Arial, sans-serif" font-size="30" font-weight="700">vstalk.phy.kr</text>
|
||||
<text x="96" y="310" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Lowest friction entrypoint.</text>
|
||||
<text x="96" y="336" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Quick signup, short reply, fast re-entry.</text>
|
||||
|
||||
<rect x="440" y="188" width="320" height="156" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<text x="472" y="234" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Windows Desktop</text>
|
||||
<text x="472" y="270" fill="#93C5FD" font-family="Arial, sans-serif" font-size="30" font-weight="700">Primary productivity client</text>
|
||||
<text x="472" y="310" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Compact shell, list, chat, send, buildable zip.</text>
|
||||
<text x="472" y="336" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Targeting search, focus, and multiwindow flow.</text>
|
||||
|
||||
<rect x="816" y="188" width="320" height="156" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<text x="848" y="234" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Android APK</text>
|
||||
<text x="848" y="270" fill="#FCD34D" font-family="Arial, sans-serif" font-size="30" font-weight="700">Parallel next channel</text>
|
||||
<text x="848" y="310" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Push, repeat use, attachment flow,</text>
|
||||
<text x="848" y="336" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">and release parity with desktop assets.</text>
|
||||
|
||||
<path d="M384 266H440" stroke="url(#lineA)" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M760 266H816" stroke="url(#lineB)" stroke-width="4" stroke-linecap="round" stroke-dasharray="10 10"/>
|
||||
<circle cx="440" cy="266" r="7" fill="#38BDF8"/>
|
||||
<circle cx="816" cy="266" r="7" fill="#F59E0B"/>
|
||||
|
||||
<rect x="64" y="410" width="1072" height="130" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<text x="96" y="454" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="20" font-weight="700">Public release path</text>
|
||||
<text x="96" y="492" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">Repository docs and screenshots -> Forge Releases -> download-vstalk.phy.kr mirror -> latest links by OS</text>
|
||||
<text x="96" y="520" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">The project treats release notes, screenshots, and downloadable artifacts as one continuous public surface.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
54
docs/assets/readme/product-pillars.svg
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<svg width="1200" height="520" viewBox="0 0 1200 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="40" y1="20" x2="1160" y2="500" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B1220"/>
|
||||
<stop offset="1" stop-color="#111827"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accentA" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#38BDF8"/>
|
||||
<stop offset="1" stop-color="#22C55E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accentB" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#F59E0B"/>
|
||||
<stop offset="1" stop-color="#FB7185"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-40" y="-40" width="1280" height="600" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="24" flood-color="#020617" flood-opacity="0.28"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect x="20" y="20" width="1160" height="480" rx="28" fill="url(#bg)" stroke="#1F2937"/>
|
||||
|
||||
<text x="64" y="86" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="34" font-weight="700">Product Pillars</text>
|
||||
<text x="64" y="122" fill="#94A3B8" font-family="Arial, sans-serif" font-size="18">A Korean-first messenger project tuned for calm, fast, and transparent communication.</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="58" y="176" width="330" height="258" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<rect x="86" y="206" width="134" height="42" rx="14" fill="url(#accentA)"/>
|
||||
<text x="110" y="233" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="18" font-weight="700">Korean-first UI</text>
|
||||
<text x="86" y="292" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Short copy, low friction,</text>
|
||||
<text x="86" y="326" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">real Korean usage flow.</text>
|
||||
<text x="86" y="372" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">Designed around onboarding, read-reply loops,</text>
|
||||
<text x="86" y="398" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">and recovery language that feel natural.</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="435" y="176" width="330" height="258" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<rect x="463" y="206" width="152" height="42" rx="14" fill="url(#accentA)"/>
|
||||
<text x="486" y="233" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="18" font-weight="700">Windows priority</text>
|
||||
<text x="463" y="292" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Compact desktop UX with</text>
|
||||
<text x="463" y="326" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">multiwindow headroom.</text>
|
||||
<text x="463" y="372" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">The project bets on search, focus tools,</text>
|
||||
<text x="463" y="398" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">and task-oriented message recovery.</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="812" y="176" width="330" height="258" rx="24" fill="#0E1828" stroke="#233246"/>
|
||||
<rect x="840" y="206" width="170" height="42" rx="14" fill="url(#accentB)"/>
|
||||
<text x="863" y="233" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="18" font-weight="700">Transparent surface</text>
|
||||
<text x="840" y="292" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Docs, screenshots,</text>
|
||||
<text x="840" y="326" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">releases, and status aligned.</text>
|
||||
<text x="840" y="372" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">This repo treats public project surfaces as</text>
|
||||
<text x="840" y="398" fill="#94A3B8" font-family="Arial, sans-serif" font-size="17">part of the product, not afterthoughts.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
26
docs/assets/readme/public-contract.svg
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<svg width="1200" height="360" viewBox="0 0 1200 360" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="360" rx="28" fill="#F8F8F7"/>
|
||||
<rect x="32" y="32" width="1136" height="296" rx="24" fill="white" stroke="#E5E7EB"/>
|
||||
<text x="64" y="88" fill="#111827" font-family="Arial, sans-serif" font-size="32" font-weight="700">Public Contract</text>
|
||||
<text x="64" y="120" fill="#6B7280" font-family="Arial, sans-serif" font-size="16">이 저장소가 공개면에서 지키려는 네 가지 약속</text>
|
||||
|
||||
<rect x="64" y="152" width="248" height="128" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="88" y="190" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Transparent State</text>
|
||||
<text x="88" y="220" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">README와 상태표는 실제 구현보다</text>
|
||||
<text x="88" y="242" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">좋아 보이도록 과장하지 않는다.</text>
|
||||
|
||||
<rect x="336" y="152" width="248" height="128" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="360" y="190" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Release Discipline</text>
|
||||
<text x="360" y="220" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">릴리즈, 스크린샷, 다운로드,</text>
|
||||
<text x="360" y="242" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">CHANGELOG를 같이 맞춘다.</text>
|
||||
|
||||
<rect x="608" y="152" width="248" height="128" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="632" y="190" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Korean-first UX</text>
|
||||
<text x="632" y="220" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">번역체보다 실제 한국어 흐름과</text>
|
||||
<text x="632" y="242" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">낮은 피로도의 사용성을 우선한다.</text>
|
||||
|
||||
<rect x="880" y="152" width="248" height="128" rx="20" fill="#FAFAFA" stroke="#E5E7EB"/>
|
||||
<text x="904" y="190" fill="#111827" font-family="Arial, sans-serif" font-size="22" font-weight="700">Open Planning</text>
|
||||
<text x="904" y="220" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">문서와 비판적 리뷰를 공개해</text>
|
||||
<text x="904" y="242" fill="#6B7280" font-family="Arial, sans-serif" font-size="15">방향과 한계를 숨기지 않는다.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
55
docs/assets/readme/release-flow.svg
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<svg width="1200" height="520" viewBox="0 0 1200 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="40" y1="30" x2="1160" y2="490" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B1220"/>
|
||||
<stop offset="1" stop-color="#111827"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chip" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#38BDF8"/>
|
||||
<stop offset="1" stop-color="#22C55E"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="20" y="20" width="1160" height="480" rx="28" fill="url(#bg)" stroke="#1F2937"/>
|
||||
<text x="64" y="86" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="34" font-weight="700">Release Surface</text>
|
||||
<text x="64" y="120" fill="#94A3B8" font-family="Arial, sans-serif" font-size="18">The repository keeps code, screenshots, release rules, and download surfaces aligned.</text>
|
||||
|
||||
<rect x="60" y="190" width="230" height="190" rx="24" fill="#0D1726" stroke="#233246"/>
|
||||
<text x="90" y="236" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Build sources</text>
|
||||
<text x="90" y="274" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Code + Docs + Screenshots</text>
|
||||
<text x="90" y="318" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">README</text>
|
||||
<text x="90" y="344" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">문서/ planning set</text>
|
||||
<text x="90" y="370" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">latest repo screenshots</text>
|
||||
|
||||
<rect x="350" y="190" width="230" height="190" rx="24" fill="#0D1726" stroke="#233246"/>
|
||||
<text x="380" y="236" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Packaging</text>
|
||||
<text x="380" y="274" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Release assets</text>
|
||||
<text x="380" y="318" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">portable zip</text>
|
||||
<text x="380" y="344" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">apk channel next</text>
|
||||
<text x="380" y="370" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">checksums and metadata</text>
|
||||
|
||||
<rect x="640" y="190" width="230" height="190" rx="24" fill="#0D1726" stroke="#233246"/>
|
||||
<text x="670" y="236" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Public surfaces</text>
|
||||
<text x="670" y="274" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Forge Releases</text>
|
||||
<text x="670" y="318" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">versioned assets</text>
|
||||
<text x="670" y="344" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">release notes</text>
|
||||
<text x="670" y="370" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">source of record</text>
|
||||
|
||||
<rect x="930" y="190" width="210" height="190" rx="24" fill="#0D1726" stroke="#233246"/>
|
||||
<text x="960" y="236" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Delivery</text>
|
||||
<text x="960" y="274" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Download host</text>
|
||||
<text x="960" y="318" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">windows/latest</text>
|
||||
<text x="960" y="344" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">android/latest</text>
|
||||
<text x="960" y="370" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">version.json</text>
|
||||
|
||||
<path d="M290 286H350" stroke="url(#chip)" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M580 286H640" stroke="url(#chip)" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M870 286H930" stroke="url(#chip)" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<circle cx="350" cy="286" r="6" fill="#38BDF8"/>
|
||||
<circle cx="640" cy="286" r="6" fill="#22C55E"/>
|
||||
<circle cx="930" cy="286" r="6" fill="#F59E0B"/>
|
||||
|
||||
<rect x="786" y="74" width="304" height="50" rx="16" fill="#101B2C" stroke="#223045"/>
|
||||
<text x="814" y="106" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Principle: what ships must match what the repo says.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
84
docs/assets/readme/system-overview.svg
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<svg width="1200" height="760" viewBox="0 0 1200 760" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="80" y1="40" x2="1080" y2="720" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F172A"/>
|
||||
<stop offset="1" stop-color="#111827"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accentA" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#22C55E"/>
|
||||
<stop offset="1" stop-color="#0EA5E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accentB" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#F59E0B"/>
|
||||
<stop offset="1" stop-color="#FB7185"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-40" y="-40" width="1280" height="840" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="24" flood-color="#020617" flood-opacity="0.32"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect x="24" y="24" width="1152" height="712" rx="32" fill="url(#bg)"/>
|
||||
<rect x="24" y="24" width="1152" height="712" rx="32" stroke="#1F2937"/>
|
||||
|
||||
<text x="70" y="92" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="34" font-weight="700">System Overview</text>
|
||||
<text x="70" y="128" fill="#94A3B8" font-family="Arial, sans-serif" font-size="18">Windows-first messenger with a live mobile web entrypoint and a transparent release surface.</text>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="72" y="184" width="270" height="148" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="102" y="230" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Windows Desktop</text>
|
||||
<text x="102" y="260" fill="#93C5FD" font-family="Arial, sans-serif" font-size="32" font-weight="700">Avalonia 12</text>
|
||||
<text x="102" y="296" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Conversation list, chat view, send flow,</text>
|
||||
<text x="102" y="320" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">portable build channel.</text>
|
||||
|
||||
<rect x="72" y="376" width="270" height="148" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="102" y="422" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Mobile Web</text>
|
||||
<text x="102" y="452" fill="#86EFAC" font-family="Arial, sans-serif" font-size="32" font-weight="700">vstalk.phy.kr</text>
|
||||
<text x="102" y="488" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">PWA shell, quick signup, list, chat,</text>
|
||||
<text x="102" y="512" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">same-origin API and WebSocket flow.</text>
|
||||
|
||||
<rect x="72" y="568" width="270" height="112" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="102" y="614" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Android</text>
|
||||
<text x="102" y="644" fill="#FCD34D" font-family="Arial, sans-serif" font-size="28" font-weight="700">Next parallel client</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="462" y="246" width="286" height="224" rx="28" fill="#0B1220" stroke="#334155"/>
|
||||
<rect x="490" y="278" width="230" height="54" rx="16" fill="url(#accentA)"/>
|
||||
<text x="521" y="313" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="24" font-weight="700">ASP.NET Core 8 API</text>
|
||||
<text x="492" y="372" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Protocols</text>
|
||||
<text x="492" y="402" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">REST bootstrap, conversations, messages</text>
|
||||
<text x="492" y="428" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">WebSocket realtime events</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="862" y="164" width="270" height="148" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="892" y="210" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Storage now</text>
|
||||
<text x="892" y="242" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="30" font-weight="700">SQLite</text>
|
||||
<text x="892" y="278" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">Alpha storage with simple deployment</text>
|
||||
<text x="892" y="302" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">and low operational overhead.</text>
|
||||
|
||||
<rect x="862" y="356" width="270" height="148" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="892" y="402" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Operational surface</text>
|
||||
<text x="892" y="434" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="28" font-weight="700">Caddy + nginx</text>
|
||||
<text x="892" y="470" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">TLS, same-origin mobile web,</text>
|
||||
<text x="892" y="494" fill="#94A3B8" font-family="Arial, sans-serif" font-size="16">shared VPS reverse proxy routing.</text>
|
||||
|
||||
<rect x="862" y="548" width="270" height="132" rx="24" fill="#0B1220" stroke="#243244"/>
|
||||
<text x="892" y="594" fill="#E2E8F0" font-family="Arial, sans-serif" font-size="18" font-weight="700">Target stack later</text>
|
||||
<text x="892" y="626" fill="#F8FAFC" font-family="Arial, sans-serif" font-size="26" font-weight="700">PostgreSQL / Redis / MinIO</text>
|
||||
</g>
|
||||
|
||||
<path d="M342 258H462" stroke="#38BDF8" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M342 450H462" stroke="#38BDF8" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M342 624H462" stroke="#F59E0B" stroke-width="4" stroke-linecap="round" stroke-dasharray="10 10"/>
|
||||
<path d="M748 358H862" stroke="#34D399" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M748 430H862" stroke="#34D399" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M748 614H862" stroke="#F59E0B" stroke-width="4" stroke-linecap="round" stroke-dasharray="10 10"/>
|
||||
|
||||
<circle cx="462" cy="258" r="7" fill="#38BDF8"/>
|
||||
<circle cx="462" cy="450" r="7" fill="#38BDF8"/>
|
||||
<circle cx="462" cy="624" r="7" fill="#F59E0B"/>
|
||||
<circle cx="862" cy="358" r="7" fill="#34D399"/>
|
||||
<circle cx="862" cy="430" r="7" fill="#34D399"/>
|
||||
<circle cx="862" cy="614" r="7" fill="#F59E0B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6 KiB |
29
docs/repository-surfaces.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Repository Surfaces
|
||||
|
||||
이 문서는 공개 저장소에서 무엇을 어떤 이름으로 노출하는지 정리합니다.
|
||||
|
||||
## Public Naming
|
||||
|
||||
- 제품 노출명: `KoTalk`
|
||||
- 한글 표기: `코톡`
|
||||
- 웹 진입 도메인: `vstalk.phy.kr`
|
||||
- 다운로드 미러: `download-vstalk.phy.kr`
|
||||
|
||||
## Repository Structure
|
||||
|
||||
- `src/`, `tests/`: 제품 코드와 검증 코드
|
||||
- `docs/`: 공개 보조 문서와 시각 자산
|
||||
- `문서/`: 제품 마스터 플랜과 UX 아틀라스
|
||||
- `deploy/`: 범용 배포 골격
|
||||
- `release-assets/`: 릴리즈 메타데이터와 배포 자산 스테이징
|
||||
|
||||
## Public Writing Rules
|
||||
|
||||
- README는 첫 방문자가 30초 안에 판단할 수 있게 유지합니다.
|
||||
- 공개 문서에는 실제 운영 힌트, 비밀값, 내부 메모를 적지 않습니다.
|
||||
- 공식 서비스와 오픈소스 저장소는 같은 표면처럼 쓰지 않습니다.
|
||||
|
||||
## Current Technical Note
|
||||
|
||||
현재 저장소의 코드 네임스페이스와 프로젝트 파일은 아직 `PhysOn.*`를 사용합니다.
|
||||
이 문서는 공개 브랜드 기준을 먼저 정리한 것이며, 소스 네임스페이스 정렬은 별도 작업입니다.
|
||||
5
global.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "8.0.420"
|
||||
}
|
||||
}
|
||||
9
release-assets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
!latest/
|
||||
!latest/.gitkeep
|
||||
!releases/
|
||||
!releases/.gitkeep
|
||||
!templates/
|
||||
!templates/RELEASE_NOTES.ko.md
|
||||
93
release-assets/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Release Assets
|
||||
|
||||
이 디렉터리는 Windows와 Android 클라이언트 산출물을 함께 정리하는 멀티플랫폼 릴리즈 스테이징 영역입니다. 소스 코드 디렉터리가 아니라, 생성된 릴리즈 메타데이터와 배포 번들을 잠시 정리하는 generated surface로 취급합니다.
|
||||
|
||||
## 목표
|
||||
|
||||
- 같은 버전 번호 아래에 Windows와 Android 산출물을 병렬로 보관합니다.
|
||||
- `latest/`는 최신 포인터, `releases/<version>/`는 불변 이력으로 구분합니다.
|
||||
- 원격 Forge Releases는 버전별 원본 저장소, `download-vs-messanger.phy.kr`는 최종 사용자용 다운로드 미러로 사용합니다.
|
||||
|
||||
## 목표 구조
|
||||
|
||||
```text
|
||||
release-assets/
|
||||
latest/
|
||||
version.json
|
||||
latest.json
|
||||
RELEASE_NOTES.ko.md
|
||||
SHA256SUMS.txt
|
||||
screenshots/
|
||||
windows/
|
||||
VsMessenger-win-x64.zip
|
||||
SHA256SUMS.txt
|
||||
version.json
|
||||
android/
|
||||
VsMessenger-android-universal.apk
|
||||
SHA256SUMS.txt
|
||||
version.json
|
||||
releases/
|
||||
v0.2.0-alpha.1/
|
||||
version.json
|
||||
RELEASE_NOTES.ko.md
|
||||
SHA256SUMS.txt
|
||||
screenshots/
|
||||
windows/
|
||||
x64/
|
||||
VsMessenger-win-x64-v0.2.0-alpha.1.zip
|
||||
SHA256SUMS.txt
|
||||
android/
|
||||
universal/
|
||||
VsMessenger-android-universal-v0.2.0-alpha.1.apk
|
||||
SHA256SUMS.txt
|
||||
```
|
||||
|
||||
## 기본 규칙
|
||||
|
||||
- 같은 버전은 같은 서버 API 계약과 같은 릴리즈 노트를 공유합니다.
|
||||
- Windows와 Android는 같은 태그 아래 병렬 산출물로 게시합니다.
|
||||
- Windows 기본 공개 형식은 `zip`, Android 기본 공개 형식은 `apk`입니다.
|
||||
- APK는 공개 채널에 올릴 때 반드시 서명본을 사용합니다.
|
||||
- `latest/version.json`은 전체 플랫폼 상태를 담고, `latest/windows/version.json`, `latest/android/version.json`은 플랫폼별 상세 포인터를 담습니다.
|
||||
|
||||
## 다운로드 경로 규칙
|
||||
|
||||
- 최신 Windows: `https://download-vs-messanger.phy.kr/windows/latest`
|
||||
- 최신 Android: `https://download-vs-messanger.phy.kr/android/latest`
|
||||
- 전체 최신 메타데이터: `https://download-vs-messanger.phy.kr/latest/version.json`
|
||||
- 버전별 Windows: `https://download-vs-messanger.phy.kr/releases/<version>/windows/x64/...`
|
||||
- 버전별 Android: `https://download-vs-messanger.phy.kr/releases/<version>/android/universal/...`
|
||||
|
||||
## 생성 스크립트
|
||||
|
||||
실제 파일 생성은 `scripts/release/release-prepare-assets.sh`를 사용합니다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
./scripts/release/release-prepare-assets.sh \
|
||||
--version v0.2.0-alpha.1 \
|
||||
--channel alpha \
|
||||
--windows-zip artifacts/release/VsMessenger-win-x64-v0.2.0-alpha.1.zip \
|
||||
--android-apk artifacts/release/VsMessenger-android-universal-v0.2.0-alpha.1.apk \
|
||||
--screenshots artifacts/screenshots \
|
||||
--force
|
||||
```
|
||||
|
||||
## 업로드 스크립트
|
||||
|
||||
- VPS 다운로드 미러 업로드: `scripts/release/release-upload-assets.sh`
|
||||
- Forge Releases 게시: `scripts/release/release-publish-forge.sh`
|
||||
|
||||
두 채널은 목적이 다릅니다.
|
||||
|
||||
- Forge Releases: 버전별 원본 보관
|
||||
- 다운로드 미러: 최신 포인터와 빠른 정적 다운로드
|
||||
- 모바일 웹앱: `release-assets/`가 아니라 `vstalk.phy.kr` 배포 트랙에서 별도 운영
|
||||
|
||||
## 운영 메모
|
||||
|
||||
- 생성된 버전별 산출물은 기본적으로 Git 추적 대상이 아닙니다.
|
||||
- 공개 릴리즈마다 `RELEASE_NOTES.ko.md`, `SHA256SUMS.txt`, `version.json`을 함께 갱신합니다.
|
||||
- 같은 버전에서 Windows만 있고 Android가 아직 없을 수는 있지만, 장기 원칙은 `같은 버전 아래 두 플랫폼 병렬 게시`입니다.
|
||||
- 모바일 웹앱 정적 산출물은 `release-assets/`가 아니라 `/srv/vs-messanger/webapp/releases/<version>`에 배포합니다.
|
||||
0
release-assets/latest/.gitkeep
Normal file
1
release-assets/releases/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
19
release-assets/templates/RELEASE_NOTES.ko.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# vs-messanger {{VERSION}}
|
||||
|
||||
- 채널: `{{CHANNEL}}`
|
||||
- 게시 시각: `{{PUBLISHED_AT}}`
|
||||
|
||||
## 이번 빌드에 포함된 것
|
||||
|
||||
- Windows x64 portable zip
|
||||
- Android universal apk
|
||||
- 무결성 체크용 `SHA256SUMS.txt`
|
||||
- 버전 메타데이터 `version.json`, `latest.json`
|
||||
- 필요 시 한국어 스크린샷
|
||||
|
||||
## 확인할 것
|
||||
|
||||
- 가입 진입
|
||||
- 첫 대화 진입
|
||||
- 메시지 송신/수신
|
||||
- 다운로드 링크 동작
|
||||
1
scripts/capture_vstalk_web_screenshots.cjs
Normal file
|
|
@ -0,0 +1 @@
|
|||
require('./ci/capture-vstalk-web-screenshots.cjs')
|
||||
397
scripts/ci/capture-vstalk-web-screenshots.cjs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const process = require('node:process')
|
||||
const { createRequire } = require('node:module')
|
||||
|
||||
const webPackageRequire = createRequire(path.resolve(process.cwd(), 'src/PhysOn.Web/package.json'))
|
||||
const puppeteer = webPackageRequire('puppeteer-core')
|
||||
|
||||
const baseUrl = process.env.VSTALK_CAPTURE_URL ?? 'http://127.0.0.1:4174/'
|
||||
const outputDir = process.env.VSTALK_CAPTURE_OUTPUT_DIR
|
||||
?? path.resolve(process.cwd(), 'docs/assets/latest')
|
||||
const executablePath = process.env.CHROME_BIN ?? '/usr/bin/google-chrome'
|
||||
|
||||
const bootstrapPayload = {
|
||||
me: {
|
||||
userId: 'me-1',
|
||||
displayName: '이안',
|
||||
profileImageUrl: null,
|
||||
statusMessage: '업무와 일상을 가볍게 잇는 중',
|
||||
},
|
||||
session: {
|
||||
sessionId: 'session-alpha-web',
|
||||
deviceId: 'device-web-alpha',
|
||||
deviceName: 'Mobile Web',
|
||||
createdAt: '2026-04-16T04:50:00Z',
|
||||
},
|
||||
ws: {
|
||||
url: 'wss://vstalk.phy.kr/v1/realtime/ws',
|
||||
},
|
||||
conversations: {
|
||||
items: [
|
||||
{
|
||||
conversationId: 'conv-team',
|
||||
type: 'group',
|
||||
title: '제품 운영',
|
||||
avatarUrl: null,
|
||||
subtitle: '10시 전에 공유안만 확인해 주세요.',
|
||||
memberCount: 4,
|
||||
isMuted: false,
|
||||
isPinned: true,
|
||||
sortKey: '2026-04-16T05:04:00Z',
|
||||
unreadCount: 2,
|
||||
lastReadSequence: 10,
|
||||
lastMessage: {
|
||||
messageId: 'msg-11',
|
||||
text: '10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
senderUserId: 'u-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
conversationId: 'conv-friends',
|
||||
type: 'group',
|
||||
title: '주말 약속',
|
||||
avatarUrl: null,
|
||||
subtitle: '토요일 2시에 브런치 어때?',
|
||||
memberCount: 3,
|
||||
isMuted: false,
|
||||
isPinned: false,
|
||||
sortKey: '2026-04-16T04:48:00Z',
|
||||
unreadCount: 0,
|
||||
lastReadSequence: 5,
|
||||
lastMessage: {
|
||||
messageId: 'msg-22',
|
||||
text: '토요일 2시에 브런치 어때?',
|
||||
createdAt: '2026-04-16T04:48:00Z',
|
||||
senderUserId: 'u-3',
|
||||
},
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
}
|
||||
|
||||
const messageMap = {
|
||||
'conv-team': {
|
||||
items: [
|
||||
{
|
||||
messageId: 'msg-8',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-8',
|
||||
kind: 'text',
|
||||
text: '회의 전에 이슈만 짧게 정리해 주세요.',
|
||||
createdAt: '2026-04-16T04:40:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 8,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-9',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-9',
|
||||
kind: 'text',
|
||||
text: '공유안은 정리해 두었습니다. 바로 올릴게요.',
|
||||
createdAt: '2026-04-16T04:47:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'me-1',
|
||||
displayName: '이안',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: true,
|
||||
serverSequence: 9,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-10',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-10',
|
||||
kind: 'text',
|
||||
text: '좋아요. 10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 10,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-11',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-11',
|
||||
kind: 'text',
|
||||
text: '10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 11,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
'conv-friends': {
|
||||
items: [
|
||||
{
|
||||
messageId: 'msg-20',
|
||||
conversationId: 'conv-friends',
|
||||
clientMessageId: 'client-20',
|
||||
kind: 'text',
|
||||
text: '이번 주말에 시간 괜찮아?',
|
||||
createdAt: '2026-04-16T04:32:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-3',
|
||||
displayName: '수아',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 4,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-21',
|
||||
conversationId: 'conv-friends',
|
||||
clientMessageId: 'client-21',
|
||||
kind: 'text',
|
||||
text: '토요일 2시에 브런치 어때?',
|
||||
createdAt: '2026-04-16T04:48:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-3',
|
||||
displayName: '수아',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 5,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
}
|
||||
|
||||
const storedSession = {
|
||||
apiBaseUrl: '',
|
||||
tokens: {
|
||||
accessToken: 'access-token-alpha',
|
||||
accessTokenExpiresAt: '2026-04-16T06:00:00Z',
|
||||
refreshToken: 'refresh-token-alpha',
|
||||
refreshTokenExpiresAt: '2026-05-16T06:00:00Z',
|
||||
},
|
||||
bootstrap: bootstrapPayload,
|
||||
savedAt: '2026-04-16T05:04:00Z',
|
||||
}
|
||||
|
||||
async function ensureOutputDir() {
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
async function createBrowser() {
|
||||
return puppeteer.launch({
|
||||
executablePath,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
defaultViewport: {
|
||||
width: 390,
|
||||
height: 844,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
deviceScaleFactor: 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function installSessionMocks(page) {
|
||||
await page.evaluateOnNewDocument((session) => {
|
||||
class FakeWebSocket {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.readyState = 0
|
||||
this.onopen = null
|
||||
this.onclose = null
|
||||
this.onerror = null
|
||||
this.onmessage = null
|
||||
window.setTimeout(() => {
|
||||
this.readyState = 1
|
||||
if (this.onopen) {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
}, 80)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3
|
||||
if (this.onclose) {
|
||||
this.onclose({ type: 'close' })
|
||||
}
|
||||
}
|
||||
|
||||
send() {}
|
||||
}
|
||||
|
||||
window.localStorage.setItem('vs-talk.session', JSON.stringify(session))
|
||||
window.localStorage.setItem('vs-talk.invite-code', 'ALPHA')
|
||||
window.WebSocket = FakeWebSocket
|
||||
}, storedSession)
|
||||
|
||||
await page.setRequestInterception(true)
|
||||
page.on('request', (request) => {
|
||||
const url = new URL(request.url())
|
||||
|
||||
if (url.pathname === '/v1/bootstrap') {
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: bootstrapPayload }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (/\/v1\/conversations\/[^/]+\/messages$/.test(url.pathname)) {
|
||||
const match = url.pathname.match(/\/v1\/conversations\/([^/]+)\/messages/)
|
||||
const conversationId = match ? match[1] : ''
|
||||
const payload = messageMap[conversationId] ?? { items: [], nextCursor: null }
|
||||
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: payload }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (/\/v1\/conversations\/[^/]+\/read-cursor$/.test(url.pathname)) {
|
||||
const match = url.pathname.match(/\/v1\/conversations\/([^/]+)\/read-cursor/)
|
||||
const conversationId = match ? match[1] : ''
|
||||
const body = JSON.parse(request.postData() ?? '{}')
|
||||
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
conversationId,
|
||||
accountId: 'me-1',
|
||||
lastReadSequence: body.lastReadSequence ?? 0,
|
||||
updatedAt: '2026-04-16T05:05:00Z',
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === '/v1/auth/token/refresh') {
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
tokens: storedSession.tokens,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
request.continue()
|
||||
})
|
||||
}
|
||||
|
||||
async function captureOnboarding(browser) {
|
||||
const page = await browser.newPage()
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
await page.reload({ waitUntil: 'networkidle2' })
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-onboarding.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureConversationList(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.conversation-row')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-list.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureConversation(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.conversation-row')
|
||||
await page.click('.conversation-row')
|
||||
await page.waitForSelector('.message-bubble')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-chat.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureSearch(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.bottom-bar')
|
||||
await page.click('.bottom-bar .nav-button:nth-child(2)')
|
||||
await page.waitForSelector('.search-field')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-search.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureSaved(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.bottom-bar')
|
||||
await page.click('.bottom-bar .nav-button:nth-child(3)')
|
||||
await page.waitForSelector('.saved-section')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-saved.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ensureOutputDir()
|
||||
const browser = await createBrowser()
|
||||
|
||||
try {
|
||||
await captureOnboarding(browser)
|
||||
await captureConversationList(browser)
|
||||
await captureSearch(browser)
|
||||
await captureSaved(browser)
|
||||
await captureConversation(browser)
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
5
scripts/deploy-mvp-stack.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/deploy/deploy-stack-mvp.sh" "$@"
|
||||
5
scripts/deploy-webapp.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/deploy/deploy-webapp-static.sh" "$@"
|
||||
106
scripts/deploy/deploy-stack-mvp.sh
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/deploy/deploy-stack-mvp.sh --host example.com --user deploy [options]
|
||||
|
||||
Options:
|
||||
--app-dir <path> Remote application root. Default: /srv/vs-messanger/app
|
||||
--download-root <path> Remote download root. Default: /srv/vs-messanger/download
|
||||
--ssh-key <path> Private key used for SSH/rsync
|
||||
--dry-run Print the rsync plan without changing the server
|
||||
|
||||
Notes:
|
||||
- Remote host must already contain a valid deploy/.env file.
|
||||
- This script syncs deploy files and the current src tree, then runs docker compose.
|
||||
EOF
|
||||
}
|
||||
|
||||
host=""
|
||||
user=""
|
||||
app_dir="/srv/vs-messanger/app"
|
||||
download_root="/srv/vs-messanger/download"
|
||||
ssh_key=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
host="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--app-dir)
|
||||
app_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--download-root)
|
||||
download_root="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-key)
|
||||
ssh_key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$host" || -z "$user" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -n "$ssh_key" ]]; then
|
||||
ssh_cmd+=(-i "$ssh_key")
|
||||
fi
|
||||
|
||||
rsync_opts=(-az)
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
rsync_opts+=(--dry-run)
|
||||
fi
|
||||
|
||||
target_host="$user@$host"
|
||||
rsh="${ssh_cmd[*]}"
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$app_dir' '$download_root'"
|
||||
|
||||
rsync "${rsync_opts[@]}" \
|
||||
-e "$rsh" \
|
||||
--delete \
|
||||
--filter="protect /deploy/.env" \
|
||||
--include "/VsMessenger.sln" \
|
||||
--include "/global.json" \
|
||||
--include "/deploy/***" \
|
||||
--include "/src/***" \
|
||||
--exclude "*" \
|
||||
"$repo_root"/ "$target_host:$app_dir/"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
echo "Dry run complete. Remote compose was not started."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" \
|
||||
"cd '$app_dir' && test -f deploy/.env && docker compose --env-file deploy/.env -f deploy/compose.mvp.yml up -d --build --remove-orphans"
|
||||
|
||||
echo "Deployed MVP stack to $target_host:$app_dir"
|
||||
117
scripts/deploy/deploy-webapp-static.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/deploy/deploy-webapp-static.sh --host example.com --user deploy --source dist [options]
|
||||
|
||||
Options:
|
||||
--source <path> Local webapp build directory to upload
|
||||
--version <name> Remote release directory name. Default: current timestamp
|
||||
--app-dir <path> Remote application root. Default: /srv/vs-messanger/app
|
||||
--target <path> Remote webapp root. Default: /srv/vs-messanger/webapp
|
||||
--ssh-key <path> Private key used for SSH/rsync
|
||||
--dry-run Print the rsync plan without changing the server
|
||||
|
||||
Notes:
|
||||
- Remote host must already contain a valid deploy/.env file.
|
||||
- This script uploads static webapp files into releases/<version> and repoints current -> releases/<version>.
|
||||
- The webapp is expected to be served at https://vstalk.phy.kr via Caddy + compose.webapp.yml.
|
||||
EOF
|
||||
}
|
||||
|
||||
host=""
|
||||
user=""
|
||||
source_dir=""
|
||||
version="$(date +%Y%m%d-%H%M%S)"
|
||||
app_dir="/srv/vs-messanger/app"
|
||||
target="/srv/vs-messanger/webapp"
|
||||
ssh_key=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
host="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--source)
|
||||
source_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--app-dir)
|
||||
app_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
target="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-key)
|
||||
ssh_key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$host" || -z "$user" || -z "$source_dir" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "Source directory not found: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -n "$ssh_key" ]]; then
|
||||
ssh_cmd+=(-i "$ssh_key")
|
||||
fi
|
||||
|
||||
rsync_opts=(-az)
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
rsync_opts+=(--dry-run)
|
||||
else
|
||||
rsync_opts+=(--delete)
|
||||
fi
|
||||
|
||||
target_host="$user@$host"
|
||||
rsh="${ssh_cmd[*]}"
|
||||
release_dir="$target/releases/$version"
|
||||
current_link="$target/current"
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$release_dir' '$target/releases' '$app_dir'"
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$source_dir"/ "$target_host:$release_dir/"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
echo "Dry run complete. Remote symlink and compose were not updated."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" \
|
||||
"ln -sfn '$release_dir' '$current_link' && cd '$app_dir' && test -f deploy/.env && docker compose --env-file deploy/.env -f deploy/compose.mvp.yml -f deploy/compose.webapp.yml up -d webapp caddy"
|
||||
|
||||
echo "Deployed webapp release $version to $target_host:$release_dir"
|
||||
5
scripts/prepare-release-assets.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-prepare-assets.sh" "$@"
|
||||
5
scripts/publish-gitea-release.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-publish-forge.sh" "$@"
|
||||
329
scripts/release/release-prepare-assets.sh
Executable file
|
|
@ -0,0 +1,329 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-prepare-assets.sh --version v0.2.0-alpha.1 [options]
|
||||
|
||||
Options:
|
||||
--windows-zip <path> Windows x64 ZIP artifact path
|
||||
--android-apk <path> Android universal APK artifact path
|
||||
--zip <path> Backward-compatible alias for --windows-zip
|
||||
--channel <name> Release channel. Default: alpha
|
||||
--notes <path> Existing Korean release notes file
|
||||
--screenshots <dir> Directory containing *.png/jpg screenshots
|
||||
--force Overwrite an existing release folder
|
||||
|
||||
Environment:
|
||||
DOWNLOAD_BASE_URL Defaults to https://download-vs-messanger.phy.kr
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
channel="alpha"
|
||||
windows_zip=""
|
||||
android_apk=""
|
||||
notes_path=""
|
||||
screenshots_dir=""
|
||||
force="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--channel)
|
||||
channel="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--windows-zip|--zip)
|
||||
windows_zip="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--android-apk)
|
||||
android_apk="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--notes)
|
||||
notes_path="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--screenshots)
|
||||
screenshots_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--force)
|
||||
force="true"
|
||||
shift
|
||||
;;
|
||||
-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
|
||||
|
||||
if [[ -z "$windows_zip" && -z "$android_apk" ]]; then
|
||||
echo "At least one artifact must be provided: --windows-zip or --android-apk" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$windows_zip" && ! -f "$windows_zip" ]]; then
|
||||
echo "Windows ZIP artifact not found: $windows_zip" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$android_apk" && ! -f "$android_apk" ]]; then
|
||||
echo "Android APK artifact not found: $android_apk" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
release_root="$repo_root/release-assets/releases/$version"
|
||||
latest_root="$repo_root/release-assets/latest"
|
||||
template_path="$repo_root/release-assets/templates/RELEASE_NOTES.ko.md"
|
||||
download_base_url="${DOWNLOAD_BASE_URL:-https://download-vs-messanger.phy.kr}"
|
||||
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
|
||||
derive_release_url() {
|
||||
local origin_url
|
||||
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$origin_url" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$origin_url" =~ ^https?:// ]]; then
|
||||
printf '%s/releases/tag/%s' "${origin_url%.git}" "$version"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$origin_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
|
||||
printf 'https://%s/%s/releases/tag/%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$version"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
release_url="$(derive_release_url)"
|
||||
|
||||
if [[ -e "$release_root" && "$force" != "true" ]]; then
|
||||
echo "Release directory already exists: $release_root" >&2
|
||||
echo "Use --force to replace it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$release_root" "$latest_root"
|
||||
mkdir -p "$release_root/screenshots" "$latest_root/screenshots"
|
||||
|
||||
if [[ -n "$notes_path" ]]; then
|
||||
cp "$notes_path" "$release_root/RELEASE_NOTES.ko.md"
|
||||
else
|
||||
sed \
|
||||
-e "s/{{VERSION}}/$version/g" \
|
||||
-e "s/{{CHANNEL}}/$channel/g" \
|
||||
-e "s/{{PUBLISHED_AT}}/$published_at/g" \
|
||||
"$template_path" > "$release_root/RELEASE_NOTES.ko.md"
|
||||
fi
|
||||
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
|
||||
|
||||
if [[ -n "$screenshots_dir" ]]; then
|
||||
while IFS= read -r screenshot; do
|
||||
cp "$screenshot" "$release_root/screenshots/$(basename "$screenshot")"
|
||||
cp "$screenshot" "$latest_root/screenshots/$(basename "$screenshot")"
|
||||
done < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
|
||||
fi
|
||||
|
||||
platform_count=0
|
||||
platforms_json=""
|
||||
top_level_windows_alias=""
|
||||
release_hash_paths=()
|
||||
latest_hash_paths=()
|
||||
|
||||
append_platform_json() {
|
||||
local body="$1"
|
||||
if (( platform_count > 0 )); then
|
||||
platforms_json+=$',\n'
|
||||
fi
|
||||
platforms_json+="$body"
|
||||
platform_count=$((platform_count + 1))
|
||||
}
|
||||
|
||||
write_platform_version_json() {
|
||||
local path="$1"
|
||||
local body="$2"
|
||||
cat > "$path" <<EOF
|
||||
{
|
||||
"version": "$version",
|
||||
"channel": "$channel",
|
||||
"publishedAt": "$published_at",
|
||||
"notesUrl": "$download_base_url/releases/$version/RELEASE_NOTES.ko.md",
|
||||
"releaseUrl": "$release_url",
|
||||
"platform": {
|
||||
$body
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ -n "$windows_zip" ]]; then
|
||||
windows_release_name="VsMessenger-win-x64-$version.zip"
|
||||
windows_latest_name="VsMessenger-win-x64.zip"
|
||||
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"
|
||||
|
||||
(
|
||||
cd "$windows_release_dir"
|
||||
sha256sum "$windows_release_name" > SHA256SUMS.txt
|
||||
)
|
||||
|
||||
(
|
||||
cd "$windows_latest_dir"
|
||||
sha256sum "$windows_latest_name" > SHA256SUMS.txt
|
||||
)
|
||||
|
||||
release_hash_paths+=("windows/x64/$windows_release_name")
|
||||
latest_hash_paths+=("windows/$windows_latest_name")
|
||||
|
||||
windows_platform_body="$(cat <<EOF
|
||||
"kind": "desktop",
|
||||
"arch": "x64",
|
||||
"latestUrl": "$download_base_url/windows/latest",
|
||||
"portableZipUrl": "$download_base_url/windows/latest/$windows_latest_name",
|
||||
"sha256Url": "$download_base_url/windows/latest/SHA256SUMS.txt"
|
||||
EOF
|
||||
)"
|
||||
|
||||
append_platform_json "$(cat <<EOF
|
||||
"windows": {
|
||||
$windows_platform_body
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
top_level_windows_alias="$(cat <<EOF
|
||||
,
|
||||
"windows": {
|
||||
$windows_platform_body
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
write_platform_version_json "$windows_latest_dir/version.json" "$windows_platform_body"
|
||||
fi
|
||||
|
||||
if [[ -n "$android_apk" ]]; then
|
||||
android_release_name="VsMessenger-android-universal-$version.apk"
|
||||
android_latest_name="VsMessenger-android-universal.apk"
|
||||
android_release_dir="$release_root/android/universal"
|
||||
android_latest_dir="$latest_root/android"
|
||||
mkdir -p "$android_release_dir" "$android_latest_dir"
|
||||
|
||||
cp "$android_apk" "$android_release_dir/$android_release_name"
|
||||
cp "$android_apk" "$android_latest_dir/$android_latest_name"
|
||||
|
||||
(
|
||||
cd "$android_release_dir"
|
||||
sha256sum "$android_release_name" > SHA256SUMS.txt
|
||||
)
|
||||
|
||||
(
|
||||
cd "$android_latest_dir"
|
||||
sha256sum "$android_latest_name" > SHA256SUMS.txt
|
||||
)
|
||||
|
||||
release_hash_paths+=("android/universal/$android_release_name")
|
||||
latest_hash_paths+=("android/$android_latest_name")
|
||||
|
||||
android_platform_body="$(cat <<EOF
|
||||
"kind": "mobile",
|
||||
"arch": "universal",
|
||||
"packageName": "kr.physia.vsmessenger",
|
||||
"minSdk": 26,
|
||||
"latestUrl": "$download_base_url/android/latest",
|
||||
"apkUrl": "$download_base_url/android/latest/$android_latest_name",
|
||||
"sha256Url": "$download_base_url/android/latest/SHA256SUMS.txt"
|
||||
EOF
|
||||
)"
|
||||
|
||||
append_platform_json "$(cat <<EOF
|
||||
"android": {
|
||||
$android_platform_body
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
write_platform_version_json "$android_latest_dir/version.json" "$android_platform_body"
|
||||
fi
|
||||
|
||||
if (( ${#release_hash_paths[@]} > 0 )); then
|
||||
(
|
||||
cd "$release_root"
|
||||
sha256sum "${release_hash_paths[@]}" > SHA256SUMS.txt
|
||||
)
|
||||
fi
|
||||
|
||||
if (( ${#latest_hash_paths[@]} > 0 )); then
|
||||
(
|
||||
cd "$latest_root"
|
||||
sha256sum "${latest_hash_paths[@]}" > SHA256SUMS.txt
|
||||
)
|
||||
fi
|
||||
|
||||
mapfile -t screenshot_files < <(find "$release_root/screenshots" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
|
||||
|
||||
screenshots_json="[]"
|
||||
if [[ ${#screenshot_files[@]} -gt 0 ]]; then
|
||||
screenshots_json=$(
|
||||
for idx in "${!screenshot_files[@]}"; do
|
||||
name="$(basename "${screenshot_files[$idx]}")"
|
||||
printf ' "%s/releases/%s/screenshots/%s"' "$download_base_url" "$version" "$name"
|
||||
if (( idx < ${#screenshot_files[@]} - 1 )); then
|
||||
printf ',\n'
|
||||
else
|
||||
printf '\n'
|
||||
fi
|
||||
done
|
||||
)
|
||||
screenshots_json="[
|
||||
$screenshots_json
|
||||
]"
|
||||
fi
|
||||
|
||||
cat > "$release_root/version.json" <<EOF
|
||||
{
|
||||
"version": "$version",
|
||||
"channel": "$channel",
|
||||
"publishedAt": "$published_at",
|
||||
"notesUrl": "$download_base_url/releases/$version/RELEASE_NOTES.ko.md",
|
||||
"releaseUrl": "$release_url",
|
||||
"platforms": {
|
||||
$platforms_json
|
||||
},
|
||||
"screenshots": $screenshots_json$top_level_windows_alias
|
||||
}
|
||||
EOF
|
||||
|
||||
cp "$release_root/version.json" "$latest_root/version.json"
|
||||
cp "$release_root/version.json" "$latest_root/latest.json"
|
||||
touch "$latest_root/.gitkeep"
|
||||
|
||||
echo "Prepared release bundle:"
|
||||
echo " release-assets/releases/$version"
|
||||
echo " release-assets/latest"
|
||||
190
scripts/release/release-publish-forge.sh
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-publish-forge.sh --version v0.2.0-alpha.1 [options]
|
||||
|
||||
Options:
|
||||
--base-url <url> Forge base URL. Example: https://forge.example.com
|
||||
--repo <owner/name> Repository in owner/name form
|
||||
--token <token> Gitea API token
|
||||
--dry-run Print planned uploads without calling the API
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
base_url=""
|
||||
repo_full_name=""
|
||||
token="${FORGE_RELEASE_TOKEN:-${GITEA_RELEASE_TOKEN:-}}"
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--base-url)
|
||||
base_url="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
repo_full_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--token)
|
||||
token="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="true"
|
||||
shift
|
||||
;;
|
||||
-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)"
|
||||
release_root="$repo_root/release-assets/releases/$version"
|
||||
|
||||
if [[ ! -d "$release_root" ]]; then
|
||||
echo "Release bundle not found: $release_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$base_url" ]]; then
|
||||
if [[ "$origin_url" =~ ^(https?://[^/]+)/(.+)\.git$ ]]; then
|
||||
base_url="${BASH_REMATCH[1]}"
|
||||
elif [[ "$origin_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
|
||||
base_url="https://${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$repo_full_name" ]]; then
|
||||
if [[ "$origin_url" =~ ^https?://[^/]+/(.+)\.git$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
elif [[ "$origin_url" =~ ^git@[^:]+:(.+)\.git$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$base_url" || -z "$repo_full_name" ]]; then
|
||||
echo "Unable to infer forge base URL or repository name from origin remote." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" != "true" && -z "$token" ]]; then
|
||||
echo "A Gitea API token is required. Use --token or FORGE_RELEASE_TOKEN." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api_root="${base_url%/}/api/v1/repos/${repo_full_name}"
|
||||
release_api="${api_root}/releases"
|
||||
tag_api="${release_api}/tags/${version}"
|
||||
pre_release="false"
|
||||
|
||||
case "$version" in
|
||||
*alpha*|*beta*|*rc*)
|
||||
pre_release="true"
|
||||
;;
|
||||
esac
|
||||
|
||||
mapfile -t asset_files < <(
|
||||
find "$release_root" -type f \
|
||||
\( -name '*.zip' -o -name '*.apk' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
|
||||
| sort
|
||||
)
|
||||
|
||||
if [[ ${#asset_files[@]} -eq 0 ]]; then
|
||||
echo "No release assets found in $release_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Forge release target: $base_url/$repo_full_name"
|
||||
echo "Version: $version"
|
||||
printf 'Assets:\n'
|
||||
printf ' - %s\n' "${asset_files[@]#$release_root/}"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
auth_header="Authorization: token $token"
|
||||
tmp_response="$(mktemp)"
|
||||
trap 'rm -f "$tmp_response"' EXIT
|
||||
|
||||
existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' -H "$auth_header" "$tag_api")"
|
||||
if [[ "$existing_status" == "200" ]]; then
|
||||
existing_release_id="$(python3 - <<'PY' "$tmp_response"
|
||||
import json, sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
print(json.load(fh)["id"])
|
||||
PY
|
||||
)"
|
||||
curl -sS -X DELETE -H "$auth_header" "${release_api}/${existing_release_id}" >/dev/null
|
||||
fi
|
||||
|
||||
release_body=$'Windows와 Android 클라이언트 산출물을 병렬로 정리한 릴리즈입니다.\n\n'
|
||||
release_body+=$'동일 버전 번호 아래 OS별 자산을 함께 게시하며, 최신 다운로드 채널은 download-vs-messanger.phy.kr에서 운영합니다.'
|
||||
|
||||
create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release"
|
||||
import json, sys
|
||||
version, body, prerelease = sys.argv[1], sys.argv[2], sys.argv[3] == "true"
|
||||
print(json.dumps({
|
||||
"tag_name": version,
|
||||
"name": version,
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": prerelease
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
|
||||
create_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$create_payload" \
|
||||
"$release_api")"
|
||||
|
||||
if [[ "$create_status" != "201" ]]; then
|
||||
echo "Failed to create forge release. HTTP $create_status" >&2
|
||||
cat "$tmp_response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_id="$(python3 - <<'PY' "$tmp_response"
|
||||
import json, sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
print(json.load(fh)["id"])
|
||||
PY
|
||||
)"
|
||||
|
||||
for asset in "${asset_files[@]}"; do
|
||||
name="$(basename "$asset")"
|
||||
curl -sS \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary @"$asset" \
|
||||
"${release_api}/${release_id}/assets?name=${name}" >/dev/null
|
||||
done
|
||||
|
||||
echo "Published forge release $version with ${#asset_files[@]} assets."
|
||||
100
scripts/release/release-upload-assets.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-upload-assets.sh --version v0.2.0-alpha.1 --host example.com --user deploy [options]
|
||||
|
||||
Options:
|
||||
--target <path> Remote download root. Default: /srv/vs-messanger/download
|
||||
--ssh-key <path> Private key used for SSH/rsync
|
||||
--dry-run Print the rsync plan without changing the server
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
host=""
|
||||
user=""
|
||||
target="/srv/vs-messanger/download"
|
||||
ssh_key=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
host="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
target="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-key)
|
||||
ssh_key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$version" || -z "$host" || -z "$user" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
release_root="$repo_root/release-assets/releases/$version"
|
||||
latest_root="$repo_root/release-assets/latest"
|
||||
|
||||
if [[ ! -d "$release_root" || ! -d "$latest_root" ]]; then
|
||||
echo "Prepared release bundle not found for version $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -n "$ssh_key" ]]; then
|
||||
ssh_cmd+=(-i "$ssh_key")
|
||||
fi
|
||||
|
||||
rsync_opts=(-az)
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
rsync_opts+=(--dry-run)
|
||||
else
|
||||
rsync_opts+=(--delete)
|
||||
fi
|
||||
|
||||
target_host="$user@$host"
|
||||
rsh="${ssh_cmd[*]}"
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$target/releases/$version' '$target/latest' '$target/windows/latest' '$target/android/latest'"
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$release_root"/ "$target_host:$target/releases/$version/"
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root"/ "$target_host:$target/latest/"
|
||||
if [[ -d "$latest_root/windows" ]]; then
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root/windows/" "$target_host:$target/windows/latest/"
|
||||
fi
|
||||
if [[ -d "$latest_root/android" ]]; then
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root/android/" "$target_host:$target/android/latest/"
|
||||
fi
|
||||
|
||||
echo "Uploaded release assets for $version to $target_host:$target"
|
||||
5
scripts/upload-release-assets.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-upload-assets.sh" "$@"
|
||||
37
src/PhysOn.Api/Auth/ClaimsPrincipalExtensions.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using PhysOn.Application.Exceptions;
|
||||
|
||||
namespace PhysOn.Api.Auth;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
public static Guid RequireAccountId(this ClaimsPrincipal principal)
|
||||
{
|
||||
var raw = principal.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||
?? principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return ParseGuid(raw, "invalid_account_claim");
|
||||
}
|
||||
|
||||
public static Guid RequireSessionId(this ClaimsPrincipal principal)
|
||||
{
|
||||
var raw = principal.FindFirstValue("sid");
|
||||
return ParseGuid(raw, "invalid_session_claim");
|
||||
}
|
||||
|
||||
public static Guid RequireDeviceId(this ClaimsPrincipal principal)
|
||||
{
|
||||
var raw = principal.FindFirstValue("did");
|
||||
return ParseGuid(raw, "invalid_device_claim");
|
||||
}
|
||||
|
||||
private static Guid ParseGuid(string? raw, string code)
|
||||
{
|
||||
if (Guid.TryParse(raw, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new AppException(code, "인증 정보가 올바르지 않습니다.", System.Net.HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||