diff --git a/.cursorrules b/.cursorrules index bfd082f..1ad6b1b 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1 +1 @@ -모든 작업 시 프로젝트 루트의 .ai-rules.md 지침을 엄격히 준수하고, 작업 종료 시마다 docs/ 폴더 내 문서를 업데이트할 것. \ No newline at end of file +모든 작업 시 프로젝트 루트의 /ai-rules.md 지침을 엄격히 준수하고, 작업 종료 시마다 docs/ 폴더 내 문서를 업데이트할 것. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 55dcbe7..31614d4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ backend/uploads/games/ backend/uploads/custom/ .DS_Store +.env.production +.vscode/ diff --git a/backend/src/db.js b/backend/src/db.js index dc46c17..3314ba3 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -78,6 +78,7 @@ function mapTierListRow(row) { thumbnailSrc: row.thumbnail_src || '', description: row.description || '', isPublic: !!row.is_public, + showCharacterNames: !!row.show_character_names, groups: parseJson(row.groups_json, []), pool: parseJson(row.pool_json, []), createdAt: Number(row.created_at), @@ -226,6 +227,7 @@ async function ensureSchema() { thumbnail_src VARCHAR(255) NOT NULL DEFAULT '', description TEXT NOT NULL, is_public TINYINT(1) NOT NULL DEFAULT 0, + show_character_names TINYINT(1) NOT NULL DEFAULT 0, groups_json LONGTEXT NOT NULL, pool_json LONGTEXT NOT NULL, created_at BIGINT NOT NULL, @@ -277,6 +279,10 @@ async function ensureSchema() { if (!tierListThumbnailColumns.length) { await query("ALTER TABLE tierlists ADD COLUMN thumbnail_src VARCHAR(255) NOT NULL DEFAULT '' AFTER title") } + const tierListShowNamesColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'show_character_names'") + if (!tierListShowNamesColumns.length) { + await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public") + } await query( ` @@ -396,13 +402,23 @@ async function listUsers() { return rows.map(mapUserRow) } -async function adminUpdateUser({ id, email, nickname, isAdmin }) { - await query('UPDATE users SET email = ?, nickname = ?, is_admin = ? WHERE id = ?', [ - email, - nickname || '', - isAdmin ? 1 : 0, - id, - ]) +async function adminUpdateUser({ id, email, nickname, isAdmin, avatarSrc }) { + if (typeof avatarSrc === 'string') { + await query('UPDATE users SET email = ?, nickname = ?, is_admin = ?, avatar_src = ? WHERE id = ?', [ + email, + nickname || '', + isAdmin ? 1 : 0, + avatarSrc, + id, + ]) + } else { + await query('UPDATE users SET email = ?, nickname = ?, is_admin = ? WHERE id = ?', [ + email, + nickname || '', + isAdmin ? 1 : 0, + id, + ]) + } return findUserById(id) } @@ -833,6 +849,7 @@ async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited t.thumbnail_src, t.description, t.is_public, + t.show_character_names, t.groups_json, t.pool_json, t.created_at, @@ -962,6 +979,7 @@ async function listAdminTierLists({ queryText = '', page = 1, limit = 50, curren t.thumbnail_src, t.description, t.is_public, + t.show_character_names, t.groups_json, t.pool_json, t.created_at, @@ -1017,6 +1035,7 @@ async function findTierListById(id, currentUserId = '') { t.thumbnail_src, t.description, t.is_public, + t.show_character_names, t.groups_json, t.pool_json, t.created_at, @@ -1214,7 +1233,7 @@ async function deleteCustomItems(ids) { await query(`DELETE FROM custom_items WHERE id IN (${placeholders})`, ids) } -async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', description, isPublic, groups, pool }) { +async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', description, isPublic, showCharacterNames = false, groups, pool }) { const existing = id ? await findTierListById(id, authorId) : null await syncOwnedCustomItemLabels({ ownerId: authorId, items: pool }) const nextThumbnailSrc = (thumbnailSrc || '').trim() || getAutoThumbnailSrc(groups, pool) @@ -1223,10 +1242,10 @@ async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', de await query( ` UPDATE tierlists - SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, groups_json = ?, pool_json = ?, updated_at = ? + SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, groups_json = ?, pool_json = ?, updated_at = ? WHERE id = ? `, - [title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), now(), existing.id] + [title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, serializeJson(groups), serializeJson(pool), now(), existing.id] ) return findTierListById(existing.id, authorId) } @@ -1235,11 +1254,11 @@ async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', de await query( ` INSERT INTO tierlists ( - id, author_id, game_id, title, thumbnail_src, description, is_public, groups_json, pool_json, created_at, updated_at + id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, groups_json, pool_json, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, - [id, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, serializeJson(groups), serializeJson(pool), createdAt, createdAt] + [id, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, serializeJson(groups), serializeJson(pool), createdAt, createdAt] ) return findTierListById(id, authorId) } diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 37f5197..f9e4620 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -61,6 +61,14 @@ const upload = multer({ limits: { fileSize: 6 * 1024 * 1024 }, }) +const avatarUpload = multer({ + storage: multer.diskStorage({ + destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'avatars')), + filename: (req, file, cb) => cb(null, buildUploadFilename(file)), + }), + limits: { fileSize: 3 * 1024 * 1024 }, +}) + router.post('/games', requireAdmin, async (req, res) => { const schema = z.object({ id: z.string().min(1), name: z.string().min(1).max(60) }) const parsed = schema.safeParse(req.body) @@ -494,6 +502,29 @@ router.patch('/users/:userId', requireAdmin, async (req, res) => { } }) +router.post('/users/:userId/avatar', requireAdmin, avatarUpload.single('avatar'), async (req, res) => { + const schema = z.object({ + removeAvatar: z.union([z.literal('1'), z.undefined()]).optional(), + }) + const parsed = schema.safeParse(req.body) + if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) + + const user = await findUserById(req.params.userId) + if (!user) return res.status(404).json({ error: 'not_found' }) + + const shouldRemoveAvatar = parsed.data.removeAvatar === '1' + const nextAvatarSrc = shouldRemoveAvatar ? '' : req.file ? `/uploads/avatars/${req.file.filename}` : user.avatarSrc || '' + const updated = await adminUpdateUser({ + id: user.id, + email: user.email, + nickname: user.nickname || '', + isAdmin: !!user.isAdmin, + avatarSrc: nextAvatarSrc, + }) + + res.json({ user: updated }) +}) + router.delete('/users/:userId', requireAdmin, async (req, res) => { if (req.params.userId === req.session.userId) { return res.status(400).json({ error: 'cannot_delete_self' }) diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index ca769e6..b375919 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -28,6 +28,7 @@ const signupSchema = z.object({ const profileSchema = z.object({ nickname: z.string().trim().min(1).max(40), + removeAvatar: z.union([z.string(), z.undefined()]).optional(), }) router.post('/signup', async (req, res) => { @@ -108,7 +109,12 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) = const user = await findUserById(req.session.userId) if (!user) return res.status(404).json({ error: 'not_found' }) - const nextAvatarSrc = req.file ? `/uploads/avatars/${req.file.filename}` : user.avatarSrc || '' + const shouldRemoveAvatar = parsed.data.removeAvatar === '1' + const nextAvatarSrc = shouldRemoveAvatar + ? '' + : req.file + ? `/uploads/avatars/${req.file.filename}` + : user.avatarSrc || '' const updated = await updateUserProfile({ id: user.id, nickname: parsed.data.nickname, diff --git a/backend/src/routes/tierlists.js b/backend/src/routes/tierlists.js index b06afe4..d555e6a 100644 --- a/backend/src/routes/tierlists.js +++ b/backend/src/routes/tierlists.js @@ -83,6 +83,7 @@ const tierListUpsertSchema = z.object({ thumbnailSrc: z.string().max(255).optional().default(''), description: z.string().max(1000).optional().default(''), isPublic: z.boolean().default(false), + showCharacterNames: z.boolean().optional().default(false), groups: z.array( z.object({ id: z.string().min(1), @@ -244,6 +245,7 @@ router.post('/', requireAuth, async (req, res) => { thumbnailSrc: payload.thumbnailSrc || '', description: payload.description || '', isPublic: !!payload.isPublic, + showCharacterNames: !!payload.showCharacterNames, groups: payload.groups, pool: normalizedPool, }) @@ -258,6 +260,7 @@ router.post('/', requireAuth, async (req, res) => { thumbnailSrc: payload.thumbnailSrc || '', description: payload.description || '', isPublic: !!payload.isPublic, + showCharacterNames: !!payload.showCharacterNames, groups: payload.groups, pool: normalizedPool, }) diff --git a/docs/update.md b/docs/update.md index e8089c4..a5f7659 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,80 @@ # 업데이트 로그 +## 2026-03-31 v1.2.41 +- **에디터 하단 아이템 풀 카드형 전환**: 브라우저 폭이 `980px` 이하로 줄어 아이템 풀이 티어표 아래로 내려오면, 세로 리스트 대신 `이미지 위 / 이름 아래` 카드형 그리드로 전환되도록 조정 +- **소형 폭 열 수 최적화**: 약 `800px` 전후에서는 6열 그리드가 유지되고, 더 작은 폭에서는 4열/3열로 자연스럽게 줄어들며 긴 이름은 가운데 정렬된 말줄임 형태로 보이도록 정리 + +## 2026-03-31 v1.2.40 +- **목록 카드 메타 정리**: `내 티어표`, `즐겨찾기`, `검색 결과`, `게임 목록` 카드의 작성자 썸네일을 원형으로 통일하고, 메타 행 간격과 날짜 크기(`10px`)를 조정했으며 날짜 정렬을 위해 `boardCard__metaRow`를 `align-items: flex-end`로 보정 +- **게임 허브 CTA 좌측 하단 이동**: 게임 목록 화면의 `새 티어표 만들기` 버튼을 오른쪽 사이드에서 제거하고, 왼쪽 하단 액션 영역으로 옮겨 관리자 메뉴와 같은 버튼 문법으로 정리 +- **필수 우측 패널 자동 열기**: 티어 메이커/관리자처럼 오른쪽 사이드 사용이 필요한 페이지는 패널이 닫혀 있더라도 진입 시 자동으로 열리게 해, 도구 접근성과 이후 광고 노출 흐름을 함께 보정 + +## 2026-03-31 v1.2.39 +- **홈 하단 액션 재배치**: 홈 오른쪽 사이드의 `커스텀 티어표 만들기` CTA를 제거하고, 로그인/관리자 메뉴가 있던 왼쪽 하단 액션 영역으로 옮겨 같은 버튼 문법으로 정리 +- **우측 중복 액션 축소**: 일반 화면에서 중복되던 `로그인 하러가기` 계열 우측 CTA는 제거하고, 오른쪽 레일은 광고/도구 용도로만 유지하도록 단순화 +- **회원가입 확인 입력 추가**: 로그인 화면 회원가입 모드에 비밀번호 확인 필드를 추가하고, 버튼 문구를 `로그인 / 가입하기 / 취소` 같은 한글 흐름으로 정리 + +## 2026-03-31 v1.2.38 +- **로그인 화면 문법 통일**: 로그인/회원가입 화면을 기존 카드형에서 Settings와 같은 단일 컬럼 계정 설정 스타일로 재구성해 두 화면의 톤을 통일 +- **일반 우측 레일 광고 슬롯 전환**: 에디터/관리자처럼 실제 도구가 필요한 화면을 제외하면 오른쪽 레일은 중복 액션 버튼 대신 AdSense 수직형 반응형 슬롯을 기본으로 표시하도록 정리 + +## 2026-03-31 v1.2.37 +- **대표 썸네일 드래그 업로드 추가**: 우측 대표 썸네일 영역도 드래그앤드롭으로 이미지를 받을 수 있게 하고, 여러 파일을 드롭하면 첫 번째만 사용된다는 안내 토스트를 표시하도록 수정 +- **삭제/업데이트 요청 액션 경량화**: 우측 하단의 삭제와 템플릿 업데이트 요청을 무거운 정식 버튼 대신 작은 보조 링크형 액션으로 정리해 실제 주 행동과 시각적으로 분리 +- **확인 모달 보강**: 템플릿 업데이트 요청과 티어표 삭제는 이제 브라우저 기본 얼럿 대신 전용 확인 모달을 통해 안내 후 진행되도록 변경 + +## 2026-03-31 v1.2.36 +- **축소 검색 모달 재정의**: 좌측 레일 축소 상태에서는 검색 아이콘 클릭 시 카드형 다이얼로그 대신, 화면 중앙보다 약간 위에 뜨는 단일 검색 바와 은은한 암전 오버레이로 재구성하고 `ESC`/바깥 클릭으로 닫을 수 있게 보정 +- **드롭 영역 위치 재조정**: 커스텀 이미지 추가 영역을 전체 `editorCanvas` 하단이 아니라 왼쪽 티어표 컬럼 내부의 보드 바로 아래로 옮겨, 오른쪽 아이템 목록 길이와 무관하게 가까운 위치에서 추가할 수 있도록 수정 + +## 2026-03-31 v1.2.35 +- **축소 좌측 검색 동작 수정**: 접힌 상태의 검색 아이콘은 이제 즉시 모달을 열고, 일반 상태에서만 폼 제출이 되도록 분기해 실제 팝업이 보이도록 수정 +- **우측 레일 높이 제한 해제**: 공통 `max-height: calc(100vh - 56px)` 규칙은 왼쪽 레일에만 남기고, 오버레이 상태를 포함한 오른쪽 레일은 별도 높이 제한 없이 내용 전체가 자연스럽게 흐르도록 조정 +- **커스텀 업로드 영역 하단 이동**: 커스텀 이미지 드래그 영역과 파일 선택 버튼을 아이템 풀 아래가 아니라 티어표 섹션 하단으로 옮겨, 긴 아이템 목록과 충돌하지 않도록 정리 + +## 2026-03-31 v1.2.34 +- **축소 좌측 검색 팝업 추가**: 왼쪽 레일이 접힌 상태에서 검색 아이콘을 누르면 즉시 검색 입력이 가능한 모달 팝업이 뜨도록 바꾸고, 셸 톤에 맞는 블러/글래스 스타일로 정리 +- **에디터 빈 우측 섹션 제거**: 티어 메이커 우측 패널의 네 번째 빈 박스는 `즐겨찾기` 버튼 래퍼였고, 조건이 맞지 않을 때 박스만 남지 않도록 섹션 자체를 조건부 렌더링으로 수정 +- **우측 레일 스크롤 구조 완화**: 오른쪽 패널은 이제 본문 전체가 자연스럽게 세로 스크롤되고, 로컬 패널 루트의 불필요한 최소 높이를 제거해 내용이 늘어나도 잘려 보이는 느낌을 줄임 + +## 2026-03-31 v1.2.33 +- **우측 패널 토글 위치 보정**: 소형 해상도에서도 오른쪽 패널 열기 버튼이 본문 아래로 내려가지 않도록 워크스페이스 헤더 최상단 액션 영역으로 이동 +- **모바일 좌측 레일 단순화**: 모바일에서는 좌측 레일 접기 버튼을 숨기고, 축소 상태가 남아 있더라도 텍스트와 사용자 메타를 다시 보여주도록 보정해 아이콘만 덩그러니 남는 상황을 제거 +- **모바일 축소 상태 자동 해제**: 화면 폭이 모바일 범위로 들어오면 좌측 레일 축소 상태를 자동으로 풀어, 작은 화면에서는 항상 읽을 수 있는 메뉴 형태를 유지 + +## 2026-03-31 v1.2.32 +- **왼쪽 레일 축소 상태 재정의**: 축소 시 사용자 정보는 아바타만 남기고, 메뉴는 아이콘만 보이도록 숨김 처리해 중앙 정렬이 자연스럽게 되도록 정리 +- **축소 레일 검색/관리자 처리 보정**: 접힌 상태에서는 검색 입력을 숨기고 아이콘 중심으로 단순화했으며, 아이콘이 없는 하단 관리자 버튼은 축소 모드에서 숨김 유지 +- **우측 패널 소형 해상도 오버레이 전환**: `1200px` 이하에서는 오른쪽 패널을 고정 컬럼 대신 오버레이 패널로 띄우고, 본문 상단 쪽에 다시 열기 버튼을 배치해 패널을 잃어버리지 않도록 수정 + +## 2026-03-31 v1.2.31 +- **사이드 아이콘 에셋 정리**: 좌측 `Favorites` 메뉴도 제공된 `favorite.svg`를 사용하도록 바꿔, 다른 사이드 아이콘 및 패널 토글 SVG와 같은 자산 흐름으로 통일 +- **프로필 아바타 삭제 UX 개선**: `Settings`에서 텍스트형 `이미지 제거` 버튼을 없애고, 아바타 썸네일 우측 상단의 고정 아이콘 버튼으로 삭제하도록 변경해 레이아웃 흔들림을 제거 +- **셸 코드 정리**: `App.vue`의 비어 있던 감시 코드를 제거해 현재 사용자 수정 위에 불필요한 잔여 로직이 남지 않도록 정리 + +## 2026-03-31 v1.2.30 +- **왼쪽 즐겨찾기 섹션 제거**: 좌측 레일의 `즐겨찾기 보기` 섹션을 삭제하고, 상단 내비의 즐겨찾기 메뉴만 진입점으로 유지 +- **Settings 화면 리디자인**: 프로필 설정 화면을 카드형 대신 단일 컬럼의 미니멀한 계정 설정 레이아웃으로 재구성 +- **아바타 클릭 업로드/삭제 UX**: 파일 input 노출을 없애고, 아바타를 클릭해 이미지 업로드와 제거를 처리하는 최근 앱 스타일 인터랙션으로 변경 +- **백엔드 아바타 제거 지원**: 프로필 저장 API가 아바타 삭제 요청도 함께 처리하도록 확장 + +## 2026-03-30 v1.2.29 +- **왼쪽 즐겨찾기 목록 제거**: 좌측 레일의 최근 즐겨찾기 목록과 관련 데이터 로딩 로직을 제거하고, `즐겨찾기 보기` 링크만 유지하도록 단순화 +- **불필요한 즐겨찾기 API 호출 제거**: 사이드바 표시만을 위해 수행되던 즐겨찾기 목록 요청을 없애 초기 렌더 비용을 줄임 + +## 2026-03-30 v1.2.28 +- **사이드 스크롤 영역 재분리**: 좌우 레일에서 스크롤되는 콘텐츠 영역과 하단 액션 영역을 분리해, 상단 헤더 높이와 무관하게 버튼이 항상 최초 화면 안에 보이도록 수정 +- **레일 바디 overflow 구조 수정**: 레일 전체가 아니라 내부 콘텐츠만 스크롤되게 바꿔, 하단 버튼이 다시 스크롤 아래로 밀리는 문제를 해소 + +## 2026-03-30 v1.2.27 +- **사이드 하단 버튼 즉시 노출**: 좌우 하단 액션 버튼을 별도 푸터가 아니라 각 레일의 스크롤 바디 안으로 옮기고, 남는 공간을 밀어내는 spacer 구조로 바꿔 스크롤 없이도 처음부터 하단에 보이도록 수정 +- **56px 하단 여백 제거**: 기존 고정 푸터 높이와 추가 하단 패딩을 제거해, 하단 액션이 자연스럽게 레일 마지막 줄에 붙도록 정리 + +## 2026-03-30 v1.2.26 +- **페이지 헤더 정렬 통일**: `Games`, `내 리스트`, `즐겨찾기`, `Settings` 화면이 모두 같은 전역 헤더 문법과 높이를 사용하도록 정리해, 페이지 이동 시 상단 블록 위치가 미묘하게 흔들리던 문제를 완화 +- **헤더 내부 패딩 제거**: 워크스페이스 본문에 이미 좌우 여백이 있는 점을 반영해, 각 페이지 헤더 내부의 작은 추가 패딩을 제거하고 동일한 배치 규칙으로 맞춤 +- **Settings 헤더 문법 통일**: 프로필 화면도 다른 목록 화면과 동일한 eyebrow/title/description 구조를 갖도록 보강해 전체 화면 톤을 통일 + ## 2026-03-30 v1.2.25 - **홈 게임 카드 썸네일 복구**: 메인 게임 선택 카드는 상단 메인 썸네일을 다시 표시하고, 하단 ID 라인 옆의 작은 보조 표시만 제거하도록 보정 - **사이드 하단 버튼 고정 가시성 보정**: 좌우 하단 액션 버튼이 스크롤을 해야 보이지 않던 문제를 수정하고, 버튼 자체는 항상 보이면서 아래쪽 여백만 확보되도록 조정 @@ -377,3 +452,42 @@ ## 2026-03-19 v0.1.17 - **내 티어표 삭제 추가**: `내 티어표` 목록에서 작성자가 자신의 티어표를 직접 삭제할 수 있도록 삭제 버튼과 API를 추가 - **미사용 커스텀 이미지 관리 추가**: 관리자 아이템 탭에서 커스텀 이미지의 사용 횟수를 표시하고, 미사용 항목만 따로 필터링해 개별/일괄 삭제할 수 있도록 보강 + +## 2026-03-31 v0.1.18 +- **에디터 보드 폭 기준 정리**: 티어표 보드 영역을 저장 이미지 기준에 맞춰 최대 약 `960px` 폭으로 묶고, 넓은 화면에서는 아이템 풀이 남는 공간을 더 가져가도록 조정 +- **아이템 풀 카드형 통일**: 넓은 화면에서도 우측 아이템 목록을 카드형 그리드로 바꿔 한 번에 더 많은 아이템을 보고 드래그할 수 있도록 개선 + +## 2026-03-31 v0.1.19 +- **이름 표시 옵션 추가**: 티어 에디터 우측 옵션에 `캐릭터 이름 표시` 토글을 추가하고, 보드 안에서는 이미지 하단 오버레이 라벨로 표시되도록 개선 +- **저장/불러오기 연동**: 이름 표시 옵션이 저장된 티어표와 다운로드 이미지에도 그대로 반영되도록 프런트/백엔드 저장 구조를 확장 + +## 2026-03-31 v0.1.20 +- **관리자 탭 구조 재정리**: `목록 관리`와 `게임 관리`를 분리하고, 게임 생성/선택 흐름을 우측 사이드가 아닌 본문 전용 작업 화면으로 이동 +- **회원/액션 레이아웃 정리**: 회원 카드의 작성 수/최근 활동을 텍스트형 정보로 단순화하고, 관리 버튼의 줄바꿈이 어색하지 않도록 액션 그리드를 보정 + +## 2026-03-31 v0.1.21 +- **회원 카드 액션 재구성**: 비밀번호 초기화와 회원 삭제를 아이콘 액션으로 축소하고, `회원정보 저장` 버튼은 실제 변경이 있을 때만 활성화되도록 조정 +- **관리자 아바타 편집 지원**: 관리자도 회원 아바타를 클릭해 변경하거나 삭제할 수 있도록 전용 업로드 API와 카드 UI를 추가 + +## 2026-03-31 v0.1.22 +- **회원 액션 플로우 수정**: 회원 카드의 불필요한 안내 문구와 상단 삭제 아이콘을 제거하고, 비밀번호 초기화/회원 삭제를 각각 전용 확인 모달로 재구성 +- **저장 버튼 활성 조건 정리**: 회원정보 저장은 필드가 실제로 바뀐 경우에만 활성화되고, 비밀번호 초기화와 삭제 아이콘은 즉시 사용할 수 있도록 조정 + +## v0.1.23 +- 관리자 회원 관리에서 비밀번호 초기화와 삭제를 실제 모달 플로우로 연결하고, 저장 버튼은 회원 정보 변경 시에만 활성화되도록 정리함. +- 상단 휴지통 아이콘과 불필요 문구를 제거하고, 관리자도 회원 썸네일을 카드 안에서 바로 수정/삭제할 수 있게 보완함. + +## v0.1.24 +- 관리자 회원 관리 배지를 Settings 화면의 Administrator 스타일로 통일하고, 카드 우측 상단에 걸치는 형태로 재배치함. +- 관리자 권한 체크박스를 제거하고 작은 텍스트 액션과 확인 모달을 거쳐 draft 상태만 바꾸는 흐름으로 정리함. + +## v0.1.25 +- 관리자 회원 저장 후 통계 정보가 흔들리던 문제를 줄이기 위해 저장/아바타 변경 뒤 회원 목록을 다시 동기화하도록 보정함. +- 회원 아바타 액션을 hover 기반으로 재배치해 평소에는 숨기고, 마우스 오버 시에만 수정 오버레이와 삭제 버튼이 나타나도록 조정함. + +## v0.1.26 +- 관리자 회원 아바타 삭제 버튼 조건을 명확히 하고 hover 표시를 visibility까지 포함해 보정해 다른 사용자 카드에서도 안정적으로 노출되도록 조정함. +- 삭제 배지 아이콘을 흰색으로 보정하고 어두운 배경 위에서 더 잘 보이도록 스타일을 다듬음. + +## v0.1.27 +- 운영 비밀값이 들어 있는 `.env.production`과 로컬 에디터 설정 `.vscode/`를 `.gitignore`에 추가해 푸시 대상에서 제외함. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 379e885..6ccb345 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,17 +1,17 @@