From 058f68a61f98622fade50f0a6e102847ad7d6300 Mon Sep 17 00:00:00 2001 From: chucoding Date: Wed, 8 Oct 2025 20:49:25 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat=20:=20[TSK-53]=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20Select=20=EC=BA=90=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/pages/Settings.tsx | 78 ++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index 78da5a0..f33aee2 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -14,6 +14,7 @@ interface RepositorySettings { // 캐시 설정 (컴포넌트 외부로 이동) const CACHE_KEY = 'github_repositories'; +const getBranchCacheKey = (owner: string, repo: string) => `github_branches_${owner}_${repo}`; const Settings: React.FC = () => { const [settings, setSettings] = useState({ @@ -114,14 +115,58 @@ const Settings: React.FC = () => { } }, []); - // 브랜치 목록 불러오기 - const fetchBranches = useCallback(async (owner: string, repo: string) => { + // 브랜치 목록 불러오기 (IndexedDB 캐싱) + const fetchBranches = useCallback(async (owner: string, repo: string, forceRefresh = false) => { try { setLoadingBranches(true); - console.log(`🌿 브랜치 목록 불러오기: ${owner}/${repo}`); + const cacheKey = getBranchCacheKey(owner, repo); + + // 캐시 확인 (수동 새로고침이 아닌 경우) + if (!forceRefresh) { + try { + const cached = await repositoriesDB.getByID(cacheKey); + if (cached) { + const now = Date.now(); + const cacheAge = now - cached.timestamp; + console.log(`✅ 캐시된 브랜치 목록 사용 (IndexedDB) - ${Math.floor(cacheAge / 1000 / 60)}분 전 캐시`); + setBranches(cached.data); + setLoadingBranches(false); + return; + } else { + console.log('📭 브랜치 캐시 없음 - API 호출'); + } + } catch (cacheError) { + console.error('❌ 브랜치 캐시 읽기 실패:', cacheError); + } + } else { + console.log('🔄 수동 새로고침 - API 호출'); + } + + // API 호출 + console.log(`🌿 API에서 브랜치 목록 불러오기: ${owner}/${repo}`); const branchList = await getBranches(owner, repo); setBranches(branchList); console.log(`✅ ${branchList.length}개의 브랜치 발견`); + + // IndexedDB에 캐시 저장 + try { + const cacheData = { + id: cacheKey, + data: branchList, + timestamp: Date.now(), + }; + + // 기존 캐시 확인 + const existing = await repositoriesDB.getByID(cacheKey); + if (existing) { + await repositoriesDB.update(cacheData); + } else { + await repositoriesDB.add(cacheData); + } + console.log('💾 브랜치 목록 캐시 저장 완료 (IndexedDB)'); + } catch (saveError) { + console.error('❌ 브랜치 캐시 저장 실패:', saveError); + } } catch (error) { console.error('❌ 브랜치 불러오기 실패:', error); setMessage({ type: 'error', text: '브랜치 목록을 불러오는데 실패했습니다.' }); @@ -129,7 +174,7 @@ const Settings: React.FC = () => { } finally { setLoadingBranches(false); } - }, []); + }, [repositoriesDB]); // 설정 및 리포지토리 목록 불러오기 useEffect(() => { @@ -446,12 +491,29 @@ const Settings: React.FC = () => {
- +
+ + {settings.repositoryFullName && ( + + )} +

커밋을 가져올 브랜치를 선택하세요 + {branches.length > 0 && ` (총 ${branches.length}개의 브랜치)`}

{loadingBranches ? ( From f44501b87def94bd4ff8cfbe0eb96f073b911f1b Mon Sep 17 00:00:00 2001 From: chucoding Date: Wed, 8 Oct 2025 21:07:38 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat=20:=20[TSK-54]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EC=9D=B8=EC=A6=9D=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20UI=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EB=B6=80=EB=93=9C=EB=9F=BD=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/pages/Settings.css | 107 ++++++++++++++++++++++++------------- app/src/pages/Settings.tsx | 84 +++++++++++++++++++++-------- 2 files changed, 131 insertions(+), 60 deletions(-) diff --git a/app/src/pages/Settings.css b/app/src/pages/Settings.css index d794697..6b092ff 100644 --- a/app/src/pages/Settings.css +++ b/app/src/pages/Settings.css @@ -404,45 +404,44 @@ 100% { transform: rotate(360deg); } } -/* 위험 구역 - 회원탈퇴 */ -.danger-zone { +/* 계정 관리 */ +.account-zone { margin-top: 40px; - padding: 24px; - border: 2px solid #feb2b2; - border-radius: 12px; - background: #fff5f5; + padding: 20px 24px; + border-top: 1px solid #e2e8f0; + text-align: center; } -.danger-zone-title { +.account-zone-title { margin: 0 0 8px 0; - color: #c53030; - font-size: 1.2rem; - font-weight: 700; + color: #718096; + font-size: 1rem; + font-weight: 600; } -.danger-zone-description { +.account-description { margin: 0 0 16px 0; - color: #742a2a; + color: #a0aec0; font-size: 0.9rem; - line-height: 1.5; + line-height: 1.6; } .delete-account-button { - padding: 10px 20px; - background: #fc8181; - color: white; - border: none; - border-radius: 8px; - font-size: 0.95rem; - font-weight: 600; + padding: 8px 16px; + background: transparent; + color: #a0aec0; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; cursor: pointer; transition: all 0.2s; } .delete-account-button:hover { - background: #f56565; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4); + color: #718096; + border-color: #cbd5e0; + background: #f7fafc; } /* 모달 스타일 */ @@ -495,31 +494,51 @@ .modal-title { margin: 0 0 16px 0; - color: #c53030; + color: #4a5568; font-size: 1.5rem; font-weight: 700; } .modal-description { - margin: 0 0 16px 0; + margin: 0 0 20px 0; color: #4a5568; font-size: 1rem; line-height: 1.6; } -.modal-warning-list { +.modal-info-box { margin: 0 0 24px 0; + padding: 16px; + background: #f7fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; +} + +.info-box-title { + margin: 0 0 12px 0; + color: #4a5568; + font-size: 0.95rem; + font-weight: 600; +} + +.modal-info-list { + margin: 0; padding-left: 20px; - color: #742a2a; - background: #fff5f5; - border-left: 3px solid #fc8181; - padding: 12px 12px 12px 32px; - border-radius: 4px; + color: #718096; } -.modal-warning-list li { +.modal-info-list li { margin: 8px 0; - line-height: 1.5; + line-height: 1.6; + font-size: 0.9rem; +} + +.modal-info-list li.info-reauth { + color: #667eea; + font-weight: 500; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed #e2e8f0; } .confirm-input { @@ -534,8 +553,8 @@ .confirm-input:focus { outline: none; - border-color: #fc8181; - box-shadow: 0 0 0 3px rgba(252, 129, 129, 0.1); + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .confirm-input:disabled { @@ -572,14 +591,14 @@ } .modal-button.danger { - background: #fc8181; + background: #a0aec0; color: white; } .modal-button.danger:hover:not(:disabled) { - background: #f56565; + background: #718096; transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4); + box-shadow: 0 4px 12px rgba(113, 128, 150, 0.3); } .modal-button:disabled { @@ -597,6 +616,18 @@ font-size: 1.5rem; } + .account-zone { + padding: 16px 20px; + } + + .account-zone-title { + font-size: 0.95rem; + } + + .account-description { + font-size: 0.85rem; + } + .modal-content { padding: 24px; } diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index f33aee2..5f52741 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore'; +import { reauthenticateWithPopup } from 'firebase/auth'; import { useIndexedDB } from 'react-indexed-db-hook'; -import { auth, db } from '../firebase'; +import { auth, db, githubProvider } from '../firebase'; import { getRepositories, getBranches, Branch } from '../api/github-api'; import { Repository } from '@til-alarm/shared'; import './Settings.css'; @@ -366,16 +367,51 @@ const Settings: React.FC = () => { } catch (error: any) { console.error('회원탈퇴 실패:', error); - // 재인증이 필요한 경우 - if (error.code === 'auth/requires-recent-login') { - setMessage({ - type: 'error', - text: '보안을 위해 다시 로그인한 후 탈퇴를 진행해주세요.' - }); + // 재인증이 필요한 경우 (다양한 오류 코드 처리) + const needsReauth = + error.code === 'auth/requires-recent-login' || + error.message?.includes('CREDENTIAL_TOO_OLD') || + error.message?.includes('LOGIN_AGAIN'); + + if (needsReauth) { + try { + // 자동으로 재인증 시도 + console.log('🔄 재인증이 필요합니다. GitHub 로그인 팝업을 엽니다...'); + setMessage({ + type: 'error', + text: '보안을 위해 재인증이 필요합니다. 팝업에서 GitHub 로그인을 진행해주세요.' + }); + + await reauthenticateWithPopup(user, githubProvider); + console.log('✅ 재인증 완료'); + + // 재인증 후 다시 계정 삭제 시도 + setMessage({ type: 'error', text: '재인증되었습니다. 다시 탈퇴를 시도합니다...' }); + + await deleteDoc(doc(db, 'users', user.uid)); + await user.delete(); + + console.log('회원탈퇴 완료'); + } catch (reauthError: any) { + console.error('재인증 실패:', reauthError); + + if (reauthError.code === 'auth/popup-closed-by-user') { + setMessage({ + type: 'error', + text: '재인증이 취소되었습니다. 탈퇴를 계속하려면 다시 시도해주세요.' + }); + } else { + setMessage({ + type: 'error', + text: '재인증에 실패했습니다. 잠시 후 다시 시도해주세요.' + }); + } + setDeleting(false); + } } else { setMessage({ type: 'error', text: '회원탈퇴에 실패했습니다.' }); + setDeleting(false); } - setDeleting(false); } }; @@ -595,35 +631,39 @@ const Settings: React.FC = () => {

- {/* 위험 구역 - 회원탈퇴 */} -
-

⚠️ 위험 구역

-

- 회원탈퇴 시 모든 데이터가 영구적으로 삭제되며 복구할 수 없습니다. + {/* 계정 관리 */} +

+

👤 계정 관리

+

+ 서비스 이용을 중단하고 싶으신가요?

- {/* 회원탈퇴 확인 다이얼로그 */} + {/* 서비스 탈퇴 확인 다이얼로그 */} {showDeleteDialog && (
!deleting && setShowDeleteDialog(false)}>
e.stopPropagation()}> -

⚠️ 회원탈퇴

+

👋 서비스 탈퇴

- 정말로 탈퇴하시겠습니까? 이 작업은 되돌릴 수 없습니다. + 정말 탈퇴하시겠어요? 걱정하지 마세요, 언제든 다시 돌아올 수 있습니다.

-
    -
  • 모든 설정 데이터가 삭제됩니다
  • -
  • 저장된 GitHub 토큰이 삭제됩니다
  • -
  • 계정이 완전히 삭제됩니다
  • -
+
+

✨ 탈퇴 시 안내사항

+
    +
  • 저장된 GitHub 토큰 및 리포지토리 설정이 삭제됩니다
  • +
  • 로컬 브라우저의 플래시카드 데이터는 유지됩니다
  • +
  • 언제든 재가입하여 동일하게 서비스를 이용할 수 있습니다
  • +
  • 💡 보안을 위해 GitHub 재인증 팝업이 표시될 수 있습니다
  • +
+