v4.0.5: 로그인·덱·관리자 Tailwind 레이아웃 통합

공통 UI를 components.css로 빌드에 포함하고 로그인·회원가입·내 덱·공개 덱·관리자 화면 레이아웃을 정리했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 15:48:09 +09:00
parent 93e81ce3ea
commit ba89235207
17 changed files with 1376 additions and 99 deletions

3
.gitignore vendored
View File

@@ -28,3 +28,6 @@ logs/
# 기타 # 기타
/coverage /coverage
# 로컬 초안 (git 미추적)
docs/posts/

View File

@@ -175,14 +175,14 @@ convention.md
예시 예시
```md ```md
/posts /deckHistory.html
pages/posts/index.vue deckHistory.html
/posts/[slug] /login.html
pages/posts/[slug].vue login.html
SiteHeader SiteHeader
components/site/SiteHeader.vue style/brand.css
``` ```
--- ---

View File

@@ -1 +1 @@
v4.0.3 v4.0.5

View File

@@ -4,15 +4,13 @@
<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" />
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/brand.css" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+KR:wght@400;700;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/deck-history.css" />
<title>관리자 - UNIARE</title> <title>관리자 - UNIARE</title>
</head> </head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 font-['Pretendard','Inter','Noto_Sans_KR',sans-serif] text-slate-900"> <body class="app-page">
<nav class="sticky top-0 z-50 border-b border-slate-200 bg-white/70 backdrop-blur-xl"> <nav class="app-nav">
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6"> <div class="app-nav-inner">
<a href="/" class="flex items-center gap-2.5 tracking-tight"> <a href="/" class="inline-flex items-center gap-2.5 no-underline">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" /> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" />
<span class="text-base font-extrabold leading-tight text-slate-800">관리자</span> <span class="text-base font-extrabold leading-tight text-slate-800">관리자</span>
</a> </a>
@@ -20,21 +18,22 @@
</div> </div>
</nav> </nav>
<main class="mx-auto max-w-7xl px-4 py-8 pb-20 sm:px-6 sm:pt-16"> <div class="app-shell">
<main class="app-main w-full">
<header class="mb-8"> <header class="mb-8">
<h1 class="mb-2 text-[1.75rem] font-black tracking-tight">관리자</h1> <h1 class="mb-2 text-3xl font-black tracking-tight text-slate-900">관리자</h1>
<p class="m-0 text-sm text-slate-500">회원과 저장된 덱을 관리합니다.</p> <p class="text-sm text-slate-500">회원과 저장된 덱을 관리합니다.</p>
</header> </header>
<div class="mb-7 flex gap-1 border-b border-slate-200"> <div class="mb-7 flex gap-1 border-b border-slate-200" role="tablist">
<button type="button" data-tab="users" class="-mb-px cursor-pointer border-0 border-b-2 border-blue-600 bg-transparent px-[1.125rem] py-3 text-sm font-bold text-blue-600 transition-colors hover:text-slate-600">회원 관리</button> <button type="button" data-tab="users" role="tab" aria-selected="true" class="-mb-px cursor-pointer border-0 border-b-2 border-blue-600 bg-transparent px-4 py-3 text-sm font-bold text-blue-600 transition-colors hover:text-slate-600">회원 관리</button>
<button type="button" data-tab="decks" class="-mb-px cursor-pointer border-0 border-b-2 border-transparent bg-transparent px-[1.125rem] py-3 text-sm font-bold text-slate-400 transition-colors hover:text-slate-600">덱 관리</button> <button type="button" data-tab="decks" role="tab" aria-selected="false" class="-mb-px cursor-pointer border-0 border-b-2 border-transparent bg-transparent px-4 py-3 text-sm font-bold text-slate-400 transition-colors hover:text-slate-600">덱 관리</button>
</div> </div>
<section id="tab-users"> <section id="tab-users">
<div class="mb-5 flex gap-3"> <div class="mb-5 flex flex-wrap gap-3">
<input type="search" id="user-search" class="min-w-0 flex-1 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm outline-none transition focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" placeholder="이메일, 사용자명 검색..." /> <input type="search" id="user-search" class="box-border min-w-0 flex-1 rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-sm outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/15" placeholder="이메일, 사용자명 검색..." />
<button type="button" id="user-search-btn" class="cursor-pointer rounded-xl border-0 bg-slate-800 px-4 py-2.5 text-[0.8125rem] font-bold text-white hover:bg-slate-700">검색</button> <button type="button" id="user-search-btn" class="cursor-pointer rounded-xl border-0 bg-slate-800 px-5 py-2.5 text-sm font-bold text-white transition hover:bg-slate-700">검색</button>
</div> </div>
<div id="user-list" class="flex flex-col gap-3"></div> <div id="user-list" class="flex flex-col gap-3"></div>
</section> </section>
@@ -50,7 +49,7 @@
</div> </div>
</div> </div>
<div id="admin-deck-list"></div> <div id="admin-deck-list"></div>
<section class="mt-8 flex flex-col items-stretch justify-between gap-4 rounded-2xl border border-slate-200 bg-white px-5 py-4 sm:flex-row sm:items-center"> <section class="mt-8 flex flex-col items-stretch justify-between gap-4 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm sm:flex-row sm:items-center">
<div> <div>
<h2 class="mb-1 text-[0.9375rem] font-bold">미참조 썸네일</h2> <h2 class="mb-1 text-[0.9375rem] font-bold">미참조 썸네일</h2>
<p id="orphan-preview-status" class="m-0 text-xs text-slate-500">확인 전</p> <p id="orphan-preview-status" class="m-0 text-xs text-slate-500">확인 전</p>
@@ -62,7 +61,8 @@
</section> </section>
<div id="orphan-preview-list" class="mt-3 hidden max-h-56 overflow-y-auto rounded-xl border border-slate-200 bg-white px-4 py-2"></div> <div id="orphan-preview-list" class="mt-3 hidden max-h-56 overflow-y-auto rounded-xl border border-slate-200 bg-white px-4 py-2"></div>
</section> </section>
</main> </main>
</div>
<!-- 회원 정보 모달 --> <!-- 회원 정보 모달 -->
<div id="edit-modal" class="fixed inset-0 z-[100] hidden items-center justify-center bg-slate-900/50 p-4"> <div id="edit-modal" class="fixed inset-0 z-[100] hidden items-center justify-center bg-slate-900/50 p-4">
@@ -484,6 +484,7 @@
tabButton.classList.toggle('text-blue-600', isActive); tabButton.classList.toggle('text-blue-600', isActive);
tabButton.classList.toggle('border-transparent', !isActive); tabButton.classList.toggle('border-transparent', !isActive);
tabButton.classList.toggle('text-slate-400', !isActive); tabButton.classList.toggle('text-slate-400', !isActive);
tabButton.setAttribute('aria-selected', isActive ? 'true' : 'false');
}); });
document.getElementById('tab-users').classList.toggle('hidden', btn.dataset.tab !== 'users'); document.getElementById('tab-users').classList.toggle('hidden', btn.dataset.tab !== 'users');
document.getElementById('tab-decks').classList.toggle('hidden', btn.dataset.tab !== 'decks'); document.getElementById('tab-decks').classList.toggle('hidden', btn.dataset.tab !== 'decks');

View File

@@ -5,9 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script>
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/app-shell.css" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+KR:wght@400;700;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/deck-history.css" />
<title>내 덱 관리 - UNIARE</title> <title>내 덱 관리 - UNIARE</title>
</head> </head>
<body class="app-page deck-history-page"> <body class="app-page deck-history-page">

View File

@@ -35,7 +35,9 @@
- 새 단순 스타일은 Tailwind 유틸리티를 우선 검토하되 기존 페이지 패턴과 빌드 여부를 함께 확인한다. - 새 단순 스타일은 Tailwind 유틸리티를 우선 검토하되 기존 페이지 패턴과 빌드 여부를 함께 확인한다.
- 반복되는 공통 스타일은 공통 CSS 또는 컴포넌트 역할의 클래스로 분리한다. - 반복되는 공통 스타일은 공통 CSS 또는 컴포넌트 역할의 클래스로 분리한다.
- `style/output.css`를 직접 편집하지 않고 `npm run build:css`로 생성한다. - `style/output.css`를 직접 편집하지 않고 `npm run build:css`로 생성한다.
- 헤더 로고는 `style/brand.css`의 `.app-brand-logo`만 사용한다. - Tailwind 스캔 대상은 `style/input.css`의 `@source`로 지정한다. HTML·JS 전환 후에는 반드시 CSS를 재빌드한다.
- 공통 UI(앱 셸·덱 카드·프로필·인증 헤더)는 `style/components.css`의 `@layer components`에 두고 `input.css`에서 import한다.
- 각 HTML 페이지는 `style/output.css`만 로드한다(`brand.css`는 빌드에 포함).
- 다크모드와 Pretendard 계열 기본 글꼴을 고려한다. - 다크모드와 Pretendard 계열 기본 글꼴을 고려한다.
## API·DB ## API·DB

View File

@@ -1,5 +1,15 @@
# 의사결정 이력 # 의사결정 이력
## 2026-06-23 v4.0.5 — 공통 UI는 Tailwind components 레이어로 통합
💡 화면마다 분리된 CSS 파일을 유지하면 Tailwind 빌드 누락·로드 순서 문제가 반복되기 때문.
🔧 `app-shell`·`deck-history`·`profile`·`auth-header`를 `style/components.css`로 옮기고 페이지는 `output.css`만 로드.
## 2026-06-23 v4.0.4 — 덱 저장과 공개는 회원 기능으로 제한
💡 이미지 다운로드는 자유롭게 제공하되 서버 저장·공개 기능은 계정 기반 관리와 연결하기 위함.
🔧 비회원 저장 API를 차단하고 결과 화면은 로그인 후 저장할 수 있음을 명확히 안내.
## 2026-06-23 v4.0.3 — 검색 설명은 정적 기본 콘텐츠를 제공 ## 2026-06-23 v4.0.3 — 검색 설명은 정적 기본 콘텐츠를 제공
💡 JavaScript 번역 로딩 전 빈 문구나 네트워크 오류가 검색 결과 설명으로 수집되는 것을 막기 위함. 💡 JavaScript 번역 로딩 전 빈 문구나 네트워크 오류가 검색 결과 설명으로 수집되는 것을 막기 위함.

View File

@@ -43,10 +43,14 @@ script/api.js
인증 헤더 인증 헤더
script/auth-header.js script/auth-header.js
style/auth-header.css style/components.css (@layer components)
페이지 공통 레이아웃 페이지 공통 레이아웃·덱 카드·프로필·광고 레일
style/app-shell.css style/components.css
Tailwind 빌드 진입점
style/input.css
style/output.css
브랜드 로고 규격 브랜드 로고 규격
style/brand.css style/brand.css
@@ -83,18 +87,11 @@ script/ads.js
## 페이지 전용 스타일 ## 페이지 전용 스타일
```text ```text
프로필 관리자·로그인·회원가입
style/profile.css admin.html, login.html, signup.html — Tailwind 유틸리티
내 덱·공개 덱 내 덱·공개 덱·프로필
style/deck-history.css deckHistory.html, preset.html, profile.html — HTML 클래스 + components.css
관리자
admin.html — 관리자 화면 Tailwind 유틸리티
로그인·회원가입
login.html
signup.html
Tailwind 입력·산출물 Tailwind 입력·산출물
style/input.css style/input.css

View File

@@ -3,7 +3,7 @@
## 프로젝트 개요 ## 프로젝트 개요
- 서비스명: `UNIARE - Union Arena Deck Builder` - 서비스명: `UNIARE - Union Arena Deck Builder`
- 현재 버전: `v4.0.3` - 현재 버전: `v4.0.4`
- 프론트: 정적 HTML, CSS, JavaScript - 프론트: 정적 HTML, CSS, JavaScript
- 백엔드: Express, SQLite(`better-sqlite3`) - 백엔드: Express, SQLite(`better-sqlite3`)
- 운영: Docker Compose 기반 Nginx + API - 운영: Docker Compose 기반 Nginx + API
@@ -17,7 +17,7 @@
| `/` | 시리즈 선택, 언어 전환, 인증 헤더 | | `/` | 시리즈 선택, 언어 전환, 인증 헤더 |
| `/cardList.html?series=<key>` | 카드 검색·필터, 수량 조절, 키 카드 지정, 덱 편집 | | `/cardList.html?series=<key>` | 카드 검색·필터, 수량 조절, 키 카드 지정, 덱 편집 |
| `/cardList.html?d=<shareCode>` | API 공유 덱 불러오기 및 편집 | | `/cardList.html?d=<shareCode>` | API 공유 덱 불러오기 및 편집 |
| `/result.html` | 덱 미리보기, PNG 다운로드, 덱 저장 | | `/result.html` | 덱 미리보기, PNG 다운로드, 회원 덱 저장 |
| `/login.html` | 로그인 | | `/login.html` | 로그인 |
| `/signup.html` | 회원가입 | | `/signup.html` | 회원가입 |
| `/profile.html` | 이메일·사용자명·아바타·비밀번호·덱 공개 기본값 관리 | | `/profile.html` | 이메일·사용자명·아바타·비밀번호·덱 공개 기본값 관리 |
@@ -90,14 +90,16 @@
- `admin`은 회원 상태·역할·비밀번호와 전체 덱을 관리할 수 있다. - `admin`은 회원 상태·역할·비밀번호와 전체 덱을 관리할 수 있다.
- 관리자는 자신의 관리자 역할을 해제하거나 자신의 계정을 비활성화할 수 없다. - 관리자는 자신의 관리자 역할을 해제하거나 자신의 계정을 비활성화할 수 없다.
- 비공개 덱은 소유자와 관리자만 조회할 수 있다. - 비공개 덱은 소유자와 관리자만 조회할 수 있다.
- 비회원은 덱을 서버에 저장하거나 공개 링크를 생성할 수 없다.
- 로그인 사용자의 즐겨찾기는 계정 DB에 저장되어 브라우저와 기기가 달라도 유지된다. - 로그인 사용자의 즐겨찾기는 계정 DB에 저장되어 브라우저와 기기가 달라도 유지된다.
- 비로그인 즐겨찾기는 브라우저에 저장하고 로그인 시 유효한 덱만 계정으로 이전한다. - 비로그인 즐겨찾기는 브라우저에 저장하고 로그인 시 유효한 덱만 계정으로 이전한다.
## 덱 저장 정책 ## 덱 저장 정책
- 공유 코드는 저장 시 생성되는 8자 코드다. - 공유 코드는 저장 시 생성되는 8자 코드다.
- 회원 덱은 만료 없이 저장하며 공개 여부를 선택할 수 있다. - 덱 저장과 공개 링크 생성은 로그인한 회원만 사용할 수 있다.
- 비회원 덱은 공개 상태로 저장되고 30일 뒤 만료된다. - 비회원은 덱 구성과 PNG 이미지 다운로드만 사용할 수 있다.
- 기존 비회원 공유 덱은 호환을 위해 원래 만료일까지 조회할 수 있다.
- DB의 `cards_json`에는 `{number, count, key}`만 저장한다. - DB의 `cards_json`에는 `{number, count, key}`만 저장한다.
- API 응답 시 `datas/<series>.js`에서 이미지·색상·희귀도·트리거 정보를 합성한다. - API 응답 시 `datas/<series>.js`에서 이미지·색상·희귀도·트리거 정보를 합성한다.
- 공개 덱 목록은 `share_type='user'`, `is_public=1`, 미만료 조건을 사용한다. - 공개 덱 목록은 `share_type='user'`, `is_public=1`, 미만료 조건을 사용한다.
@@ -127,13 +129,13 @@
| `share_code` | 고유 공유 코드 | | `share_code` | 고유 공유 코드 |
| `share_type` | `user` 또는 레거시 `preset` | | `share_type` | `user` 또는 레거시 `preset` |
| `content_hash` | 레거시 프리셋 중복 판별 | | `content_hash` | 레거시 프리셋 중복 판별 |
| `user_id` | 소유 회원, 비회원은 `null` | | `user_id` | 소유 회원, 과거 비회원 덱은 `null` |
| `series`, `series_name` | 시리즈 키와 표시명 | | `series`, `series_name` | 시리즈 키와 표시명 |
| `name` | 덱 이름 | | `name` | 덱 이름 |
| `cards_json` | 최소 카드 구성 JSON | | `cards_json` | 최소 카드 구성 JSON |
| `is_public` | 공개 여부 | | `is_public` | 공개 여부 |
| `view_count` | 상세 조회 횟수 | | `view_count` | 상세 조회 횟수 |
| `expires_at` | 비회원 공유 만료 시각 | | `expires_at` | 과거 비회원 공유 덱의 만료 시각 |
| `preview_image` | 저장된 미리보기 파일 정보 | | `preview_image` | 저장된 미리보기 파일 정보 |
| `created_at`, `updated_at` | 생성·수정 시각 | | `created_at`, `updated_at` | 생성·수정 시각 |
@@ -172,7 +174,7 @@
| GET | `/api/v1/decks/:shareCode/preview` | 공개 | 덱 미리보기 PNG | | GET | `/api/v1/decks/:shareCode/preview` | 공개 | 덱 미리보기 PNG |
| GET | `/api/v1/decks/:shareCode` | 조건부 | 공개 덱 또는 소유 덱 상세 | | GET | `/api/v1/decks/:shareCode` | 조건부 | 공개 덱 또는 소유 덱 상세 |
| GET | `/api/v1/decks` | 회원 | 내 덱 목록 | | GET | `/api/v1/decks` | 회원 | 내 덱 목록 |
| POST | `/api/v1/decks` | 공개 | 회원 또는 비회원 덱 저장 | | POST | `/api/v1/decks` | 회원 | 내 덱 저장과 공개 링크 생성 |
| PUT | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 수정 | | PUT | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 수정 |
| DELETE | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 삭제 | | DELETE | `/api/v1/decks/:shareCode` | 소유자·관리자 | 덱 삭제 |
| GET | `/api/v1/decks/favorites` | 회원 | 내 즐겨찾기 코드 목록 | | GET | `/api/v1/decks/favorites` | 회원 | 내 즐겨찾기 코드 목록 |
@@ -200,7 +202,7 @@
- 기존 CSS는 `todo.md`의 화면별 체크리스트 순서에 따라 점진적으로 Tailwind로 전환한다. - 기존 CSS는 `todo.md`의 화면별 체크리스트 순서에 따라 점진적으로 Tailwind로 전환한다.
- 관리자와 로그인·회원가입 화면은 Tailwind 전환을 완료하고 전용 CSS를 제거했다. - 관리자와 로그인·회원가입 화면은 Tailwind 전환을 완료하고 전용 CSS를 제거했다.
- 관리자 탭·모달 상태는 JavaScript에서 `border-*`, `text-*`, `hidden`, `flex` 유틸리티를 전환한다. - 관리자 탭·모달 상태는 JavaScript에서 `border-*`, `text-*`, `hidden`, `flex` 유틸리티를 전환한다.
- 관리자 덱 목록이 재사용하는 `style/deck-history.css`는 내 덱·공개 덱 화면 전환 때 함께 정리한다. - 덱 카드·툴바·프로필·앱 셸 스타일은 `style/components.css`(`@layer components`)에 통합되어 `output.css`로 빌드된다.
- `style/output.css`는 `style/input.css`에서 생성하는 빌드 산출물이다. - `style/output.css`는 `style/input.css`에서 생성하는 빌드 산출물이다.
- 헤더 로고 크기는 `style/brand.css`만 기준으로 사용한다. - 헤더 로고 크기는 `style/brand.css`만 기준으로 사용한다.
- 기본 글꼴은 Pretendard 계열을 우선 사용한다. - 기본 글꼴은 Pretendard 계열을 우선 사용한다.

View File

@@ -22,13 +22,9 @@
## 화면별 Tailwind 전환 ## 화면별 Tailwind 전환
- [x] 관리자 (`admin.html`, 전용 CSS 제거) - [x] 관리자 (`admin.html`)
- [x] 로그인·회원가입 (`login.html`, `signup.html`, 전용 CSS 제거) - [x] 로그인·회원가입 (`login.html`, `signup.html`)
- [ ] 프로필 (`profile.html`, `style/profile.css`) - [x] 내 덱 (`deckHistory.html` — `components.css` 빌드 포함)
- [ ] 내 덱 (`deckHistory.html`, `style/deck-history.css`) - [x] 공개 덱 (`preset.html` — `components.css` 빌드 포함)
- [ ] 공개 덱 (`preset.html`, `style/deck-history.css`) - [ ] 프로필 (`profile.html`, `style/profile.css` — 기존 CSS 유지)
- [ ] 공통 인증 헤더 (`script/auth-header.js`, `style/auth-header.css`) - [ ] 카드 목록·덱 결과·메인 (`cardList.html`, `result.html`, `index.html` — 원래 Tailwind 기반, 레거시 CSS 정리만 검토)
- [ ] 공통 앱 셸 (`style/app-shell.css`)
- [ ] 카드 목록 (`cardList.html`, `style/style.css`)
- [ ] 덱 결과 (`result.html`)
- [ ] 메인·공통 내비게이션 (`index.html`, `style/navigation.css`)

View File

@@ -1,5 +1,31 @@
# 업데이트 이력 # 업데이트 이력
## 2026-06-23 / v4.0.5 — 로그인·덱·관리자 Tailwind 레이아웃 통합
### 수행 작업
- `style/components.css` 신설: 앱 셸·인증 헤더·덱 카드 스타일을 `@layer components`로 빌드에 포함
- `style/input.css`: `brand.css`·`components.css` import, `@source`로 HTML·JS 스캔
- 로그인·회원가입·관리자: `app-page`/`app-nav`/`app-shell` + Tailwind 유틸리티로 레이아웃 정리
- 내 덱·공개 덱: `output.css` 단일 로드 (AdSense 레일 유지)
- `cardList`·`result`·`index`·`profile` 등 기존 페이지는 변경 없음
## 2026-06-23 / v4.0.4 — Tailwind 빌드·로그인/관리자 레이아웃 수정
### 수행 작업
- `style/input.css`: `@source`로 HTML·JS만 스캔하도록 지정 (`images/`·`datas/` 제외) — arbitrary 클래스가 `output.css`에 누락되던 문제 해결
- `focus:ring-3`(미지원) → `focus:ring-2`로 수정
- `npm run build:css` 재실행
- `docs/posts/` gitignore 처리 및 프로젝트 문서에서 posts 참조 제거
## 2026-06-23 / v4.0.4 — 비회원 덱 저장 제한
### 수행 작업
- 비회원은 덱 구성과 PNG 이미지 다운로드만 이용하도록 정책 변경
- `POST /api/v1/decks`에 필수 인증 적용
- 결과 화면의 비회원 저장 버튼을 `로그인 후 덱 저장`으로 안내
- 기존 비회원 공유 덱은 만료일까지 조회 호환 유지
- 서비스 소개 글과 기능 명세·변경사항 갱신
## 2026-06-23 / v4.0.3 — 계정 즐겨찾기 동기화 ## 2026-06-23 / v4.0.3 — 계정 즐겨찾기 동기화
### 수행 작업 ### 수행 작업

View File

@@ -4,34 +4,34 @@
<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" />
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/brand.css" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+KR:wght@400;700;900&display=swap" rel="stylesheet" />
<title>로그인 - UNIARE</title> <title>로그인 - UNIARE</title>
</head> </head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 font-['Pretendard','Inter','Noto_Sans_KR',sans-serif] text-slate-900"> <body class="app-page">
<nav class="sticky top-0 z-50 border-b border-slate-200 bg-white/70 backdrop-blur-xl"> <nav class="app-nav">
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6"> <div class="app-nav-inner">
<a href="/" class="inline-flex shrink-0 items-center leading-none text-slate-800 no-underline"> <a href="/" class="app-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" /> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" />
</a> </a>
</div> </div>
</nav> </nav>
<main class="mx-auto max-w-[28rem] px-6 py-12 sm:py-16"> <main class="mx-auto w-full max-w-md px-6 pt-12 pb-16 sm:pt-16 sm:pb-20">
<header class="mb-8 text-center"> <header class="mb-8 text-center">
<h1 class="mb-2 text-[1.75rem] font-black tracking-[-0.02em] text-slate-900" data-i18n="loginPageTitle">로그인</h1> <h1 class="mb-2 text-3xl font-black tracking-tight text-slate-900" data-i18n="loginPageTitle">로그인</h1>
<p class="m-0 text-sm leading-normal text-slate-500" data-i18n-html="loginPageDesc">덱을 저장하고 관리하려면<br />계정으로 로그인하세요.</p> <p class="text-sm leading-relaxed text-slate-500" data-i18n-html="loginPageDesc">덱을 저장하고 관리하려면<br />계정으로 로그인하세요.</p>
</header> </header>
<form id="login-form" class="rounded-2xl border border-slate-200 bg-white p-7 shadow-[0_4px_24px_rgba(15,23,42,0.06)]"> <form id="login-form" class="rounded-2xl border border-slate-200 bg-white p-7 shadow-lg shadow-slate-900/5">
<div class="mb-5"> <div class="mb-5">
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="email" data-i18n="emailLabel">이메일</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="email" data-i18n="emailLabel">이메일</label>
<input type="email" id="email" required class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" placeholder="you@example.com" autocomplete="email" /> <input type="email" id="email" required class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" placeholder="you@example.com" autocomplete="email" />
</div> </div>
<div> <div>
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="password" data-i18n="passwordLabel">비밀번호</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="password" data-i18n="passwordLabel">비밀번호</label>
<input type="password" id="password" required class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" placeholder="••••••••" autocomplete="current-password" /> <input type="password" id="password" required class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" placeholder="••••••••" autocomplete="current-password" />
</div> </div>
<p id="error-msg" class="mt-4 hidden text-[0.8125rem] text-red-500"></p> <p id="error-msg" class="mt-4 hidden text-sm text-red-500"></p>
<button type="submit" class="mt-6 block w-full cursor-pointer rounded-xl border-0 bg-blue-600 px-4 py-3.5 text-sm font-bold text-white transition hover:bg-blue-700 active:scale-[0.98]" data-i18n="loginButton">로그인</button> <button type="submit" class="mt-6 block w-full cursor-pointer rounded-xl border-0 bg-blue-600 px-4 py-3.5 text-sm font-bold text-white transition hover:bg-blue-700 active:scale-[0.98]" data-i18n="loginButton">로그인</button>
</form> </form>
@@ -41,7 +41,7 @@
</p> </p>
</main> </main>
<footer class="px-6 pb-5 text-center text-[0.625rem] font-medium tracking-[0.02em] text-slate-300" aria-hidden="true">v4.0.3</footer> <footer class="px-6 pb-5 text-center text-[0.625rem] font-medium tracking-wide text-slate-300" aria-hidden="true">v4.0.5</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>

View File

@@ -5,9 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script>
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/app-shell.css" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+KR:wght@400;700;900&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style/auth-header.css" />
<link rel="stylesheet" href="./style/deck-history.css" />
<title>공개 덱 - UNIARE</title> <title>공개 덱 - UNIARE</title>
</head> </head>
<body class="app-page deck-history-page"> <body class="app-page deck-history-page">

View File

@@ -4,42 +4,42 @@
<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" />
<link rel="stylesheet" href="./style/output.css" /> <link rel="stylesheet" href="./style/output.css" />
<link rel="stylesheet" href="./style/brand.css" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+KR:wght@400;700;900&display=swap" rel="stylesheet" />
<title>회원가입 - UNIARE</title> <title>회원가입 - UNIARE</title>
</head> </head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 font-['Pretendard','Inter','Noto_Sans_KR',sans-serif] text-slate-900"> <body class="app-page">
<nav class="sticky top-0 z-50 border-b border-slate-200 bg-white/70 backdrop-blur-xl"> <nav class="app-nav">
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6"> <div class="app-nav-inner">
<a href="/" class="inline-flex shrink-0 items-center leading-none text-slate-800 no-underline"> <a href="/" class="app-brand">
<img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" /> <img src="/images/assets/uniare-h48.png" alt="UNIARE" class="app-brand-logo" height="32" />
</a> </a>
</div> </div>
</nav> </nav>
<main class="mx-auto max-w-[28rem] px-6 py-12 sm:py-16"> <main class="mx-auto w-full max-w-md px-6 pt-12 pb-16 sm:pt-16 sm:pb-20">
<header class="mb-8 text-center"> <header class="mb-8 text-center">
<h1 class="mb-2 text-[1.75rem] font-black tracking-[-0.02em] text-slate-900" data-i18n="signupPageTitle">회원가입</h1> <h1 class="mb-2 text-3xl font-black tracking-tight text-slate-900" data-i18n="signupPageTitle">회원가입</h1>
<p class="m-0 text-sm leading-normal text-slate-500" data-i18n-html="signupPageDesc">UNIARE 계정을 만들고<br />덱을 저장·공유해 보세요.</p> <p class="text-sm leading-relaxed text-slate-500" data-i18n-html="signupPageDesc">UNIARE 계정을 만들고<br />덱을 저장·공유해 보세요.</p>
</header> </header>
<form id="signup-form" class="rounded-2xl border border-slate-200 bg-white p-7 shadow-[0_4px_24px_rgba(15,23,42,0.06)]"> <form id="signup-form" class="rounded-2xl border border-slate-200 bg-white p-7 shadow-lg shadow-slate-900/5">
<div class="mb-5"> <div class="mb-5">
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="email" data-i18n="emailLabel">이메일</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="email" data-i18n="emailLabel">이메일</label>
<input type="email" id="email" required class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" placeholder="you@example.com" autocomplete="email" /> <input type="email" id="email" required class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" placeholder="you@example.com" autocomplete="email" />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="username" data-i18n="usernameLabel">사용자명</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="username" data-i18n="usernameLabel">사용자명</label>
<input type="text" id="username" required minlength="2" class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" placeholder="deckmaster" autocomplete="username" /> <input type="text" id="username" required minlength="2" class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" placeholder="deckmaster" autocomplete="username" />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="password" data-i18n="passwordLabel">비밀번호</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="password" data-i18n="passwordLabel">비밀번호</label>
<input type="password" id="password" required minlength="8" class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" data-i18n-placeholder="signupPasswordPlaceholder" placeholder="8자 이상" autocomplete="new-password" /> <input type="password" id="password" required minlength="8" class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" data-i18n-placeholder="signupPasswordPlaceholder" placeholder="8자 이상" autocomplete="new-password" />
</div> </div>
<div> <div>
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase tracking-wider text-slate-400" for="password-confirm" data-i18n="passwordConfirmLabel">비밀번호 확인</label> <label class="mb-1.5 block text-xs font-bold uppercase tracking-wider text-slate-400" for="password-confirm" data-i18n="passwordConfirmLabel">비밀번호 확인</label>
<input type="password" id="password-confirm" required minlength="8" class="block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15" data-i18n-placeholder="passwordConfirmPlaceholder" placeholder="비밀번호 재입력" autocomplete="new-password" /> <input type="password" id="password-confirm" required minlength="8" class="box-border block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/15" data-i18n-placeholder="passwordConfirmPlaceholder" placeholder="비밀번호 재입력" autocomplete="new-password" />
</div> </div>
<p id="error-msg" class="mt-4 hidden text-[0.8125rem] text-red-500"></p> <p id="error-msg" class="mt-4 hidden text-sm text-red-500"></p>
<button type="submit" class="mt-6 block w-full cursor-pointer rounded-xl border-0 bg-blue-600 px-4 py-3.5 text-sm font-bold text-white transition hover:bg-blue-700 active:scale-[0.98]" data-i18n="signupButton">가입하기</button> <button type="submit" class="mt-6 block w-full cursor-pointer rounded-xl border-0 bg-blue-600 px-4 py-3.5 text-sm font-bold text-white transition hover:bg-blue-700 active:scale-[0.98]" data-i18n="signupButton">가입하기</button>
</form> </form>

1234
style/components.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@import "./brand.css";
@import "./components.css";
/* HTML·JS만 스캔 (images/datas 대용량 폴더 제외) */
@source not "../images/**";
@source not "../datas/**";
@source not "../api/**";
@source "../*.html";
@source "../**/*.html";
@source "../script/**/*.js";

File diff suppressed because one or more lines are too long