Compare commits

..

13 Commits

Author SHA1 Message Date
574a6ee277 fix: DPI 스케일링 정밀도 개선 및 토큰 형식 검증
- DPI 계산을 float64 기반으로 변경하여 반올림 정확도 향상
- JWT 토큰 3-part 형식 사전 검증 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 21:40:12 +09:00
b71c0d7baf fix: URI 스킴 검증 추가
handleURI에서 parsed.Scheme이 "a301"인지 검증하여
잘못된 스킴의 URI 입력 방지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:49:54 +09:00
a0face763a fix: 다운로드·파일 처리 버그 수정 및 에러 핸들링 강화
- 416 응답 시 무한 재귀를 doDownloadRequest 반복문으로 교체
- copyFile에서 out.Close() 에러를 반환하도록 수정
- ContentLength=-1일 때 잘못된 total 계산 방지
- fetchServerInfo 재시도 로직을 errNoRetry 타입 에러로 교체
- extractZip에서 최상위 디렉토리 엔트리 스킵 처리
- moveContents 크로스 드라이브 복사 후 원본 파일 삭제
- 진행률 100% 초과 클램핑
- install() SetStringValue 에러 체크 추가
- handleURI에서 gameDir 미존재 시 MkdirAll 방어 코드 추가
- ensureGame에서 os.Stat 비정상 에러 명시적 처리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:08:39 +09:00
48df55a82e fix: 보안 강화, 안정성 및 UX 개선
- 토큰 전달 방식 변경: 명령줄 인자(-token) → 환경변수(A301_TOKEN)로 프로세스 목록 노출 방지
- 고정 설치 경로: %LOCALAPPDATA%\A301\로 런처 복사 후 레지스트리 등록 (Downloads 정리 시 깨짐 방지)
- zip 추출 시 symlink 엔트리 스킵 (경로 탈출 방지)
- fetchServerInfo 3회 재시도 (exponential backoff)
- 다운로드 이어받기: Range 헤더 지원, 취소/오류 시 임시 파일 유지
- 416 응답 시 서버 파일 변경 감지하여 처음부터 재다운로드
- 단일 인스턴스 UX: 기존 창 FindWindow+SetForegroundWindow로 활성화
- uninstall 시 설치 디렉토리 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:10:11 +09:00
9bb422f9b2 fix: 보안 강화 및 안정성 개선
- extractZip: io.LimitReader 적용으로 zip bomb 방어 (개별 파일 4GB 제한)
- moveContents: cross-drive 복사 실패 시 부분 파일 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 09:51:43 +09:00
9fb98b0028 fix: 보안 및 안정성 보강
- Zip Slip 경로 검증 추가
- HTTP 상태 코드 검증 (doDownload)
- HTTP 타임아웃 설정 (API/다운로드 클라이언트 분리)
- 다운로드 URL 스킴 검증 (https/http만 허용)
- 리다이렉트 스킴 제한 (CheckRedirect)
- 다운로드 크기 제한 (2GB)
- fetchServerInfo 응답 크기 제한 (1MB)
- 다운로드 후 해시 검증
- 다중 인스턴스 실행 방지 (CreateMutexW)
- 다운로드 취소 기능 (wmClose 핸들러)
- 압축 해제 실패 시 잔여 파일 정리 (임시 디렉토리 추출)
- 도달 불가능한 dead code 및 미사용 코드 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:04:45 +09:00
10651d294a docs: README, CLAUDE.md 작성
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:20:43 +09:00
f77a7e0e23 feat: 게임 가제 'One of the plans' UI 텍스트 적용
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:18:13 +09:00
28c1b377df feat: 다운로드 창 다크 테마 적용
- 배경 #2E2C2F, 타이틀 #BACDB0(강조색), 상태 텍스트 밝은 회색
- WM_CTLCOLORSTATIC으로 STATIC 컨트롤 색상 제어
- SetWindowTheme + PBM_SETBARCOLOR로 진행 막대 색상 변경
- "A301" 타이틀 레이블(13pt bold) + 상태 레이블(9pt) 분리
- 웹사이트 색상 팔레트와 통일

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:14:39 +09:00
22b5efdaab feat: DPI 인식 및 진행 막대 UI 추가
- SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)로 고DPI 디스플레이 지원
- GetDpiForSystem()으로 시스템 DPI 조회 후 모든 크기 동적 스케일링
- 다운로드 진행창에 msctls_progress32 진행 막대 추가 (0~100%)
- Segoe UI 9pt 폰트를 DPI에 맞게 CreateFontIndirectW로 생성
- 4K(200% / 192 DPI)와 FHD(100% / 96 DPI) 모두 동일한 시각적 크기 보장

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 00:08:06 +09:00
6fafe1f3af fix: serverInfoURL을 API 서버 주소로 수정
프론트엔드 nginx에 /api/ 프록시 없음 → index.html 반환 → JSON 파싱 실패
직접 API 서버(a301.api.tolelom.xyz)로 요청하도록 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 23:40:48 +09:00
90fcc0f94e fix: 서버 상태 코드별 명확한 에러 메시지 처리
- 404: 게임이 아직 준비되지 않았습니다
- 4xx/5xx: 서버 오류 (HTTP 상태코드)
- JSON 파싱 실패는 진짜 비정상 응답일 때만 표시

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 23:38:24 +09:00
ad9d372d7c feat: 자동 다운로드/설치 기능 추가
- 게임 파일 없거나 해시 불일치 시 서버에서 zip 자동 다운로드
- Win32 진행률 창으로 다운로드 진행률 표시 (X% 업데이트)
- zip 압축 해제 후 게임 실행 (런처 자신은 덮어쓰기 방지)
- 더블클릭 시 프로토콜만 등록 (토큰 없으므로 다운로드 불필요)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 23:00:19 +09:00
4 changed files with 1296 additions and 140 deletions

288
ANALYSIS.md Normal file
View File

@@ -0,0 +1,288 @@
# A301 시스템 통합 분석 및 개선 계획
> 분석일: 2026-03-06
> 대상: 런처(Go) / 서버(Go+Fiber) / 웹 클라이언트(React+Vite)
---
## 1. 시스템 전체 흐름
```
[웹 클라이언트] [서버] [런처] [게임 클라이언트]
| | | |
|-- POST /auth/login ----->| | |
|<-- {token, refresh} -----| | |
| | | |
|-- GET /download/info --->| | |
|<-- {url,hash,version} ---| | |
| | | |
|== a301://launch?token= =|========================>| |
| | | |
| |<-- GET /download/info --| |
| |-- {url,hash,version} -->| |
| | |-- SHA256 비교 |
| |<-- GET /download/file --| (불일치시) |
| |-- game.zip ------------>| |
| | |-- 압축 해제 |
| | |-- A301.exe -token xxx ->|
| | | |
| |<--- /internal/chain ----|-- 게임 내 API 호출 ---->|
```
---
## 2. 발견된 버그 및 문제점
### ~~2.1 [RESOLVED] 런처: Zip Slip 경로 검증 로직~~
> 재검증 결과 로직이 정상임. `!A && B`는 De Morgan 법칙에 의해 "descendant이거나 self"일 때만 허용하는 올바른 조건.
---
### ~~2.2 [RESOLVED] 서버: /auth/verify Redis 세션 검증~~
> 재검증 결과 `service.go:212-216`에서 이미 Redis 세션을 확인하고 있음.
---
### 2.3 [HIGH] 런처: 서버 fileHash 비어있으면 검증 우회
**위치**: `launcher/main.go` ensureGame()
**문제**: `serverInfo.FileHash == ""`이면 해시 비교를 건너뛰고 기존 파일을 그대로 사용. 서버에서 zip에 A301.exe가 없으면 fileHash가 빈 문자열로 저장됨.
**연쇄 시나리오**:
1. 관리자가 A301.exe 없는 zip 업로드 → 서버 fileHash = ""
2. 런처가 info 조회 → hash 빈 문자열 → 검증 스킵
3. 로컬 A301.exe가 어떤 파일이든 실행됨
**수정안**:
- 서버: zip에 A301.exe 없으면 업로드 거부 (현재는 빈 해시 허용)
- 런처: fileHash가 비어있으면 에러 처리
**심각도**: HIGH (보안/무결성)
---
### 2.4 [HIGH] 런처: 토큰이 프로세스 명령줄에 노출
**위치**: `launcher/main.go` handleURI()
**문제**: `A301.exe -token <JWT>`로 실행 → tasklist, Process Explorer 등에서 토큰 노출
**수정안**:
- 환경변수로 전달: `os.Setenv("A301_TOKEN", token)` 후 실행
- 또는 임시 파일에 토큰 쓰고 파일 경로 전달
**심각도**: HIGH (보안)
---
### 2.5 [HIGH] 블록체인 트랜잭션 멱등성 미보장
**위치**: `server/internal/chain/service.go` 모든 트랜잭션 함수
**문제**: 동일 요청 재전송 시 중복 처리 (토큰 이중 전송, NFT 이중 민팅)
- 네트워크 타임아웃 후 클라이언트 재시도 시 발생 가능
**수정안**:
- 클라이언트에서 idempotency key 전송
- 서버에서 Redis로 중복 요청 감지 (TTL 기반)
**심각도**: HIGH (데이터 무결성)
---
### 2.6 [HIGH] 서버: Rate Limiting 없음
**위치**: 전체 API
**문제**: 토큰 리프레시, 로그인, 다운로드 등 모든 엔드포인트에 속도 제한 없음.
- 브루트포스 로그인 공격 가능
- 리프레시 토큰 재사용 공격 가능
- 다운로드 대역폭 남용 가능
**수정안**: Fiber 미들웨어로 rate limiter 추가 (IP 기반 + 토큰 기반)
**심각도**: HIGH (보안)
---
### 2.7 [MEDIUM] 웹 클라: 관리자 권한 클라이언트 사이드만 검증
**위치**: `client/src/components/AdminRoute.jsx`
**문제**: localStorage의 `role`을 조작하면 관리자 UI 접근 가능. API 호출은 서버에서 차단되지만, UI 자체가 노출됨.
**영향**: 관리자 기능 목록/구조가 일반 사용자에게 보임
**수정안**: 서버 API가 이미 권한 체크하므로 실질적 위험은 낮음. 다만 관리자 라우트 로딩 시 서버에서 role 재확인하면 더 안전.
**심각도**: MEDIUM (UX/보안)
---
### 2.8 [MEDIUM] 런처: Symlink 공격 미차단
**위치**: `launcher/main.go` extractZip()
**문제**: zip 내 심볼릭 링크를 통한 디렉토리 탈출 미검증
- `filepath.Clean()`은 심볼릭 링크를 해석하지 않음
**수정안**: zip 엔트리의 파일 모드에서 symlink 비트 확인 → 거부
**심각도**: MEDIUM (보안)
---
### 2.9 [MEDIUM] 런처: 네트워크 재시도 로직 없음
**위치**: `launcher/main.go` fetchServerInfo(), doDownload()
**문제**: 일시적 네트워크 오류 시 즉시 실패. 다운로드 99%에서 끊기면 처음부터 재시작.
**수정안**:
- fetchServerInfo: 3회 재시도 (exponential backoff)
- doDownload: Range 헤더 지원으로 이어받기 구현
**심각도**: MEDIUM (안정성/UX)
---
### 2.10 [MEDIUM] 런처: 단일 인스턴스 체크 시 사용자 피드백 없음
**위치**: `launcher/main.go` main() mutex 체크
**문제**: 이미 실행 중이면 조용히 종료 → 사용자는 클릭했는데 아무 반응 없음
**수정안**: 기존 인스턴스 창을 foreground로 가져오기 (`FindWindow` + `SetForegroundWindow`)
**심각도**: MEDIUM (UX)
---
### 2.11 [MEDIUM] 웹 클라: 다운로드 정보 실패 시 무음 처리
**위치**: `client/src/components/DownloadSection.jsx`
**문제**: `/api/download/info` 실패 시 console.error도 없이 "런처 준비 중" 메시지 표시. 디버깅 불가.
**수정안**: 에러 로깅 추가 + 재시도 버튼 제공
**심각도**: MEDIUM (UX/디버깅)
---
### 2.12 [MEDIUM] 서버: 파일 업로드 크기 제한 없음
**위치**: `server/internal/download/handler.go`
**문제**: StreamRequestBody 활성화되어 있지만 별도 크기 제한 없음. 수십 GB 파일 업로드 시 디스크 고갈 가능.
**수정안**: Fiber의 BodyLimit 미들웨어 적용 (예: 4GB)
**심각도**: MEDIUM (안정성)
---
### 2.13 [LOW] 서버: Username 대소문자 구분
**위치**: `server/internal/auth/model.go`
**문제**: "User"와 "user"가 별도 계정으로 등록 가능. 블록체인 username 조회 시 혼동 가능.
**수정안**: 등록/로그인 시 username을 항상 소문자로 정규화
**심각도**: LOW (데이터 일관성)
---
### 2.14 [LOW] 서버: Wallet 암호화 키 시작 시 미검증
**위치**: `server/internal/chain/service.go`
**문제**: WALLET_ENCRYPTION_KEY가 잘못되면 런타임에 지갑 복호화 실패. 서버 시작 시 검증하지 않음.
**수정안**: 서비스 초기화 시 키 길이/형식 검증 (64 hex = 32 bytes) → fail fast
**심각도**: LOW (운영 안정성)
---
### 2.15 [LOW] 웹 클라: 비밀번호 검증이 약함
**위치**: `client/src/pages/RegisterPage.jsx`
**문제**: 6자 이상만 체크. 복잡도 요구 없음.
**수정안**: 서버 측 검증에 의존하더라도, 클라이언트에서도 실시간 피드백 제공
**심각도**: LOW (보안/UX)
---
## 3. 통합 흐름 검증 결과
### 3.1 정상 시나리오 (Happy Path) - OK
| 단계 | 동작 | 상태 |
|------|------|------|
| 웹 로그인 | POST /auth/login → JWT 발급 | OK |
| 다운로드 정보 | GET /download/info → hash/url/version | OK |
| 런처 호출 | a301://launch?token=xxx | OK |
| 런처 서버 조회 | GET /download/info | OK |
| 해시 비교 | 로컬 A301.exe SHA256 vs 서버 fileHash | OK |
| 다운로드 | GET /download/file → zip | OK |
| 압축 해제 | extractZip → moveContents | OK |
| 게임 실행 | A301.exe -token xxx | OK |
| 게임→서버 | /internal/chain/* (X-API-Key) | OK |
### 3.2 엣지 케이스 검증
| 시나리오 | 기대 동작 | 실제 동작 | 판정 |
|----------|-----------|-----------|------|
| 서버 다운 시 런처 호출 | 에러 표시 | MessageBox 표시 후 종료 | OK |
| 토큰 만료 후 게임 시작 | 리프레시 후 전달 | 웹 클라: localStorage에서 읽음 (최신 토큰) | PARTIAL - 리프레시 타이밍에 따라 만료 토큰 전달 가능 |
| 다운로드 중 네트워크 끊김 | 재시도 또는 복구 | 즉시 실패, 재시작 필요 | FAIL |
| 동시에 런처 2개 실행 | 하나만 실행 | Mutex로 차단, 피드백 없음 | PARTIAL |
| A301.exe 없는 zip 업로드 | 업로드 거부 | ~~fileHash="" 로 저장~~ → 업로드 거부 (수정됨) | **FIXED** |
| 로그아웃 후 토큰으로 verify | 무효 판정 | Redis 세션 확인으로 무효 판정 (기존 정상) | OK |
| 관리자가 대용량 파일 업로드 | 크기 제한 | 제한 없음 | FAIL |
| 블록체인 트랜잭션 중복 요청 | 한 번만 처리 | 중복 처리됨 | FAIL |
---
## 4. 개선 우선순위 로드맵
### Phase 1: 보안 긴급 패치
| # | 항목 | 대상 | 상태 |
|---|------|------|------|
| 1 | ~~Zip Slip 검증 로직 수정~~ | 런처 | 재검증: 정상 |
| 2 | ~~/verify Redis 세션 확인 추가~~ | 서버 | 재검증: 이미 구현됨 |
| 3 | fileHash 빈 문자열 시 업로드 거부 | 서버 | **완료** |
| 4 | 토큰 전달 방식 변경 (명령줄 → 환경변수) | 런처 | **완료** |
| 5 | Rate limiting 추가 | 서버 | **완료** |
| 6 | Symlink 차단 | 런처 | **완료** |
### Phase 2: 안정성 개선
| # | 항목 | 대상 | 상태 |
|---|------|------|------|
| 7 | 네트워크 재시도 로직 (fetchServerInfo) | 런처 | **완료** |
| 8 | 블록체인 트랜잭션 멱등성 키 | 서버 | **완료** |
| 9 | 파일 업로드 크기 제한 | 서버 | **완료** |
| 10 | 런처 단일 인스턴스 UX 개선 | 런처 | **완료** |
### Phase 3: UX 개선
| # | 항목 | 대상 | 상태 |
|---|------|------|------|
| 11 | 다운로드 정보 실패 시 재시도 버튼 | 웹 클라 | **완료** |
| 12 | 비밀번호 복잡도 실시간 피드백 | 웹 클라 | **완료** |
| 13 | Username 대소문자 정규화 | 서버 | **완료** |
| 14 | Wallet 암호화 키 시작 시 검증 | 서버 | 이미 구현됨 |
| 15 | 다운로드 이어받기 (Range 헤더) | 런처 | **완료** |
---
## 5. 컴포넌트별 요약
### 런처 (Go/Win32)
- **강점**: DPI 인식, 원자적 파일 이동, 다운로드 진행 UI, 2GB 크기 제한
- **약점**: Zip Slip 검증 오류, 토큰 노출, 재시도 없음, symlink 미차단
### 서버 (Go/Fiber)
- **강점**: 모듈화된 구조, JWT+Redis 세션, AES-256 지갑 암호화, 원자적 파일 업로드
- **약점**: verify 세션 미검증, rate limiting 없음, 트랜잭션 멱등성 없음, 빈 해시 허용
### 웹 클라이언트 (React/Vite)
- **강점**: 토큰 자동 리프레시, 관리자 UI 분리, 다크 테마, SPA 라우팅
- **약점**: 에러 피드백 부족, 관리자 클라이언트 사이드만 검증, 약한 비밀번호 정책

60
CLAUDE.md Normal file
View File

@@ -0,0 +1,60 @@
# CLAUDE.md
## Build Command
```bash
C:\Users\98kim\sdk\go1.25.1\bin\go.exe build -ldflags="-H windowsgui -s -w" -o launcher.exe .
```
`-H windowsgui` 필수 — 없으면 실행 시 콘솔 창이 함께 열림.
## Tech Stack
- **Go** 단일 파일 (`main.go`)
- **Win32 API** — `user32.dll`, `gdi32.dll`, `comctl32.dll`, `uxtheme.dll`, `shell32.dll`
- `golang.org/x/sys/windows` + `windows/registry`
## Project Purpose
"One of the plans" Windows 런처.
`a301://` 커스텀 URI 프로토콜 등록 → 게임 자동 다운로드/업데이트/실행.
## Architecture
모든 로직이 `main.go` 단일 파일에 있음:
- **`enableDPIAwareness()`** — `SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)` 호출. `main()` 첫 줄에서 실행.
- **`downloadWithProgress()`** — Win32 메시지 루프 직접 운영. 반드시 메인 고루틴에서 호출 (`runtime.LockOSThread`).
- **`progressWndProc()`** — `WM_CTLCOLORSTATIC`으로 다크 테마 적용. `hBrushBg` 전역 변수 참조.
- **`setProgress(text, pct)`** — 다운로드 고루틴에서 호출해 레이블 텍스트와 진행 막대 동시 업데이트.
- **`fetchServerInfo()`** — `https://a301.api.tolelom.xyz/api/download/info` 조회.
- **`ensureGame()`** — `A301.exe` SHA256 해시 비교 후 불일치 시 재다운로드.
## UI Details
- DPI: `GetDpiForSystem()``dpiScale(px, dpi)` 함수로 모든 크기 동적 계산
- 진행 막대 색: `SetWindowTheme("", "")` 비주얼 스타일 비활성화 후 `PBM_SETBARCOLOR` 적용
- 다크 배경: `WM_CTLCOLORSTATIC` 핸들러에서 `hBrushBg` 반환 + `SetBkMode(TRANSPARENT)`
- 타이틀 레이블(`titleLabelHwnd`)만 강조색, 나머지 STATIC은 밝은 회색
## Key Constants
```go
serverInfoURL = "https://a301.api.tolelom.xyz/api/download/info"
gameExeName = "A301.exe" // 기술 식별자 — 게임 표기명과 별개
protocolName = "a301" // 기술 식별자
```
## Install Location
- `install()` 시 런처를 `%LOCALAPPDATA%\A301\launcher.exe`로 복사 후 해당 경로를 레지스트리에 등록.
- 게임 파일(`A301.exe` 등)도 `%LOCALAPPDATA%\A301\`에 설치됨.
- 사용자가 원본 다운로드 파일을 삭제해도 프로토콜 핸들러가 정상 동작.
- 토큰은 명령줄이 아닌 `A301_TOKEN` 환경변수로 게임에 전달.
## Notes
- `extractZip()` — zip 내 최상위 디렉토리 1단계 제거 후 추출. `launcher.exe` 자신은 덮어쓰기 방지. Symlink 엔트리는 스킵.
- 레지스트리는 `HKCU` (현재 사용자) 에만 쓰므로 관리자 권한 불필요.
- `fetchServerInfo()` — 3회 재시도 (exponential backoff).
- `doDownload()` — Range 헤더로 이어받기 지원. 취소/오류 시 임시 파일 유지.

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
# One of the plans — Launcher
Windows 전용 게임 런처. `a301://` 커스텀 URI 프로토콜을 등록하고, 웹에서 게임 시작 버튼 클릭 시 게임을 자동 다운로드/업데이트/실행합니다.
## 빌드
```bash
# Go 1.24+ 필요
go build -ldflags="-H windowsgui -s -w" -o launcher.exe .
```
`-H windowsgui` — 콘솔 창 없이 실행 (Win32 GUI 앱)
`-s -w` — 디버그 심볼 제거 (바이너리 크기 축소)
## 사용법
| 실행 방법 | 동작 |
|-----------|------|
| `launcher.exe` (인자 없음) | `a301://` 프로토콜 등록 확인 다이얼로그 |
| `launcher.exe install` | 프로토콜 강제 등록 |
| `launcher.exe uninstall` | 프로토콜 제거 |
| `launcher.exe "a301://launch?token=<JWT>"` | 게임 실행 (웹에서 자동 호출) |
## 게임 실행 흐름
```
웹 "게임 시작" 클릭
└─ a301://launch?token=JWT
└─ launcher.exe 실행
├─ 서버에서 최신 버전 정보 조회 (fileHash, url)
├─ A301.exe 없음 또는 해시 불일치 → 다운로드 창 표시
│ ├─ game.zip 다운로드 (진행률 표시)
│ └─ 압축 해제 (launcher.exe 자신은 덮어쓰기 방지)
└─ A301.exe -token <JWT> 실행
```
## UI
다크 테마 Win32 창 (배경 `#2E2C2F`, 강조색 `#BACDB0`).
4K / FHD 해상도 모두 자연스럽게 표시 (DPI 인식 + 동적 스케일링).
## 배포 구조
유저가 받는 구성:
```
(게임 폴더)/
├── launcher.exe ← 이 파일
└── A301.exe ← 런처가 자동 다운로드
```
`launcher.exe`를 처음 실행하면 레지스트리에 `a301://` 프로토콜을 등록하고, 이후 웹에서 게임 시작 버튼을 누를 때마다 자동으로 이 런처가 실행됩니다.
## 레지스트리 등록 경로
```
HKCU\Software\Classes\a301\
HKCU\Software\Classes\a301\shell\open\command → "launcher.exe" "%1"
```
## 의존성
- `golang.org/x/sys/windows` — Win32 API
- `golang.org/x/sys/windows/registry` — 레지스트리 접근

1012
main.go

File diff suppressed because it is too large Load Diff