v4.0.0: UNIARE 리브랜딩·레이아웃·덱 UX 개선

제품명·헤더 로고 이미지 통일, 덱 이름 변경·조회 수 제거, 썸네일 홀드 미리보기를 누르는 동안만 표시하도록 수정하고 Tailwind CSS를 재빌드했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 11:19:07 +09:00
parent 2228757ea5
commit a281d731a1
32 changed files with 201 additions and 1910 deletions

View File

@@ -1,4 +1,4 @@
# UADECK API 환경변수 — 이 파일을 .env로 복사한 뒤 값을 채우세요 # UNIARE API 환경변수 — 이 파일을 .env로 복사한 뒤 값을 채우세요
# --- API --- # --- API ---
API_PORT=3000 API_PORT=3000

View File

@@ -6,12 +6,15 @@
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/auth-header.css" /> <link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/admin.css" /> <link rel="stylesheet" href="./style/admin.css" />
<title>관리자 - UADECK</title> <title>관리자 - UNIARE</title>
</head> </head>
<body class="admin-page"> <body class="admin-page">
<nav class="admin-nav"> <nav class="admin-nav">
<div class="admin-nav-inner"> <div class="admin-nav-inner">
<a href="/" class="admin-brand">UADECK <span>v4.0.0</span> 관리자</a> <a href="/" class="admin-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
<span class="admin-brand-label">관리자</span>
</a>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div> </div>
</nav> </nav>

View File

@@ -47,7 +47,7 @@ app.use((err, _req, res, _next) => {
}); });
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`UADECK API v4.0.0 listening on :${PORT}`); console.log(`UNIARE API v4.0.0 listening on :${PORT}`);
console.log(`DB: ${DB_PATH}`); console.log(`DB: ${DB_PATH}`);
console.log(`DATAS: ${process.env.DATAS_PATH || '(default ../datas)'}`); console.log(`DATAS: ${process.env.DATAS_PATH || '(default ../datas)'}`);
}); });

View File

@@ -82,7 +82,8 @@
<div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div> <div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
</div> </div>
<div class="sticky top-0 z-50 h-16 bg-white/80 backdrop-blur-md border-b px-4 py-3 flex justify-between items-center"> <div class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex justify-between items-center">
<button type="button" id="i18n-home" onclick="goToHome()" class="font-bold text-slate-600 hover:text-blue-600 cursor-pointer">← 홈</button> <button type="button" id="i18n-home" onclick="goToHome()" class="font-bold text-slate-600 hover:text-blue-600 cursor-pointer">← 홈</button>
<div class="font-bold text-slate-800"> <div class="font-bold text-slate-800">
<span id="i18n-searchResults">검색결과</span> <span id="i18n-searchResults">검색결과</span>
@@ -90,9 +91,10 @@
<span id="SearchResultsCounter" class="text-blue-600">0</span> <span id="SearchResultsCounter" class="text-blue-600">0</span>
</div> </div>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div>
</div> </div>
<div class="max-w-[1400px] mx-auto p-4 flex flex-col lg:flex-row gap-6"> <div class="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex flex-col lg:flex-row gap-6">
<aside class="w-full lg:w-64 space-y-4 lg:sticky lg:top-20 lg:h-fit"> <aside class="w-full lg:w-64 space-y-4 lg:sticky lg:top-20 lg:h-fit">
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm"> <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
<button id="resetSelection" class="w-full mb-4 flex items-center justify-center gap-2 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-xl font-bold text-xs transition-all border border-red-100"> <button id="resetSelection" class="w-full mb-4 flex items-center justify-center gap-2 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-xl font-bold text-xs transition-all border border-red-100">

View File

@@ -8,12 +8,14 @@
<link rel="stylesheet" href="./style/app-shell.css" /> <link rel="stylesheet" href="./style/app-shell.css" />
<link rel="stylesheet" href="./style/auth-header.css" /> <link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/deck-history.css" /> <link rel="stylesheet" href="./style/deck-history.css" />
<title>내 덱 관리 - UADECK</title> <title>내 덱 관리 - UNIARE</title>
</head> </head>
<body class="app-page deck-history-page"> <body class="app-page deck-history-page">
<nav class="app-nav"> <nav class="app-nav">
<div class="app-nav-inner"> <div class="app-nav-inner">
<a href="/" class="app-brand">UADECK <span>v4.0.0</span></a> <a href="/" class="app-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
</a>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div> </div>
</nav> </nav>
@@ -278,7 +280,7 @@
${visibilityBadge} ${visibilityBadge}
</div> </div>
<h2 class="deck-card-title">${title}</h2> <h2 class="deck-card-title">${title}</h2>
<div class="deck-card-meta">${escapeHtml(t('deckViews', '조회'))} ${d.view_count} · ${date}</div> <div class="deck-card-meta">${date}</div>
<div class="deck-card-actions deck-card-actions--primary"> <div class="deck-card-actions deck-card-actions--primary">
${favoriteButtonHtml(d.share_code)} ${favoriteButtonHtml(d.share_code)}
<button type="button" class="deck-card-btn deck-card-btn-load" data-load="${escapeHtml(d.share_code)}" data-series="${escapeHtml(d.series)}" data-name="${escapeHtml(d.series_name || '')}">${escapeHtml(t('deckLoad', '불러오기'))}</button> <button type="button" class="deck-card-btn deck-card-btn-load" data-load="${escapeHtml(d.share_code)}" data-series="${escapeHtml(d.series)}" data-name="${escapeHtml(d.series_name || '')}">${escapeHtml(t('deckLoad', '불러오기'))}</button>
@@ -288,6 +290,7 @@
</button> </button>
<div class="deck-card-more-menu hidden" role="menu"> <div class="deck-card-more-menu hidden" role="menu">
<button type="button" class="deck-card-more-item" role="menuitem" data-copy="${escapeHtml(d.share_code)}">${escapeHtml(t('deckCopyLink', '링크 복사'))}</button> <button type="button" class="deck-card-more-item" role="menuitem" data-copy="${escapeHtml(d.share_code)}">${escapeHtml(t('deckCopyLink', '링크 복사'))}</button>
<button type="button" class="deck-card-more-item" role="menuitem" data-rename="${escapeHtml(d.share_code)}" data-current-name="${escapeHtml(getDeckTitle(d))}">${escapeHtml(t('deckRename', '이름 변경'))}</button>
<button type="button" class="deck-card-more-item" role="menuitem" data-toggle="${escapeHtml(d.share_code)}" data-public="${isPublic ? '1' : '0'}">${escapeHtml(toggleLabel)}</button> <button type="button" class="deck-card-more-item" role="menuitem" data-toggle="${escapeHtml(d.share_code)}" data-public="${isPublic ? '1' : '0'}">${escapeHtml(toggleLabel)}</button>
<button type="button" class="deck-card-more-item deck-card-more-item--danger" role="menuitem" data-delete="${escapeHtml(d.share_code)}">${escapeHtml(t('deckDelete', '삭제'))}</button> <button type="button" class="deck-card-more-item deck-card-more-item--danger" role="menuitem" data-delete="${escapeHtml(d.share_code)}">${escapeHtml(t('deckDelete', '삭제'))}</button>
</div> </div>
@@ -441,6 +444,33 @@
return; return;
} }
const renameBtn = e.target.closest('[data-rename]');
if (renameBtn) {
const code = renameBtn.dataset.rename;
const current = renameBtn.dataset.currentName || '';
const newName = prompt(t('deckRenamePrompt', '새 덱 이름을 입력하세요'), current);
if (newName === null) return;
const trimmed = newName.trim();
if (!trimmed) {
alert(t('deckRenameEmpty', '이름을 입력해 주세요.'));
return;
}
if (trimmed === current) {
closeAllDeckMenus();
return;
}
try {
await UaDeckApi.updateDeck(code, { name: trimmed });
const deck = allDecks.find((d) => d.share_code === code);
if (deck) deck.name = trimmed;
closeAllDeckMenus();
renderDeckList();
} catch (err) {
alert(err.message || t('deckRenameFailed', '이름 변경에 실패했습니다.'));
}
return;
}
const toggleBtn = e.target.closest('[data-toggle]'); const toggleBtn = e.target.closest('[data-toggle]');
if (toggleBtn) { if (toggleBtn) {
const code = toggleBtn.dataset.toggle; const code = toggleBtn.dataset.toggle;

View File

@@ -1,4 +1,4 @@
# UADECK v4 프로덕션 스택 (NAS Docker) # UNIARE v4 프로덕션 스택 (NAS Docker)
# 사용: docker compose -f docker-compose.prod.yml up -d --build # 사용: docker compose -f docker-compose.prod.yml up -d --build
# #
# 정적 파일(HTML, images, datas)은 필요한 경로만 read-only 마운트 — 카드/이미지 추가 시 재빌드 불필요 # 정적 파일(HTML, images, datas)은 필요한 경로만 read-only 마운트 — 카드/이미지 추가 시 재빌드 불필요

View File

@@ -1,6 +1,6 @@
# 옆 프로젝트 API 연동 가이드 # 옆 프로젝트 API 연동 가이드
UADECK v4부터 덱 데이터는 **공유 코드 + REST API**로 제공합니다. UNIARE v4부터 덱 데이터는 **공유 코드 + REST API**로 제공합니다.
URL 쿼리(`?cards=`) 방식은 사용하지 않습니다. URL 쿼리(`?cards=`) 방식은 사용하지 않습니다.
## 공식 엔드포인트 ## 공식 엔드포인트
@@ -48,7 +48,7 @@ GET https://uniare.sori.studio/api/v1/decks/:shareCode
| 방법 | 예시 | | 방법 | 예시 |
|---|---| |---|---|
| UADECK 공유 버튼 | `https://uniare.sori.studio/d/x7Kp9m2Q` | | UNIARE 공유 버튼 | `https://uniare.sori.studio/d/x7Kp9m2Q` |
| URL 파라미터 | `cardList.html?d=x7Kp9m2Q` | | URL 파라미터 | `cardList.html?d=x7Kp9m2Q` |
| 코드만 | `x7Kp9m2Q` | | 코드만 | `x7Kp9m2Q` |
@@ -115,7 +115,7 @@ https://uniare.sori.studio/images{imgSrc}
## CORS ## CORS
다른 도메인에서 브라우저 `fetch`를 쓰려면 UADECK 서버 `.env`의 `CORS_ORIGINS`에 해당 origin을 추가하세요. 다른 도메인에서 브라우저 `fetch`를 쓰려면 UNIARE 서버 `.env`의 `CORS_ORIGINS`에 해당 origin을 추가하세요.
``` ```
CORS_ORIGINS=https://your-app.sori.studio,https://uniare.sori.studio CORS_ORIGINS=https://your-app.sori.studio,https://uniare.sori.studio

View File

@@ -24,7 +24,8 @@
## CSS 규칙 ## CSS 규칙
- 공통 UI는 가능한 한 Tailwind 유틸리티 클래스를 우선 사용한다. - 공통 UI는 가능한 한 Tailwind 유틸리티 클래스를 우선 사용한다.
- `style/output.css`에 포함되지 않은 유틸리티 클래스는 적용되지 않으므로, 페이지 특화 스타일은 인라인 `<style>` 또는 전용 CSS 파일(`style/auth-pages.css` 등)에 명시한다. - `style/output.css`는 **빌드 산출물**이다. HTML 등에 새 Tailwind 클래스를 추가한 뒤 `npm run build:css`로 재생성한다.
- `style/output.css`에 포함되지 않은 유틸리티 클래스는 적용되지 않으므로, 빌드 전까지는 페이지 특화 스타일을 인라인 `<style>` 또는 전용 CSS 파일(`style/auth-pages.css` 등)에 명시한다.
- 페이지 특화 보정 스타일만 인라인 `<style>` 또는 전용 CSS 파일에 둔다. - 페이지 특화 보정 스타일만 인라인 `<style>` 또는 전용 CSS 파일에 둔다.
- 캡처 안정성처럼 이유가 분명한 예외 스타일은 주석으로 목적을 남긴다. - 캡처 안정성처럼 이유가 분명한 예외 스타일은 주석으로 목적을 남긴다.

View File

@@ -1,4 +1,4 @@
# NAS 배포 가이드 (UADECK v4) # NAS 배포 가이드 (UNIARE v4)
## 1. 사전 준비 ## 1. 사전 준비

View File

@@ -1,7 +1,7 @@
# 기술 명세 # 기술 명세
## 1. 프로젝트 개요 ## 1. 프로젝트 개요
- 프로젝트명: `UADECK V4.0.0 - Union Arena Deck Builder` - 프로젝트명: `UNIARE - Union Arena Deck Builder` (표시명 UNIARE, 내부 API 식별자 일부는 `uadeck` 레거시 유지)
- 형태: 정적 HTML/CSS/JavaScript 프론트 + Docker API 백엔드 - 형태: 정적 HTML/CSS/JavaScript 프론트 + Docker API 백엔드
- 패키지 의존성 (프론트): `tailwindcss`, `@tailwindcss/cli` - 패키지 의존성 (프론트): `tailwindcss`, `@tailwindcss/cli`
- 패키지 의존성 (API): `express`, `better-sqlite3`, `bcryptjs`, `jsonwebtoken`, `cors` - 패키지 의존성 (API): `express`, `better-sqlite3`, `bcryptjs`, `jsonwebtoken`, `cors`

View File

@@ -1,5 +1,33 @@
# 업데이트 이력 # 업데이트 이력
## 2026-06-22 / v4.0.0 — 덱 카드 조회 수 제거
### 수행 작업
- `deckHistory.html`, `preset.html`: 카드 메타에서 조회 수 표시 제거 (저장일·작성자만 유지)
## 2026-06-22 / v4.0.0 — 제품명 UNIARE·로고 이미지
### 수행 작업
- 표시 제품명 `UADECK` → **UNIARE** (페이지 title, i18n, 관리자, 연동 문서)
- 상단 헤더 텍스트 로고 → `images/assets/uniare-h48.png` 이미지 로고
- 이미지 다운로드 파일명 접두사 `uadeck_` → `uniare_`
- 내부 호환 유지: `UaDeckApi`, `uadeck_token`, `uadeck.db`, `UADECK_API_BASE` 등
## 2026-06-22 / v4.0.0 — Tailwind CSS 재빌드
### 수행 작업
- `package.json`: `npm run build:css` 스크립트 추가
- `style/output.css` 재생성 — `sm:px-6` 등 HTML에 추가됐으나 빌드에 없던 클래스 반영
- `cardList.html`, `result.html`: `px-4 sm:px-6` 유지 (클래스 우회 제거)
## 2026-06-22 / v4.0.0 — 레이아웃·덱 이름 변경
### 수행 작업
- 헤더 `v4.0.0` 표시 제거 (로그인 페이지 하단 푸터에만 작게 표시)
- `deckHistory.html`: 더보기 메뉴에 **이름 변경** 추가 (`PUT /decks/:code`)
- `cardList.html`, `result.html`: 콘텐츠·헤더 최대 너비 `1400px` → **1280px** (`max-w-7xl`) 통일
- `profile.css`: 프로필 본문 `max-width: 42rem` 제거 → 내 덱·공개 덱과 동일한 `app-main` 너비
## 2026-06-22 / v4.0.0 — Docker 서비스명 `uniare-*` 통일 ## 2026-06-22 / v4.0.0 — Docker 서비스명 `uniare-*` 통일
### 수행 작업 ### 수행 작업
@@ -45,7 +73,7 @@ npm stop && npm start
## 2026-06-19 / v4.0.0 — 공개 덱 미리보기 수정 ## 2026-06-19 / v4.0.0 — 공개 덱 미리보기 수정
### 수행 작업 ### 수행 작업
- `deck-preview-hold.js`: mouseleave 즉시 닫힘 버그 수정, 클릭 고정·길게 누르기 동시 지원 - `deck-preview-hold.js`: mouseleave 즉시 닫힘 버그 수정, 길게 누르는 동안만 확대 (짧은 클릭 고정 제거)
- ~~안내 문구:「클릭·길게 눌러 크게 보기」~~ → 이후 제거됨 - ~~안내 문구:「클릭·길게 눌러 크게 보기」~~ → 이후 제거됨
## 2026-06-19 / v4.0.0 — 프로필 덱 저장 설정 UI ## 2026-06-19 / v4.0.0 — 프로필 덱 저장 설정 UI

View File

@@ -77,7 +77,7 @@
"loginPageTitle": "Log in", "loginPageTitle": "Log in",
"loginPageDesc": "Sign in to save and manage<br />your decks.", "loginPageDesc": "Sign in to save and manage<br />your decks.",
"signupPageTitle": "Sign up", "signupPageTitle": "Sign up",
"signupPageDesc": "Create a UADECK account to<br />save and share decks.", "signupPageDesc": "Create a UNIARE account to<br />save and share decks.",
"emailLabel": "Email", "emailLabel": "Email",
"passwordLabel": "Password", "passwordLabel": "Password",
"usernameLabel": "Username", "usernameLabel": "Username",
@@ -127,6 +127,10 @@
"deckMakePrivate": "Make private", "deckMakePrivate": "Make private",
"deckLoad": "Load", "deckLoad": "Load",
"deckCopyLink": "Copy link", "deckCopyLink": "Copy link",
"deckRename": "Rename",
"deckRenamePrompt": "Enter a new deck name",
"deckRenameEmpty": "Please enter a name.",
"deckRenameFailed": "Failed to rename deck.",
"deckDelete": "Delete", "deckDelete": "Delete",
"deckViews": "Views", "deckViews": "Views",
"deckCountFormat": "{filtered} / {total}", "deckCountFormat": "{filtered} / {total}",

View File

@@ -77,7 +77,7 @@
"loginPageTitle": "ログイン", "loginPageTitle": "ログイン",
"loginPageDesc": "デッキを保存・管理するには<br />アカウントでログインしてください。", "loginPageDesc": "デッキを保存・管理するには<br />アカウントでログインしてください。",
"signupPageTitle": "会員登録", "signupPageTitle": "会員登録",
"signupPageDesc": "UADECKアカウントを作成して<br />デッキを保存・共有しましょう。", "signupPageDesc": "UNIAREアカウントを作成して<br />デッキを保存・共有しましょう。",
"emailLabel": "メール", "emailLabel": "メール",
"passwordLabel": "パスワード", "passwordLabel": "パスワード",
"usernameLabel": "ユーザー名", "usernameLabel": "ユーザー名",
@@ -127,6 +127,10 @@
"deckMakePrivate": "非公開にする", "deckMakePrivate": "非公開にする",
"deckLoad": "読み込み", "deckLoad": "読み込み",
"deckCopyLink": "リンクをコピー", "deckCopyLink": "リンクをコピー",
"deckRename": "名前を変更",
"deckRenamePrompt": "新しいデッキ名を入力してください",
"deckRenameEmpty": "名前を入力してください。",
"deckRenameFailed": "名前の変更に失敗しました。",
"deckDelete": "削除", "deckDelete": "削除",
"deckViews": "閲覧", "deckViews": "閲覧",
"deckCountFormat": "{filtered} / {total}件", "deckCountFormat": "{filtered} / {total}件",

View File

@@ -77,7 +77,7 @@
"loginPageTitle": "로그인", "loginPageTitle": "로그인",
"loginPageDesc": "덱을 저장하고 관리하려면<br />계정으로 로그인하세요.", "loginPageDesc": "덱을 저장하고 관리하려면<br />계정으로 로그인하세요.",
"signupPageTitle": "회원가입", "signupPageTitle": "회원가입",
"signupPageDesc": "UADECK 계정을 만들고<br />덱을 저장·공유해 보세요.", "signupPageDesc": "UNIARE 계정을 만들고<br />덱을 저장·공유해 보세요.",
"emailLabel": "이메일", "emailLabel": "이메일",
"passwordLabel": "비밀번호", "passwordLabel": "비밀번호",
"usernameLabel": "사용자명", "usernameLabel": "사용자명",
@@ -127,6 +127,10 @@
"deckMakePrivate": "비공개 전환", "deckMakePrivate": "비공개 전환",
"deckLoad": "불러오기", "deckLoad": "불러오기",
"deckCopyLink": "링크 복사", "deckCopyLink": "링크 복사",
"deckRename": "이름 변경",
"deckRenamePrompt": "새 덱 이름을 입력하세요",
"deckRenameEmpty": "이름을 입력해 주세요.",
"deckRenameFailed": "이름 변경에 실패했습니다.",
"deckDelete": "삭제", "deckDelete": "삭제",
"deckViews": "조회", "deckViews": "조회",
"deckCountFormat": "{filtered} / {total}개", "deckCountFormat": "{filtered} / {total}개",

BIN
images/assets/uniare-h48.png LFS Normal file

Binary file not shown.

View File

@@ -10,7 +10,7 @@
<script src="/script/navigation.js"></script> <script src="/script/navigation.js"></script>
<script src="/script/api.js"></script> <script src="/script/api.js"></script>
<link rel="stylesheet" href="./style/auth-header.css" /> <link rel="stylesheet" href="./style/auth-header.css" />
<title>UADECK V4.0.0 - Union Arena Deck Builder</title> <title>UNIARE - Union Arena Deck Builder</title>
<style> <style>
body { body {
@@ -58,15 +58,9 @@
<body class="text-slate-900"> <body class="text-slate-900">
<nav class="sticky top-0 z-50 bg-white/70 backdrop-blur-lg border-b border-slate-200"> <nav class="sticky top-0 z-50 bg-white/70 backdrop-blur-lg border-b border-slate-200">
<div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"> <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div class="flex items-center"> <a href="/" class="inline-flex items-center">
<div class="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden"> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="h-8 w-auto" />
<img src="/images/assets/apple-touch-icon.png" alt="UA Logo" class="w-6 h-6 object-contain" /> </a>
</div>
<span class="text-xl font-extrabold tracking-tight text-slate-800">
UADECK
<span class="text-blue-600">v4.0.0</span>
</span>
</div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="language-selector flex gap-1 p-1 bg-slate-100 rounded-xl"> <div class="language-selector flex gap-1 p-1 bg-slate-100 rounded-xl">

View File

@@ -16,7 +16,7 @@ const client = new UaDeckDeckClient({
imageBase: 'https://uniare.sori.studio', imageBase: 'https://uniare.sori.studio',
}); });
// UADECK 공유 링크에서 덱 로드 // UNIARE 공유 링크에서 덱 로드
const deck = await client.fetchFromUrl('https://uniare.sori.studio/d/SHARE_CODE'); const deck = await client.fetchFromUrl('https://uniare.sori.studio/d/SHARE_CODE');
// 카드 사용 // 카드 사용

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UADECK API 연동 예제</title> <title>UNIARE API 연동 예제</title>
<style> <style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; } body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
input { width: 100%; padding: 0.5rem; margin: 0.5rem 0 1rem; box-sizing: border-box; } input { width: 100%; padding: 0.5rem; margin: 0.5rem 0 1rem; box-sizing: border-box; }
@@ -14,7 +14,7 @@
</style> </style>
</head> </head>
<body> <body>
<h1>UADECK API 연동 예제</h1> <h1>UNIARE API 연동 예제</h1>
<p>공유 링크 또는 shareCode로 덱을 불러옵니다.</p> <p>공유 링크 또는 shareCode로 덱을 불러옵니다.</p>
<label>API Base URL</label> <label>API Base URL</label>

View File

@@ -1,5 +1,5 @@
/** /**
* UADECK 덱 API 클라이언트 (옆 프로젝트 연동용) * UNIARE 덱 API 클라이언트 (옆 프로젝트 연동용)
* *
* @example * @example
* const client = new UaDeckDeckClient({ baseUrl: 'https://uniare.sori.studio/api/v1' }); * const client = new UaDeckDeckClient({ baseUrl: 'https://uniare.sori.studio/api/v1' });

View File

@@ -6,14 +6,13 @@
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/auth-pages.css" /> <link rel="stylesheet" href="./style/auth-pages.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet" />
<title>로그인 - UADECK</title> <title>로그인 - UNIARE</title>
</head> </head>
<body class="auth-page"> <body class="auth-page">
<nav class="auth-nav"> <nav class="auth-nav">
<div class="auth-nav-inner"> <div class="auth-nav-inner">
<a href="/" class="auth-brand"> <a href="/" class="auth-brand">
<img src="/images/assets/apple-touch-icon.png" alt="UA Logo" /> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
<span class="auth-brand-text">UADECK <span>v4.0.0</span></span>
</a> </a>
</div> </div>
</nav> </nav>
@@ -43,6 +42,8 @@
</p> </p>
</main> </main>
<footer class="auth-version-footer" aria-hidden="true">v4.0.0</footer>
<script src="/script/page-i18n.js"></script> <script src="/script/page-i18n.js"></script>
<script src="/script/api.js"></script> <script src="/script/api.js"></script>
<script> <script>

View File

@@ -4,7 +4,8 @@
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.12"
}, },
"scripts": { "scripts": {
"start": "docker compose -f docker-compose.prod.yml up -d --build && node -e \"console.log('\\nUADECK running: http://localhost:' + (process.env.WEB_PORT || '44082') + '\\nStop: npm stop\\nLogs: npm run logs\\n')\"", "build:css": "npx @tailwindcss/cli -i ./style/input.css -o ./style/output.css --minify",
"start": "docker compose -f docker-compose.prod.yml up -d --build && node -e \"console.log('\\nUNIARE running: http://localhost:' + (process.env.WEB_PORT || '44082') + '\\nStop: npm stop\\nLogs: npm run logs\\n')\"",
"stop": "docker compose -f docker-compose.prod.yml down", "stop": "docker compose -f docker-compose.prod.yml down",
"logs": "docker compose -f docker-compose.prod.yml logs -f", "logs": "docker compose -f docker-compose.prod.yml logs -f",
"dev": "docker compose -f docker-compose.prod.yml up --build", "dev": "docker compose -f docker-compose.prod.yml up --build",

View File

@@ -8,12 +8,14 @@
<link rel="stylesheet" href="./style/app-shell.css" /> <link rel="stylesheet" href="./style/app-shell.css" />
<link rel="stylesheet" href="./style/auth-header.css" /> <link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/deck-history.css" /> <link rel="stylesheet" href="./style/deck-history.css" />
<title>공개 덱 - UADECK</title> <title>공개 덱 - UNIARE</title>
</head> </head>
<body class="app-page deck-history-page"> <body class="app-page deck-history-page">
<nav class="app-nav"> <nav class="app-nav">
<div class="app-nav-inner"> <div class="app-nav-inner">
<a href="/" class="app-brand">UADECK <span>v4.0.0</span></a> <a href="/" class="app-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
</a>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div> </div>
</nav> </nav>
@@ -189,7 +191,7 @@
<div class="deck-card-body"> <div class="deck-card-body">
<span class="deck-card-series">${seriesLabel}</span> <span class="deck-card-series">${seriesLabel}</span>
<h2 class="deck-card-title">${title}</h2> <h2 class="deck-card-title">${title}</h2>
<div class="deck-card-meta">${escapeHtml(d.author)} · ${escapeHtml(t('deckViews', '조회'))} ${d.view_count} · ${date}</div> <div class="deck-card-meta">${escapeHtml(d.author)} · ${date}</div>
<div class="deck-card-actions"> <div class="deck-card-actions">
${favoriteButtonHtml(d.share_code)} ${favoriteButtonHtml(d.share_code)}
<button type="button" class="deck-card-btn deck-card-btn-load" data-load="${escapeHtml(d.share_code)}" data-series="${escapeHtml(d.series)}" data-name="${escapeHtml(d.series_name || '')}">${escapeHtml(t('deckLoad', '불러오기'))}</button> <button type="button" class="deck-card-btn deck-card-btn-load" data-load="${escapeHtml(d.share_code)}" data-series="${escapeHtml(d.series)}" data-name="${escapeHtml(d.series_name || '')}">${escapeHtml(t('deckLoad', '불러오기'))}</button>

View File

@@ -8,12 +8,14 @@
<link rel="stylesheet" href="./style/app-shell.css" /> <link rel="stylesheet" href="./style/app-shell.css" />
<link rel="stylesheet" href="./style/auth-header.css" /> <link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/profile.css" /> <link rel="stylesheet" href="./style/profile.css" />
<title>내 프로필 - UADECK</title> <title>내 프로필 - UNIARE</title>
</head> </head>
<body class="app-page profile-page"> <body class="app-page profile-page">
<nav class="app-nav"> <nav class="app-nav">
<div class="app-nav-inner"> <div class="app-nav-inner">
<a href="/" class="app-brand">UADECK <span>v4.0.0</span></a> <a href="/" class="app-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
</a>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div> </div>
</nav> </nav>

View File

@@ -424,13 +424,15 @@
</div> </div>
</div> </div>
<div class="sticky top-0 z-50 h-16 bg-white/80 backdrop-blur-md border-b px-4 py-3 flex justify-between items-center"> <div class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex justify-between items-center">
<button id="i18n-editDeck" onclick="goBackToCardList()" class="font-bold text-slate-600 hover:text-blue-600 transition-colors">← EDIT DECK</button> <button id="i18n-editDeck" onclick="goBackToCardList()" class="font-bold text-slate-600 hover:text-blue-600 transition-colors">← EDIT DECK</button>
<div id="i18n-previewTitle" class="font-bold text-slate-800 uppercase tracking-wider">Deck Preview</div> <div id="i18n-previewTitle" class="font-bold text-slate-800 uppercase tracking-wider">Deck Preview</div>
<div id="auth-header-slot"></div> <div id="auth-header-slot"></div>
</div>
</div> </div>
<div class="max-w-[1400px] mx-auto p-4 flex flex-col lg:flex-row gap-6"> <div class="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex flex-col lg:flex-row gap-6">
<aside class="w-full lg:w-80 space-y-4 lg:sticky lg:top-20 lg:h-fit"> <aside class="w-full lg:w-80 space-y-4 lg:sticky lg:top-20 lg:h-fit">
<div class="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-6"> <div class="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-6">
<div> <div>
@@ -1177,7 +1179,7 @@
loader.setProgress(92, t('generatingImage', '이미지 생성 중...')); loader.setProgress(92, t('generatingImage', '이미지 생성 중...'));
const now = new Date(); const now = new Date();
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
const fileNameStr = `uadeck_${deckName}_${timestamp}`; const fileNameStr = `uniare_${deckName}_${timestamp}`;
const link = document.createElement('a'); const link = document.createElement('a');
link.download = `${fileNameStr}.png`; link.download = `${fileNameStr}.png`;

View File

@@ -1,5 +1,5 @@
/** /**
* UADECK API 클라이언트 * UNIARE API 클라이언트
*/ */
const UaDeckApi = (() => { const UaDeckApi = (() => {
const TOKEN_KEY = 'uadeck_token'; const TOKEN_KEY = 'uadeck_token';
@@ -10,6 +10,7 @@ const UaDeckApi = (() => {
* @returns {string} * @returns {string}
*/ */
function getBaseUrl() { function getBaseUrl() {
if (window.UNIARE_API_BASE) return window.UNIARE_API_BASE;
if (window.UADECK_API_BASE) return window.UADECK_API_BASE; if (window.UADECK_API_BASE) return window.UADECK_API_BASE;
const { hostname, port, protocol } = window.location; const { hostname, port, protocol } = window.location;

View File

@@ -1,15 +1,12 @@
/** /**
* 썸네일 미리보기 확대 (클릭 고정 / 길게 누르기) * 썸네일 미리보기 확대 (길게 누르는 동안만 표시)
*/ */
(function initDeckPreviewHold() { (function initDeckPreviewHold() {
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
let overlay = null; let overlay = null;
/** @type {number} */ /** @type {number|null} */
let pressStartAt = 0; let holdTimer = null;
/** @type {boolean} */
let suppressClick = false;
/** @type {number} */ /** @type {number} */
const HOLD_THRESHOLD_MS = 280; const HOLD_THRESHOLD_MS = 280;
@@ -25,21 +22,11 @@
overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML = overlay.innerHTML =
'<div class="deck-preview-hold-box"><img alt="" draggable="false" /></div>'; '<div class="deck-preview-hold-box"><img alt="" draggable="false" /></div>';
overlay.addEventListener('click', (e) => {
if (e.target === overlay) hide();
});
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
return overlay; return overlay;
} }
/**
* @returns {boolean}
*/
function isVisible() {
return Boolean(overlay && !overlay.classList.contains('hidden'));
}
/** /**
* @param {string} src * @param {string} src
*/ */
@@ -51,10 +38,16 @@
el.classList.remove('hidden'); el.classList.remove('hidden');
} }
function clearHoldTimer() {
if (holdTimer !== null) {
window.clearTimeout(holdTimer);
holdTimer = null;
}
}
function hide() { function hide() {
clearHoldTimer();
if (overlay) overlay.classList.add('hidden'); if (overlay) overlay.classList.add('hidden');
pressStartAt = 0;
suppressClick = false;
} }
/** /**
@@ -75,6 +68,20 @@
return hold.dataset.previewSrc || ''; return hold.dataset.previewSrc || '';
} }
/**
* @param {PointerEvent} e
* @param {HTMLElement|null} hold
*/
function releaseCapture(e, hold) {
if (hold?.releasePointerCapture && e.pointerId !== undefined) {
try {
hold.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
}
}
function initGlobal() { function initGlobal() {
if (document.documentElement.dataset.deckHoldInit === '1') return; if (document.documentElement.dataset.deckHoldInit === '1') return;
document.documentElement.dataset.deckHoldInit = '1'; document.documentElement.dataset.deckHoldInit = '1';
@@ -87,8 +94,11 @@
const src = getPreviewSrc(hold); const src = getPreviewSrc(hold);
if (!src) return; if (!src) return;
pressStartAt = Date.now(); clearHoldTimer();
show(src); holdTimer = window.setTimeout(() => {
holdTimer = null;
show(src);
}, HOLD_THRESHOLD_MS);
if (hold.setPointerCapture && e.pointerId !== undefined) { if (hold.setPointerCapture && e.pointerId !== undefined) {
try { try {
@@ -104,32 +114,9 @@
document.addEventListener( document.addEventListener(
'pointerup', 'pointerup',
(e) => { (e) => {
if (!pressStartAt) return;
const hold = findHoldTarget(e); const hold = findHoldTarget(e);
if (hold?.releasePointerCapture && e.pointerId !== undefined) { releaseCapture(e, hold);
try { hide();
hold.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
}
const heldMs = Date.now() - pressStartAt;
pressStartAt = 0;
if (heldMs >= HOLD_THRESHOLD_MS) {
hide();
suppressClick = true;
window.setTimeout(() => {
suppressClick = false;
}, 0);
} else {
suppressClick = true;
window.setTimeout(() => {
suppressClick = false;
}, 400);
}
}, },
true, true,
); );
@@ -142,25 +129,6 @@
true, true,
); );
document.addEventListener('click', (e) => {
if (suppressClick) return;
const hold = findHoldTarget(e);
if (hold) {
const src = getPreviewSrc(hold);
if (!src) return;
e.preventDefault();
e.stopPropagation();
if (isVisible()) hide();
else show(src);
return;
}
if (isVisible() && e.target === overlay) {
hide();
}
});
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') hide(); if (e.key === 'Escape') hide();
}); });

View File

@@ -6,14 +6,13 @@
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/auth-pages.css" /> <link rel="stylesheet" href="./style/auth-pages.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet" />
<title>회원가입 - UADECK</title> <title>회원가입 - UNIARE</title>
</head> </head>
<body class="auth-page"> <body class="auth-page">
<nav class="auth-nav"> <nav class="auth-nav">
<div class="auth-nav-inner"> <div class="auth-nav-inner">
<a href="/" class="auth-brand"> <a href="/" class="auth-brand">
<img src="/images/assets/apple-touch-icon.png" alt="UA Logo" /> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" />
<span class="auth-brand-text">UADECK <span>v4.0.0</span></span>
</a> </a>
</div> </div>
</nav> </nav>
@@ -21,7 +20,7 @@
<main class="auth-main"> <main class="auth-main">
<header class="auth-header"> <header class="auth-header">
<h1 data-i18n="signupPageTitle">회원가입</h1> <h1 data-i18n="signupPageTitle">회원가입</h1>
<p data-i18n-html="signupPageDesc">UADECK 계정을 만들고<br />덱을 저장·공유해 보세요.</p> <p data-i18n-html="signupPageDesc">UNIARE 계정을 만들고<br />덱을 저장·공유해 보세요.</p>
</header> </header>
<form id="signup-form" class="auth-card"> <form id="signup-form" class="auth-card">

View File

@@ -29,15 +29,25 @@
} }
.admin-brand { .admin-brand {
font-size: 1.25rem; display: inline-flex;
font-weight: 800; align-items: center;
color: #1e293b; gap: 0.625rem;
text-decoration: none; text-decoration: none;
letter-spacing: -0.025em; letter-spacing: -0.025em;
line-height: 0;
} }
.admin-brand span { .admin-brand .app-brand-logo {
color: #2563eb; height: 2rem;
width: auto;
display: block;
}
.admin-brand-label {
font-size: 1rem;
font-weight: 800;
color: #1e293b;
line-height: 1.2;
} }
.admin-main { .admin-main {

View File

@@ -38,16 +38,16 @@
} }
.app-brand { .app-brand {
font-size: 1.25rem; display: inline-flex;
font-weight: 800; align-items: center;
color: #1e293b;
text-decoration: none; text-decoration: none;
letter-spacing: -0.025em; line-height: 0;
white-space: nowrap;
} }
.app-brand span { .app-brand-logo {
color: #2563eb; height: 2rem;
width: auto;
display: block;
} }
.app-ad-top { .app-ad-top {

View File

@@ -30,27 +30,17 @@
} }
.auth-brand { .auth-brand {
display: flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem;
text-decoration: none; text-decoration: none;
color: #1e293b; color: #1e293b;
line-height: 0;
} }
.auth-brand img { .auth-brand .app-brand-logo {
width: 2rem;
height: 2rem; height: 2rem;
object-fit: contain; width: auto;
} display: block;
.auth-brand-text {
font-size: 1.25rem;
font-weight: 800;
letter-spacing: -0.025em;
}
.auth-brand-text span {
color: #2563eb;
} }
.auth-main { .auth-main {
@@ -185,3 +175,12 @@
.auth-footer a:hover { .auth-footer a:hover {
text-decoration: underline; text-decoration: underline;
} }
.auth-version-footer {
text-align: center;
padding: 0 1.5rem 1.25rem;
font-size: 0.625rem;
font-weight: 500;
color: #cbd5e1;
letter-spacing: 0.02em;
}

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,6 @@
.profile-content { .profile-content {
width: 100%; width: 100%;
max-width: 42rem;
} }
.profile-header { .profile-header {