From c8b061959bf33cca21b0b08b3074f0b349b07b54 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Tue, 18 Aug 2026 00:14:39 +0900 Subject: [PATCH] =?UTF-8?q?MEDIABROWSER=20=EB=B0=B0=ED=8F=AC=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20+=20=EA=B3=B5=EA=B0=9C=20=EB=A6=B4?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20(DEPLOYMENT=5FPLAN=20=C2=A75-ter,=20BACKLOG=2012?= =?UTF-8?q?=EC=B0=A8)=20=E2=80=94=20site/:=20=ED=94=84=EB=A1=9C=EB=AA=A8?= =?UTF-8?q?=EC=85=98(=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EA=B8=80=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=AA=A8=ED=94=BC=EC=A6=98,=20CSS=20scroll-driven=20?= =?UTF-8?q?=ED=8C=A8=EB=9F=B4=EB=9E=99=EC=8A=A4+IO=20=EB=93=B1=EC=9E=A5,?= =?UTF-8?q?=20=EB=AA=A8=EC=85=98=C2=B7=ED=88=AC=EB=AA=85=EB=8F=84=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C=20=EC=A1=B4=EC=A4=91,=20no-JS=20=EB=B0=B1?= =?UTF-8?q?=EC=A7=80=20=EB=B0=A9=EC=A7=80=20html.js=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8)=C2=B7=EB=A6=B4=EB=A6=AC=EC=8A=A4(=ED=94=8C=EB=9E=AB?= =?UTF-8?q?=ED=8F=BC=20=EA=B7=B8=EB=A3=B9,=20OS=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EA=B0=90=EC=A7=80=20=EA=B6=8C=EC=9E=A5,=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20=EB=B2=84=EC=A0=84=20=EC=A0=91=ED=9E=98,=20=EB=B9=88=20?= =?UTF-8?q?=EC=83=81=ED=83=9C)=C2=B7=EC=82=AC=EC=9A=A9=20=EC=95=88?= =?UTF-8?q?=EB=82=B4(=EC=84=A4=EC=B9=98=E2=86=92=EC=B2=AB=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=E2=86=92=EA=B4=80=EB=A6=AC=E2=86=92?= =?UTF-8?q?=ED=9C=B4=EB=8C=80=ED=8F=B0=20=EC=97=B0=EA=B2=B0=E2=86=92FAQ)?= =?UTF-8?q?=203=ED=8E=98=EC=9D=B4=EC=A7=80.=20=ED=9E=88=EC=96=B4=EB=A1=9C/?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20VD=5FDATA=5FDIR=20=EA=B2=A9=EB=A6=AC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=EB=A1=9C=20=EB=9D=84=EC=9A=B4=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EC=95=B1=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=BA=A1=EC=B2=98(=EB=B0=9D=EA=B8=B0=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95).=20=EB=A6=B4=EB=A6=AC=EC=8A=A4=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=8A=94=20CORS=20=ED=9A=8C=ED=94=BC?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C?= =?UTF-8?q?=20fetch-releases.mjs=20=EA=B0=80=20releases.json=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=A0=81=20=EC=83=9D=EC=84=B1(git=20?= =?UTF-8?q?=EB=AC=B4=EC=8B=9C).=20=EA=B3=B5=EA=B0=9C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20Video-Downloader/releases=20=EC=8B=A0=EC=84=A4(?= =?UTF-8?q?=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EC=86=8C=EC=8A=A4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=EB=8A=94=20=EC=9E=90=EC=82=B0=20=EC=9D=B5?= =?UTF-8?q?=EB=AA=85=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80)=20=E2=80=94=20CI=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20env=20RELEASE=5FREPO=20=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98,=20draft=20=EC=9D=B5=EB=AA=85=20=EB=B9=84=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=EC=9D=B4=EB=9D=BC=20QA=20=ED=9B=84=20Publish=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EC=9C=A0=EC=A7=80.=20build/deploy-site.ps?= =?UTF-8?q?1(wrangler=20pages=20deploy)=20=EC=B6=94=EA=B0=80,=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C:=20https://mediabrowser-7r4.pages.dev=20?= =?UTF-8?q?=E2=80=94=20Playwright=20=EB=A1=9C=203=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=84=EC=88=98=20=EC=8A=A4=ED=81=AC=EB=A6=B0?= =?UTF-8?q?=EC=83=B7=20=EA=B2=80=EC=A6=9D(=EB=B9=88=20=EC=83=81=ED=83=9C+?= =?UTF-8?q?=EC=83=98=ED=94=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=20=ED=8F=AC=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .forgejo/workflows/release.yml | 3 + .gitignore | 5 + build/ci/upload-release-asset.ps1 | 4 +- build/ci/upload-release-asset.sh | 3 +- build/deploy-site.ps1 | 29 +++ docs/BACKLOG.md | 22 +++ docs/DEPLOYMENT_PLAN.md | 22 +++ site/assets/icons.svg | 1 + site/assets/panel-downloads.png | Bin 0 -> 7477 bytes site/fetch-releases.mjs | 35 ++++ site/guide.html | 149 +++++++++++++++ site/index.html | 156 ++++++++++++++++ site/releases.html | 76 ++++++++ site/site.js | 131 ++++++++++++++ site/styles.css | 289 ++++++++++++++++++++++++++++++ 15 files changed, 923 insertions(+), 2 deletions(-) create mode 100644 build/deploy-site.ps1 create mode 100644 site/assets/icons.svg create mode 100644 site/assets/panel-downloads.png create mode 100644 site/fetch-releases.mjs create mode 100644 site/guide.html create mode 100644 site/index.html create mode 100644 site/releases.html create mode 100644 site/site.js create mode 100644 site/styles.css diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 8bc0394..b58009b 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -14,6 +14,9 @@ on: env: DOTNET_NOLOGO: "true" DOTNET_CLI_TELEMETRY_OPTOUT: "true" + # 릴리스 자산은 공개 배포 저장소로 업로드한다(소스 저장소는 비공개라 익명 다운로드 불가). + # draft 상태는 익명에게 보이지 않으므로 QA 후 수동 Publish 흐름은 그대로다. + RELEASE_REPO: "Video-Downloader/releases" jobs: # ── 관문: 전 플랫폼 빌드 전에 단위+E2E 통과 ── diff --git a/.gitignore b/.gitignore index 6e1a581..fe8d4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,8 @@ CLAUDE.local.md ._* Thumbs.db desktop.ini + +# ── 배포 사이트 산출물 (deploy-site 가 배포 시점에 생성) ───────────── +site/releases.json +.playwright-mcp/ +.wrangler/ diff --git a/build/ci/upload-release-asset.ps1 b/build/ci/upload-release-asset.ps1 index 483a3e3..f65efa5 100644 --- a/build/ci/upload-release-asset.ps1 +++ b/build/ci/upload-release-asset.ps1 @@ -1,6 +1,7 @@ # Forgejo 릴리스(draft)에 산출물을 업로드한다. 릴리스가 없으면 만들고, 동명 자산은 교체한다(재실행 멱등). # 사용: ./build/ci/upload-release-asset.ps1 -Tag v1.0.0 -Files out/a.exe, out/b.exe # 필요 환경: RELEASE_TOKEN + (Actions 기본 제공) GITHUB_SERVER_URL, GITHUB_REPOSITORY +# RELEASE_REPO 를 주면 해당 저장소로 업로드(공개 배포 저장소용), 없으면 현재 저장소 # RELEASE_PRERELEASE=true 면 prerelease 로 생성(리허설용) param( [Parameter(Mandatory)][string]$Tag, @@ -8,7 +9,8 @@ param( ) $ErrorActionPreference = 'Stop' if (-not $env:RELEASE_TOKEN) { throw "RELEASE_TOKEN 환경변수가 없습니다" } -$api = "$($env:GITHUB_SERVER_URL.TrimEnd('/'))/api/v1/repos/$($env:GITHUB_REPOSITORY)" +$repo = if ($env:RELEASE_REPO) { $env:RELEASE_REPO } else { $env:GITHUB_REPOSITORY } +$api = "$($env:GITHUB_SERVER_URL.TrimEnd('/'))/api/v1/repos/$repo" $hdr = @{ Authorization = "token $env:RELEASE_TOKEN" } # draft 릴리스는 tags/ 단건 조회에 안 잡힐 수 있어 목록에서 tag_name 으로 찾는다 diff --git a/build/ci/upload-release-asset.sh b/build/ci/upload-release-asset.sh index 54ece8c..75077ee 100755 --- a/build/ci/upload-release-asset.sh +++ b/build/ci/upload-release-asset.sh @@ -2,12 +2,13 @@ # Forgejo 릴리스(draft)에 산출물을 업로드한다. 릴리스가 없으면 만들고, 동명 자산은 교체한다(재실행 멱등). # 사용: build/ci/upload-release-asset.sh <릴리스태그> <파일...> # 필요 환경: RELEASE_TOKEN + (Actions 기본 제공) GITHUB_SERVER_URL, GITHUB_REPOSITORY +# RELEASE_REPO 를 주면 해당 저장소로 업로드(공개 배포 저장소용), 없으면 현재 저장소 # RELEASE_PRERELEASE=true 면 prerelease 로 생성(리허설용) set -euo pipefail [ $# -ge 2 ] || { echo "사용법: $0 <태그> <파일...>" >&2; exit 2; } [ -n "${RELEASE_TOKEN:-}" ] || { echo "RELEASE_TOKEN 환경변수가 없습니다" >&2; exit 2; } tag="$1"; shift -api="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}" +api="${GITHUB_SERVER_URL%/}/api/v1/repos/${RELEASE_REPO:-$GITHUB_REPOSITORY}" auth="Authorization: token ${RELEASE_TOKEN}" # draft 릴리스는 tags/ 단건 조회에 안 잡힐 수 있어 목록에서 tag_name 으로 찾는다 diff --git a/build/deploy-site.ps1 b/build/deploy-site.ps1 new file mode 100644 index 0000000..a54d59a --- /dev/null +++ b/build/deploy-site.ps1 @@ -0,0 +1,29 @@ +# MEDIABROWSER 사이트(site/)를 Cloudflare Pages 에 배포한다. +# 사용: ./build/deploy-site.ps1 [-Project mediabrowser] +# 필요: .env 의 CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID · node/npx(wrangler) +# 절차: 공개 릴리스 저장소에서 releases.json 생성 → wrangler pages deploy +param([string]$Project = "mediabrowser") + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot + +# .env 로드 (값은 출력하지 않는다) +$envFile = Join-Path $root ".env" +if (-not (Test-Path $envFile)) { throw ".env 가 없습니다. .env.example 을 복사해 채우세요." } +foreach ($line in Get-Content $envFile) { + if ($line -match '^\s*(CLOUDFLARE_API_TOKEN|CLOUDFLARE_ACCOUNT_ID)\s*=\s*(.+)$') { + Set-Item "env:$($Matches[1])" $Matches[2].Trim() + } +} +if (-not $env:CLOUDFLARE_API_TOKEN) { throw ".env 에 CLOUDFLARE_API_TOKEN 이 없습니다." } +if (-not $env:CLOUDFLARE_ACCOUNT_ID) { throw ".env 에 CLOUDFLARE_ACCOUNT_ID 가 없습니다." } + +Write-Host "1) releases.json 생성" -ForegroundColor Cyan +node (Join-Path $root "site\fetch-releases.mjs") +if ($LASTEXITCODE -ne 0) { throw "releases.json 생성 실패" } + +Write-Host "2) Cloudflare Pages 배포 ($Project)" -ForegroundColor Cyan +npx wrangler pages deploy (Join-Path $root "site") --project-name $Project --branch main --commit-dirty=true +if ($LASTEXITCODE -ne 0) { throw "wrangler 배포 실패" } + +Write-Host "완료: https://$Project.pages.dev" -ForegroundColor Green diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index bda5070..a130d7b 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -259,3 +259,25 @@ - 리허설 1차(b05e8bf): test **453/453 통과**(실창 E2E 포함 — 러너가 사용자 세션이라 창 생성 가능) · windows/linux/macos 성공 · android 실패(MSB3030). - 리허설 2차(d98c734, 수정 후): **5/5 잡 전부 성공** — `ci-rehearsal` draft prerelease 자산 7개: Setup x64 57.2MB·arm64 51.5MB / linux tar.gz x64 71.1MB·arm64 68.1MB / osx tar.gz arm64 69.6MB·x64 72.7MB / APK(arm64-v8a 단독) 23.3MB. - 정식 배포: `git tag v1.0.0 && git push origin v1.0.0` → 동일 파이프라인이 `v1.0.0` draft 릴리스 생성 → QA 후 수동 Publish (§7.5 런북 그대로). + +## 12차 (2026-08-18) — MEDIABROWSER 배포 사이트 + 공개 릴리스 저장소 +> 사용자: "릴리즈 페이지 만들자 — 프로모션(패럴랙스·글래스모피즘·라이트) + 릴리스 + 사용 안내, +> wrangler 로 Cloudflare 배포, 파일은 NAS 호스팅, 배포명은 MEDIABROWSER". DEPLOYMENT_PLAN §5-ter 참조. + +- [x] **`site/` 3페이지** — index(프로모션: 히어로 스플릿+벤토+기능 그리드+모바일+플랫폼 밴드, + CSS scroll-driven 패럴랙스 + IntersectionObserver 등장, `prefers-reduced-motion/transparency` 존중, + JS 실패 시에도 내용이 보이는 `html.js` 게이트) · releases(플랫폼 그룹·OS 자동 감지 권장·이전 버전 접힘·빈 상태) + · guide(설치→첫 다운로드→관리→휴대폰 연결→FAQ). Wanted Sans Variable + Phosphor 스프라이트. +- [x] **실앱 자산** — 히어로/가이드의 다운로드 패널 이미지는 `VD_DATA_DIR` 격리 프로필로 띄운 실제 앱 + 창 캡처(밝기 보정). 가짜 목업 없음. +- [x] **공개 릴리스 저장소** — 소스(client)가 비공개라 자산 익명 다운로드 불가 → `Video-Downloader/releases` + (공개) 신설, CI 업로드를 env `RELEASE_REPO` 로 전환. draft 는 익명에 안 보여 QA 후 Publish 흐름 유지. + 생성 직후 저장소 설명이 인코딩 깨짐(EFBFBD)으로 올라간 문제(사용자 보고)는 UTF-8 파일 기반 PATCH 로 교정. +- [x] **배포** — `build/deploy-site.ps1`(releases.json 생성 → wrangler pages deploy). + 라이브: https://mediabrowser-7r4.pages.dev (mediabrowser 서브도메인 선점 → 접미). + +### 검증 (12차) +- Playwright 로 3페이지 전체 스크린샷 검증(1440px): 히어로/벤토/기능/모바일/밴드 렌더, 릴리스 빈 상태, + 샘플 데이터 주입 렌더(플랫폼 4그룹·권장 강조·프리뷰 필), 가이드 목차·FAQ. 라이브 URL 에서 재확인. +- 도중 결함 2건 수정: fullPage 캡처로 드러난 reveal 무한 숨김 위험(no-JS 백지) → `html.js` 게이트, + 패널 캡처의 사용자 세션 노출 위험 → 격리 프로필 재캡처로 교체. diff --git a/docs/DEPLOYMENT_PLAN.md b/docs/DEPLOYMENT_PLAN.md index 5bd0668..c1dca0d 100644 --- a/docs/DEPLOYMENT_PLAN.md +++ b/docs/DEPLOYMENT_PLAN.md @@ -174,10 +174,32 @@ jobs: - **리허설**: workflow_dispatch → `ci-rehearsal` draft prerelease 로 태그 없이 전 파이프라인 검증. - **시크릿**: `RELEASE_TOKEN`(Forgejo PAT, `write:repository`) — 릴리스 생성·자산 업로드. 체크아웃은 잡 자동 토큰(`github.token`). `ANDROID_KEYSTORE/ALIAS/PASS` 등록 전까지 APK 는 디버그 서명. +- **업로드 대상**: 공개 배포 저장소 **`Video-Downloader/releases`**(워크플로 env `RELEASE_REPO`). + 소스 저장소(client)는 비공개라 자산 익명 다운로드가 불가해 분리했다. draft 는 익명에게 보이지 + 않으므로 "빌드 → draft → QA → 수동 Publish" 흐름은 그대로다. Publish 하면 §5-ter 사이트가 노출한다. - **머신 전제**: win = SDK 8/10 + `maui-android` 워크로드 + JDK 17 + Inno Setup 6 · linux/mac = `~/.dotnet` SDK 8.0.424 + 10.0.400 (§0.5). - **갭**: §6 자동 업데이트 v1(`UpdateChecker`)은 GitHub Releases API 를 폴링하므로 GitHub 공개 전까지 미동작 — Forgejo Releases API(`/repos/{owner}/{repo}/releases/latest`, 동일 형태) 대응은 후속 항목. + 대상은 공개 저장소 `Video-Downloader/releases` 가 되어야 한다. + +## 5-ter. 배포 사이트 — MEDIABROWSER (Cloudflare Pages, 2026-08-18 가동) + +제품 배포명은 **MEDIABROWSER** 로 하고, 프로모션/다운로드/사용 안내 3페이지 정적 사이트를 +Cloudflare Pages 에 올렸다. **https://mediabrowser-7r4.pages.dev** (mediabrowser 서브도메인 선점으로 +접미 부여 — 커스텀 도메인 연결은 후속). + +- **소스**: `site/` — `index.html`(프로모션: 라이트 글래스모피즘 + CSS scroll-driven 패럴랙스, + 모션 축소·투명도 축소 미디어쿼리 존중) · `releases.html`(플랫폼별 다운로드) · `guide.html`(사용 안내). + 히어로/가이드의 다운로드 패널 이미지는 격리 프로필(`VD_DATA_DIR`)로 띄운 실제 앱 캡처다. +- **릴리스 데이터**: 브라우저에서 Forgejo API 를 직접 부르면 CORS 에 막히므로, 배포 시점에 + `site/fetch-releases.mjs` 가 공개 저장소 릴리스를 `site/releases.json`(git 무시)으로 굽는다. + published 릴리스만 포함(draft 제외), 최신판 강조 + 이전 버전 접힘 + OS 자동 감지 권장 표시. +- **배포**: `./build/deploy-site.ps1` — .env 의 `CLOUDFLARE_API_TOKEN/ACCOUNT_ID` 로 + `wrangler pages deploy site --project-name mediabrowser`. **릴리스를 Publish 한 뒤 이 스크립트를 + 다시 실행해야 사이트 목록이 갱신된다**(후속: 릴리스 CI 에 사이트 재배포 잡 추가). +- **후속**: 커스텀 도메인 · 설치자/실행 파일의 MEDIABROWSER 개명(제품명 통일) · Obtainium `app.json` + 의 URL 을 공개 저장소로 갱신. ## 6. 업데이트 체계 diff --git a/site/assets/icons.svg b/site/assets/icons.svg new file mode 100644 index 0000000..36386d3 --- /dev/null +++ b/site/assets/icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/assets/panel-downloads.png b/site/assets/panel-downloads.png new file mode 100644 index 0000000000000000000000000000000000000000..128a91aefabc24e4aed8a703d59be7dc2b3da75f GIT binary patch literal 7477 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 literal 0 HcmV?d00001 diff --git a/site/fetch-releases.mjs b/site/fetch-releases.mjs new file mode 100644 index 0000000..d507aa7 --- /dev/null +++ b/site/fetch-releases.mjs @@ -0,0 +1,35 @@ +// 배포 전에 공개 릴리스 저장소의 릴리스 목록을 releases.json 으로 굽는다. +// (브라우저에서 Forgejo API 를 직접 호출하면 CORS 에 막히므로 빌드 시점에 정적 생성) +// 사용: node site/fetch-releases.mjs +import { writeFileSync } from "node:fs"; + +const API = "https://git.chanpaca.net/api/v1/repos/Video-Downloader/releases/releases?limit=50"; + +const res = await fetch(API, { headers: { Accept: "application/json" } }); +if (!res.ok) throw new Error(`릴리스 API 실패: HTTP ${res.status}`); +const raw = await res.json(); + +const releases = raw + .filter((r) => !r.draft) + .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)) + .map((r) => ({ + tag: r.tag_name, + name: r.name || r.tag_name, + published_at: r.published_at, + prerelease: r.prerelease, + notes: r.body || "", + assets: (r.assets || []).map((a) => ({ + name: a.name, + size: a.size, + url: a.browser_download_url, + })), + })); + +const out = { + generated_at: new Date().toISOString(), + repo_url: "https://git.chanpaca.net/Video-Downloader/releases", + releases, +}; + +writeFileSync(new URL("./releases.json", import.meta.url), JSON.stringify(out, null, 2)); +console.log(`releases.json 생성: 릴리스 ${releases.length}건`); diff --git a/site/guide.html b/site/guide.html new file mode 100644 index 0000000..443a512 --- /dev/null +++ b/site/guide.html @@ -0,0 +1,149 @@ + + + + + +사용 안내 - MEDIABROWSER + + + + + + + + + +
+ +
+

사용 안내

+

설치부터 첫 다운로드, 휴대폰 연결까지. 처음이라면 위에서부터 순서대로 따라오세요.

+
+
+ +
+ + +
+
+

설치

+

다운로드 페이지에서 플랫폼에 맞는 빌드를 받으세요.

+
    +
  • + Windows: 설치자(exe)를 실행하고 안내를 따르면 끝입니다. x64 와 arm64 를 제공합니다.
  • +
  • + macOS 프리뷰: tar.gz 를 풀고 앱을 실행합니다. 아직 서명이 없어 처음 실행 시 보안 확인이 필요할 수 있습니다.
  • +
  • + Linux 프리뷰: tar.gz 를 풀고 실행 파일을 실행합니다.
  • +
  • + Android: APK 를 설치합니다. 데스크톱 앱과 페어링해 쓰는 컴패니언입니다.
  • +
+

다운로드에 필요한 도구(yt-dlp, ffmpeg, deno)는 앱이 필요할 때 자동으로 설치합니다. 따로 준비할 것은 없습니다.

+
+ +
+

첫 다운로드

+

MEDIABROWSER 는 브라우저입니다. 평소처럼 사이트에 들어가 영상을 재생하면 나머지는 앱이 합니다.

+
    +
  • + 주소창에 사이트를 입력해 이동하고, 원하는 영상을 재생합니다.
  • +
  • + 재생이 시작되면 스트림(HLS, DASH, MP4)이 감지되어 오른쪽 다운로드 패널에 나타납니다.
  • +
  • + 목록에서 다운로드를 누르면 로그인 상태 그대로 내려받기가 시작됩니다.
  • +
+
+ 다운로드 패널의 빈 상태 화면. 영상을 재생하면 여기에 나타납니다 라는 안내 문구가 표시되어 있다. +
감지 전의 다운로드 패널. 재생을 시작하면 이 목록이 채워집니다.
+
+
+ +
+

다운로드 관리

+

패널의 탭으로 전체, 진행, 완료, 실패를 나눠 보고, 항목마다 속도와 남은 시간, 세그먼트 진행을 확인합니다.

+
    +
  • + 모두 일시정지 / 모두 재개로 한 번에 멈추고 다시 시작합니다. 항목별 일시정지도 됩니다.
  • +
  • + 예약을 걸면 지정한 시간부터 내려받습니다. 이미 받은 영상은 중복 확인으로 알려줍니다.
  • +
  • + 받은 영상은 라이브러리에서 모아 보고, 재생목록과 즐겨찾기, 아카이브로 정리합니다.
  • +
  • + 목록은 CSV 로 내보내 기록으로 남길 수 있습니다.
  • +
+
+ +
+

휴대폰 연결

+

데스크톱 앱에 내장된 서버가 휴대폰과 라이브러리를 이어줍니다. 같은 네트워크에서 페어링 한 번이면 됩니다.

+
    +
  • + 휴대폰에서 컴패니언 앱을 열고, 데스크톱 앱이 보여주는 QR 코드를 스캔합니다.
  • +
  • + 페어링이 끝나면 라이브러리의 영상을 휴대폰에서 바로 재생하거나 파일로 전송받습니다.
  • +
  • + 연결은 내 네트워크 안에서만 이루어집니다. 외부 서버를 거치지 않습니다.
  • +
+
+ +
+

자주 묻는 질문

+
+
+ ffmpeg 나 yt-dlp 를 따로 설치해야 하나요? +

아니요. 필요한 시점에 앱이 자동으로 내려받아 관리합니다. 설정에서 직접 지정한 경로가 있으면 그걸 우선합니다.

+
+
+ 받은 파일은 어디에 저장되나요? +

기본 저장 폴더에 저장되며, 설정에서 폴더를 바꿀 수 있습니다. 완료 목록에서 파일 위치를 바로 열 수 있습니다.

+
+
+ 다운로드가 중간에 끊기면 처음부터 다시 받나요? +

아니요. 세그먼트 단위로 이어받기를 지원해서 받다 만 지점부터 다시 진행합니다.

+
+
+ 업데이트는 어떻게 하나요? +

설정의 앱 업데이트 항목에서 새 버전을 확인할 수 있고, 다운로드 페이지에서 새 설치자를 받아 덮어 설치하면 됩니다.

+
+
+ 어떤 사이트를 지원하나요? +

표준 스트리밍 형식(HLS, DASH, MP4)을 쓰는 사이트에서 동작합니다. 콘텐츠의 이용 약관과 저작권을 지키는 범위에서 사용해 주세요.

+
+
+
+
+
+ + + + + + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..b307d9e --- /dev/null +++ b/site/index.html @@ -0,0 +1,156 @@ + + + + + +MEDIABROWSER - 보던 영상, 그 자리에서 내 것으로 + + + + + + + + + +
+ +
+
+

보던 영상,
그 자리에서 내 것으로.

+

MEDIABROWSER 는 재생 중인 미디어를 감지해 내려받는 브라우저입니다. + 받은 영상은 라이브러리로 관리하고, 휴대폰으로 이어봅니다.

+ +
+
+
+ MEDIABROWSER 다운로드 패널. 전체·진행·완료·실패 탭과 모두 일시정지, 모두 재개, CSV 버튼이 있고, 영상을 재생하면 여기에 나타납니다 라는 안내가 보인다. +
앱의 다운로드 패널. 재생을 감지하면 이 목록에 자동으로 쌓입니다.
+
+
+
+
+ +
+
+

주소를 붙여넣는 게 아니라,
그냥 봅니다.

+
+
+
+
+
+ HLSDASHMP4 +
+

재생하면 감지됩니다

+

내장 브라우저에서 영상이 재생되는 순간 스트림을 인식해 다운로드 목록에 올립니다. + 별도 확장이나 주소 복사가 필요 없습니다.

+
+
+
+

로그인 그대로

+

탭의 쿠키와 Referer 를 그대로 물려받아 내려받습니다. 브라우저에서 보이던 영상은 그대로 받아집니다.

+
+
+

끊겨도 이어서

+

병렬 분할 다운로드와 이어받기, 그리고 ffmpeg 리먹스까지 자동으로 처리합니다. AES-128 스트림도 지원합니다.

+
+
+
+
+ +
+
+

다운로드는 관리가 절반입니다.

+
+
+
+

속도와 세그먼트가 보입니다

항목마다 속도, 남은 시간, 세그먼트 맵을 실시간으로 표시합니다.

+
+
+
+

전체 일시정지와 재개

한 번에 멈추고 한 번에 다시 시작합니다. 예약 시간을 걸어둘 수도 있습니다.

+
+
+
+

중복은 미리 알려줍니다

이미 받은 영상을 다시 받으려 하면 내려받기 전에 확인해 줍니다.

+
+
+
+

라이브러리와 아카이브

받은 영상을 모아 보고, 정리하고, 재생목록과 즐겨찾기로 관리합니다.

+
+
+
+
+ +
+
+ +
+

받은 영상,
휴대폰에서 이어보기.

+

데스크톱 앱에 내장된 서버가 라이브러리를 휴대폰으로 스트리밍하거나 전송합니다. + QR 코드 한 번으로 페어링하고, Android 컴패니언 앱으로 어디서든 이어봅니다. iOS 는 준비 중입니다.

+
+
+
+ +
+
+
+

Windows 에서 시작하세요.

+

Windows 는 설치자로 바로 시작할 수 있습니다. + macOS 와 Linux 는 새 셸의 프리뷰 빌드, Android 는 컴패니언 앱을 제공합니다.

+
+
Windows
+
macOS 프리뷰
+
Linux 프리뷰
+
Android
+
+ 다운로드 +
+
+
+ + + + + + diff --git a/site/releases.html b/site/releases.html new file mode 100644 index 0000000..5c0844e --- /dev/null +++ b/site/releases.html @@ -0,0 +1,76 @@ + + + + + +다운로드 - MEDIABROWSER + + + + + + + + + +
+ +
+

다운로드

+

사용하는 플랫폼에 맞는 빌드를 받으세요. 설치가 끝나면 + 사용 안내에서 첫 다운로드까지 3분이면 충분합니다.

+
+
+ +
+ + +
+
+ +

릴리스 목록을 불러오는 중

+

잠시만 기다려 주세요.

+
+
+
+ + + + + + diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..a922c9c --- /dev/null +++ b/site/site.js @@ -0,0 +1,131 @@ +// MEDIABROWSER 사이트 공용 스크립트 +// 1) 스크롤 등장(reveal) — IntersectionObserver, 모션 축소 설정이면 즉시 표시 +// 2) releases.html — releases.json(배포 시 생성)을 읽어 릴리스 목록 렌더 + +(function initReveal() { + const items = document.querySelectorAll(".reveal"); + if (!items.length) return; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) { + return; // .js 게이트를 켜지 않으면 CSS 기본값(표시 상태) 그대로 + } + document.documentElement.classList.add("js"); + const io = new IntersectionObserver((entries) => { + for (const e of entries) { + if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } + } + }, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" }); + items.forEach((el) => io.observe(el)); +})(); + +// ── 릴리스 렌더 ────────────────────────────────────────────── +const PLATFORMS = [ + { id: "windows", label: "Windows", icon: "windows-logo", sub: "설치자를 실행하면 됩니다", + match: (n) => /setup.*\.exe$/i.test(n) || /win/i.test(n) }, + { id: "macos", label: "macOS 프리뷰", icon: "apple-logo", sub: "tar.gz 를 풀어 실행하는 프리뷰 빌드", + match: (n) => /osx|darwin|mac/i.test(n) }, + { id: "linux", label: "Linux 프리뷰", icon: "linux-logo", sub: "tar.gz 를 풀어 실행하는 프리뷰 빌드", + match: (n) => /linux/i.test(n) }, + { id: "android", label: "Android", icon: "android-logo", sub: "컴패니언 앱 APK", + match: (n) => /\.apk$/i.test(n) }, +]; + +function detectPreferredAsset(name) { + const ua = navigator.userAgent; + if (/Windows/i.test(ua)) return /setup-x64\.exe$/i.test(name); + if (/Android/i.test(ua)) return /\.apk$/i.test(name); + if (/Mac/i.test(ua)) return /osx-arm64/i.test(name); + if (/Linux/i.test(ua)) return /linux-x64/i.test(name); + return false; +} + +function fmtSize(bytes) { + if (!bytes) return ""; + const mb = bytes / 1048576; + return mb >= 1024 ? (mb / 1024).toFixed(1) + " GB" : mb.toFixed(1) + " MB"; +} + +function fmtDate(iso) { + if (!iso) return ""; + const d = new Date(iso); + return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일`; +} + +function esc(s) { + return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); +} + +function assetRow(a, recommended) { + return `
+
${esc(a.name)}
${fmtSize(a.size)}
+ 받기 +
`; +} + +function groupsHtml(assets) { + const used = new Set(); + const groups = PLATFORMS.map((p) => { + const list = assets.filter((a) => !used.has(a.name) && p.match(a.name)); + list.forEach((a) => used.add(a.name)); + if (!list.length) return ""; + return `
+
${p.label}
+
${p.sub}
+ ${list.map((a) => assetRow(a, detectPreferredAsset(a.name))).join("")} +
`; + }).filter(Boolean); + const rest = assets.filter((a) => !used.has(a.name)); + if (rest.length) { + groups.push(`
기타
그 밖의 파일
+ ${rest.map((a) => assetRow(a, false)).join("")}
`); + } + return `
${groups.join("")}
`; +} + +function releaseCard(r, featured) { + const pill = r.prerelease ? `프리뷰` : `안정판`; + const meta = ` + ${r.notes ? `` : ""} + ${groupsHtml(r.assets)}`; + if (featured) return ``; + return `
${esc(r.name || r.tag)}${pill} + ${fmtDate(r.published_at)} +
${r.notes ? `` : ""}${groupsHtml(r.assets)}
+
`; +} + +function emptyState() { + return `
+ +

첫 공개 릴리스를 준비하고 있어요

+

빌드 파이프라인은 이미 돌아가고 있습니다. 곧 이 자리에서 설치자를 받을 수 있습니다. + 먼저 사용 안내를 둘러보세요.

+
`; +} + +(async function initReleases() { + const root = document.getElementById("release-root"); + if (!root) return; + try { + const res = await fetch("releases.json", { cache: "no-cache" }); + if (!res.ok) throw new Error("HTTP " + res.status); + const data = await res.json(); + const releases = (data.releases || []); + if (!releases.length) { root.innerHTML = emptyState(); return; } + const [latest, ...older] = releases; + let html = releaseCard(latest, true); + if (older.length) { + html += `

이전 버전

+ ${older.map((r) => releaseCard(r, false)).join("")}
`; + } + root.innerHTML = html; + } catch (err) { + root.innerHTML = `
+

목록을 불러오지 못했어요

+

잠시 후 새로고침해 주세요. 급하다면 + 릴리스 저장소에서 직접 받을 수 있습니다.

+
`; + } +})(); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..9a1ab76 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,289 @@ +/* MEDIABROWSER 사이트 공용 디자인 시스템 + 라이트 단일 테마(사용자 지정) · 글래스모피즘 · 절제된 스크롤 모션 + 반경 체계: 버튼=필, 카드/패널=20px, 칩/입력=12px */ + +:root { + --bg: #f5f7fc; + --bg-deep: #e9edf8; + --ink: #171a23; + --muted: #5b6273; + --line: rgba(23, 26, 35, 0.08); + --accent: #2050f0; + --accent-deep: #1740c8; + --accent-soft: rgba(32, 80, 240, 0.09); + --glass: rgba(255, 255, 255, 0.58); + --glass-strong: rgba(255, 255, 255, 0.78); + --glass-brd: rgba(255, 255, 255, 0.72); + --shadow-soft: 0 24px 60px -24px rgba(32, 60, 160, 0.18); + --shadow-lift: 0 32px 80px -28px rgba(32, 60, 160, 0.28); + --r-card: 20px; + --r-chip: 12px; + --nav-h: 68px; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } + +body { + font-family: "Wanted Sans Variable", "Wanted Sans", system-ui, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; + background: var(--bg); + color: var(--ink); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +::selection { background: var(--accent); color: #fff; } + +.icon { width: 1.25em; height: 1.25em; fill: currentColor; flex: none; vertical-align: -0.25em; } + +/* ── 배경 오브(패럴랙스 레이어) ─────────────────────────────── */ +.orbs { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: -1; } +.orb { position: absolute; border-radius: 50%; filter: blur(70px); opacity: 0.55; } +.orb--a { width: 560px; height: 560px; left: -140px; top: -120px; + background: radial-gradient(circle at 35% 35%, #c9d6ff, #eef1fb 70%); } +.orb--b { width: 480px; height: 480px; right: -120px; top: 160px; + background: radial-gradient(circle at 40% 40%, #d8e4ff, #f0f3fc 70%); } +.orb--c { width: 380px; height: 380px; left: 34%; top: 560px; + background: radial-gradient(circle at 50% 40%, #dfe9ff, #f4f6fd 72%); opacity: 0.45; } + +/* 스크롤 패럴랙스: 지원 브라우저에서만, 모션 축소 설정이면 정지 */ +@media (prefers-reduced-motion: no-preference) { + @supports (animation-timeline: scroll()) { + .orb { animation: orb-drift linear both; animation-timeline: scroll(); } + .orb--a { --drift: 180px; } + .orb--b { --drift: -240px; } + .orb--c { --drift: 120px; } + } +} +@keyframes orb-drift { to { transform: translateY(var(--drift, 0px)); } } + +/* ── 글래스 재질 ────────────────────────────────────────────── */ +.glass { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0.16)), var(--glass); + backdrop-filter: blur(18px) saturate(160%); + -webkit-backdrop-filter: blur(18px) saturate(160%); + border: 1px solid var(--glass-brd); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), var(--shadow-soft); +} +@media (prefers-reduced-transparency: reduce) { + .glass { background: rgba(255, 255, 255, 0.96); backdrop-filter: none; -webkit-backdrop-filter: none; } +} + +/* ── 내비게이션 ─────────────────────────────────────────────── */ +.nav { + position: fixed; top: 14px; left: 50%; transform: translateX(-50%); + width: min(1120px, calc(100% - 32px)); height: var(--nav-h); + display: flex; align-items: center; gap: 28px; padding: 0 22px; + border-radius: 999px; z-index: 50; +} +.nav__brand { + font-weight: 800; letter-spacing: 0.04em; font-size: 15px; + color: var(--ink); text-decoration: none; display: flex; align-items: center; gap: 9px; +} +.nav__mark { + width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; + background: linear-gradient(135deg, var(--accent), #4d79ff); color: #fff; +} +.nav__mark .icon { width: 16px; height: 16px; } +.nav__links { display: flex; gap: 22px; margin-left: auto; } +.nav__links a { color: var(--muted); text-decoration: none; font-size: 14.5px; font-weight: 600; } +.nav__links a:hover, .nav__links a[aria-current="page"] { color: var(--ink); } + +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + border-radius: 999px; font-weight: 700; text-decoration: none; cursor: pointer; + border: 0; white-space: nowrap; + transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.18s, background 0.18s; +} +.btn:active { transform: translateY(1px) scale(0.98); } +.btn--primary { + background: var(--accent); color: #fff; padding: 12px 22px; font-size: 15px; + box-shadow: 0 10px 24px -10px rgba(32, 80, 240, 0.55); +} +.btn--primary:hover { background: var(--accent-deep); } +.btn--ghost { + background: rgba(255, 255, 255, 0.6); color: var(--ink); padding: 12px 22px; font-size: 15px; + border: 1px solid var(--line); +} +.btn--ghost:hover { background: #fff; } +.btn--sm { padding: 9px 16px; font-size: 14px; } + +/* ── 공통 레이아웃 ──────────────────────────────────────────── */ +.wrap { width: min(1120px, calc(100% - 40px)); margin: 0 auto; } +section { position: relative; } +.section-pad { padding: 108px 0; } +h1, h2, h3 { line-height: 1.22; letter-spacing: -0.02em; font-weight: 800; } +.sub { color: var(--muted); font-size: 17px; max-width: 58ch; } + +/* 스크롤 등장 — JS 가 html 에 .js 를 붙였을 때만 숨김(no-JS 에서도 내용이 보이는 점진적 향상) */ +html.js .reveal { opacity: 0; transform: translateY(22px); transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); } +html.js .reveal.in { opacity: 1; transform: none; } +@media (prefers-reduced-motion: reduce) { html.js .reveal { opacity: 1; transform: none; transition: none; } } + +/* ── 히어로 ─────────────────────────────────────────────────── */ +.hero { padding: calc(var(--nav-h) + 78px) 0 96px; min-height: 86dvh; display: flex; align-items: center; } +.hero__grid { display: grid; grid-template-columns: 7fr 5fr; gap: 56px; align-items: center; } +.hero h1 { font-size: clamp(38px, 5.2vw, 62px); } +.hero .sub { margin-top: 22px; font-size: 18px; } +.hero__cta { display: flex; gap: 14px; margin-top: 34px; } +.hero__visual { position: relative; } +.hero__panel { + border-radius: var(--r-card); padding: 14px; + transform: rotate(1.8deg); +} +.hero__panel img { + display: block; width: 100%; border-radius: 12px; + box-shadow: 0 18px 44px -18px rgba(10, 16, 40, 0.45); +} +.hero__caption { margin-top: 12px; font-size: 13.5px; color: var(--muted); text-align: center; } + +/* ── 벤토 ───────────────────────────────────────────────────── */ +.bento { display: grid; grid-template-columns: 3fr 2fr; grid-template-rows: auto auto; gap: 20px; margin-top: 52px; } +.bento__cell { border-radius: var(--r-card); padding: 34px; position: relative; overflow: hidden; } +.bento__cell--wide { grid-row: span 2; display: flex; flex-direction: column; justify-content: flex-end; min-height: 380px; + background: linear-gradient(160deg, #1b2440 0%, #22346e 55%, #2050f0 130%); color: #fff; } +.bento__cell--wide h3, .bento__cell--wide p { color: #fff; } +.bento__cell--wide p { opacity: 0.82; } +.bento__wave { position: absolute; top: 30px; left: 34px; opacity: 0.9; } +.bento__wave .icon { width: 44px; height: 44px; color: #9db6ff; } +.bento__cell h3 { font-size: 21px; margin-bottom: 10px; } +.bento__cell p { color: var(--muted); font-size: 15.5px; } +.bento__chipline { display: flex; gap: 8px; margin-bottom: 18px; flex-wrap: wrap; } +.chip { + display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; + border-radius: 999px; font-size: 13px; font-weight: 700; + background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.22); color: #dfe8ff; +} + +/* ── 기능 그리드(카드 없는 정리) ────────────────────────────── */ +.featlist { display: grid; grid-template-columns: repeat(2, 1fr); gap: 40px 64px; margin-top: 56px; } +.featlist__item { display: flex; gap: 16px; align-items: flex-start; } +.featlist__icon { + width: 42px; height: 42px; flex: none; border-radius: var(--r-chip); display: grid; place-items: center; + background: var(--accent-soft); color: var(--accent); +} +.featlist__icon .icon { width: 21px; height: 21px; } +.featlist__item h3 { font-size: 17px; margin-bottom: 4px; } +.featlist__item p { color: var(--muted); font-size: 15px; } + +/* ── 모바일 섹션 ────────────────────────────────────────────── */ +.mobile__grid { display: grid; grid-template-columns: 5fr 7fr; gap: 56px; align-items: center; } +.mobile__stage { position: relative; min-height: 320px; } +.mobile__card { position: absolute; border-radius: var(--r-card); display: flex; align-items: center; gap: 14px; padding: 20px 24px; } +.mobile__card .icon { width: 30px; height: 30px; color: var(--accent); } +.mobile__card strong { display: block; font-size: 15.5px; } +.mobile__card span { font-size: 13.5px; color: var(--muted); } +.mobile__card--a { top: 8%; left: 4%; transform: rotate(-2.4deg); } +.mobile__card--b { top: 40%; right: 6%; transform: rotate(1.6deg); } +.mobile__card--c { bottom: 4%; left: 14%; transform: rotate(-1deg); } + +/* ── 플랫폼 밴드 ────────────────────────────────────────────── */ +.band { border-radius: 28px; padding: 64px 56px; text-align: center; } +.band h2 { font-size: clamp(28px, 3.6vw, 40px); } +.band .sub { margin: 16px auto 0; } +.band__oses { display: flex; gap: 34px; justify-content: center; margin: 36px 0 40px; color: var(--muted); } +.band__os { display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600; } +.band__os .icon { width: 34px; height: 34px; color: var(--ink); } +.band__os--soon { opacity: 0.55; } + +/* ── 푸터 ───────────────────────────────────────────────────── */ +footer { padding: 48px 0 60px; } +.footer__row { display: flex; align-items: center; gap: 26px; flex-wrap: wrap; border-top: 1px solid var(--line); padding-top: 32px; } +.footer__row a { color: var(--muted); text-decoration: none; font-size: 14px; font-weight: 600; } +.footer__row a:hover { color: var(--ink); } +.footer__brand { font-weight: 800; letter-spacing: 0.04em; font-size: 14px; } +.footer__copy { margin-left: auto; color: var(--muted); font-size: 13.5px; } + +/* ── 릴리스 페이지 ──────────────────────────────────────────── */ +.pagehead { padding: calc(var(--nav-h) + 74px) 0 30px; } +.pagehead h1 { font-size: clamp(32px, 4.2vw, 48px); } +.pagehead .sub { margin-top: 16px; } + +.rel-layout { display: grid; grid-template-columns: 280px 1fr; gap: 44px; align-items: start; padding-bottom: 110px; } +.rel-rail { position: sticky; top: calc(var(--nav-h) + 30px); display: flex; flex-direction: column; gap: 10px; } +.rel-rail__box { border-radius: var(--r-card); padding: 22px; } +.rel-rail__box h3 { font-size: 15px; margin-bottom: 12px; } +.rel-rail__os { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--r-chip); + color: var(--ink); text-decoration: none; font-size: 14.5px; font-weight: 600; } +.rel-rail__os:hover { background: rgba(255, 255, 255, 0.75); } +.rel-rail__os .icon { width: 19px; height: 19px; color: var(--muted); } +.rel-rail__note { font-size: 13.5px; color: var(--muted); line-height: 1.55; } +.rel-rail__note a { color: var(--accent); font-weight: 700; text-decoration: none; } + +.rel-featured { border-radius: 24px; padding: 40px; } +.rel-featured__meta { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; } +.rel-featured__tag { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; } +.rel-featured__date { color: var(--muted); font-size: 14.5px; } +.rel-pill { display: inline-flex; padding: 4px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 800; + background: var(--accent-soft); color: var(--accent); } +.rel-pill--pre { background: rgba(214, 122, 20, 0.12); color: #a35a00; } +.rel-featured__notes { margin-top: 18px; color: var(--muted); font-size: 15px; white-space: pre-line; max-width: 70ch; } + +.dl-groups { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; margin-top: 30px; } +.dl-group { border-radius: var(--r-card); background: rgba(255, 255, 255, 0.66); border: 1px solid var(--line); padding: 22px; } +.dl-group__head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-weight: 800; font-size: 16px; } +.dl-group__head .icon { width: 21px; height: 21px; color: var(--ink); } +.dl-group__sub { font-size: 13.5px; color: var(--muted); margin-bottom: 14px; } +.dl-asset { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; } +.dl-asset + .dl-asset { border-top: 1px solid var(--line); } +.dl-asset__name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; } +.dl-asset__size { color: var(--muted); font-size: 12.5px; } +.dl-asset .btn { flex: none; } +.dl-reco { outline: 2px solid var(--accent); outline-offset: 2px; } + +.rel-old { margin-top: 30px; display: flex; flex-direction: column; gap: 14px; } +.rel-old details { border-radius: var(--r-card); border: 1px solid var(--line); background: rgba(255, 255, 255, 0.55); } +.rel-old summary { cursor: pointer; padding: 18px 22px; font-weight: 700; display: flex; align-items: center; gap: 12px; list-style: none; } +.rel-old summary::-webkit-details-marker { display: none; } +.rel-old summary .rel-featured__date { margin-left: auto; } +.rel-old__body { padding: 0 22px 20px; } + +.rel-empty { border-radius: 24px; padding: 72px 40px; text-align: center; } +.rel-empty .icon { width: 44px; height: 44px; color: var(--accent); } +.rel-empty h2 { font-size: 24px; margin: 18px 0 10px; } +.rel-empty p { color: var(--muted); max-width: 48ch; margin: 0 auto; } + +/* ── 가이드 페이지 ──────────────────────────────────────────── */ +.guide-layout { display: grid; grid-template-columns: 240px 1fr; gap: 52px; align-items: start; padding-bottom: 110px; } +.guide-toc { position: sticky; top: calc(var(--nav-h) + 30px); border-radius: var(--r-card); padding: 22px; } +.guide-toc a { display: block; padding: 7px 10px; border-radius: 10px; color: var(--muted); text-decoration: none; font-size: 14.5px; font-weight: 600; } +.guide-toc a:hover { color: var(--ink); background: rgba(255, 255, 255, 0.75); } +.guide-step { padding: 44px 0; border-top: 1px solid var(--line); } +.guide-step:first-of-type { border-top: 0; padding-top: 10px; } +.guide-step h2 { font-size: 24px; display: flex; align-items: center; gap: 13px; } +.guide-step h2 .featlist__icon { width: 38px; height: 38px; } +.guide-step > p { color: var(--muted); margin-top: 14px; max-width: 66ch; } +.guide-step ul { margin: 16px 0 0 2px; list-style: none; display: flex; flex-direction: column; gap: 9px; } +.guide-step li { display: flex; gap: 10px; color: var(--ink); font-size: 15px; } +.guide-step li .icon { width: 18px; height: 18px; color: var(--accent); margin-top: 4px; } +.guide-step li span b { font-weight: 750; } +.guide-media { margin-top: 24px; border-radius: var(--r-card); padding: 12px; max-width: 460px; } +.guide-media img { display: block; width: 100%; border-radius: 12px; } +.guide-media figcaption { font-size: 13px; color: var(--muted); padding: 10px 6px 2px; } +.guide-faq { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; } +.guide-faq details { border: 1px solid var(--line); border-radius: var(--r-chip); background: rgba(255, 255, 255, 0.55); } +.guide-faq summary { cursor: pointer; padding: 15px 18px; font-weight: 700; font-size: 15px; list-style: none; } +.guide-faq summary::-webkit-details-marker { display: none; } +.guide-faq p { padding: 0 18px 16px; color: var(--muted); font-size: 14.5px; } + +/* ── 모바일 반응형 ──────────────────────────────────────────── */ +@media (max-width: 860px) { + .nav { gap: 14px; } + .nav__links { display: none; } + .hero { min-height: auto; padding-bottom: 64px; } + .hero__grid, .mobile__grid { grid-template-columns: 1fr; gap: 40px; } + .bento { grid-template-columns: 1fr; } + .bento__cell--wide { min-height: 300px; } + .featlist { grid-template-columns: 1fr; gap: 28px; } + .section-pad { padding: 72px 0; } + .band { padding: 48px 24px; } + .band__oses { gap: 22px; flex-wrap: wrap; } + .rel-layout, .guide-layout { grid-template-columns: 1fr; gap: 28px; } + .rel-rail, .guide-toc { position: static; } + .dl-groups { grid-template-columns: 1fr; } + .mobile__stage { min-height: 380px; } + .rel-featured { padding: 26px; } +}