Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules
# production
app/dist
functions/dist
packages/shared/dist

# misc
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc && vite build"
},
"dependencies": {
"@til-alarm/shared": "workspace:*",
"axios": "^1.12.2",
"firebase": "^10.4.0",
"react": "^18.2.0",
Expand Down
98 changes: 60 additions & 38 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import FlashCardViewer from './pages/FlashCardViewer';
import Login from './pages/Login';
import Settings from './pages/Settings';
import { getGithubData } from './services/github-service';
import UserDropdown from './widgets/UserDropdown';

initDB(DBConfig);
const dates = [1, 7, 30]; // days ago list
Expand All @@ -22,6 +23,7 @@ const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [authLoading, setAuthLoading] = useState<boolean>(true);
const [currentPage, setCurrentPage] = useState<Page>('flashcard');
const [isScrollAtTop, setIsScrollAtTop] = useState<boolean>(true);

// 인증 상태 감지
useEffect(() => {
Expand All @@ -33,6 +35,21 @@ const App: React.FC = () => {
return () => unsubscribe();
}, []);

// 스크롤 위치 감지
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const isAtTop = scrollTop <= 10; // 10px 이하면 맨 위로 간주
setIsScrollAtTop(isAtTop);
};

// 초기 스크롤 위치 체크
handleScroll();

window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);

// 사용자가 로그인한 경우에만 데이터 로드
useEffect(() => {
if (!user) {
Expand Down Expand Up @@ -114,55 +131,60 @@ const App: React.FC = () => {
top: 0,
left: 0,
right: 0,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
background: 'transparent',
zIndex: 1000,
padding: '12px 20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
opacity: isScrollAtTop ? 1 : 0,
transform: isScrollAtTop ? 'translateY(0)' : 'translateY(-20px)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: isScrollAtTop ? 'auto' : 'none',
}}>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => setCurrentPage('flashcard')}
style={{
padding: '8px 16px',
background: currentPage === 'flashcard' ? '#667eea' : 'transparent',
color: currentPage === 'flashcard' ? 'white' : '#667eea',
border: '2px solid #667eea',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
}}
>
📚 학습하기
</button>
<button
onClick={() => setCurrentPage('settings')}
style={{
padding: '8px 16px',
background: currentPage === 'settings' ? '#667eea' : 'transparent',
color: currentPage === 'settings' ? 'white' : '#667eea',
border: '2px solid #667eea',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
}}
>
⚙️ 설정
</button>
<div>
{currentPage === 'settings' && (
<button
onClick={() => setCurrentPage('flashcard')}
style={{
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.95)',
color: '#333',
border: '1px solid rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
display: 'flex',
alignItems: 'center',
gap: '6px',
backdropFilter: 'blur(10px)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 1)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.95)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
}}
>
← 뒤로가기
</button>
)}
</div>

<div style={{ fontSize: '0.9rem', color: '#666' }}>
👤 {user.displayName || user.email}
</div>
<UserDropdown
user={user}
onNavigateToSettings={() => setCurrentPage('settings')}
/>
</nav>

{/* 페이지 컨텐츠 */}
<div style={{ paddingTop: '60px' }}>
<div>
{currentPage === 'flashcard' && <FlashCardViewer />}
{currentPage === 'settings' && <Settings />}
</div>
Expand Down
11 changes: 10 additions & 1 deletion app/src/DBConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const DBConfig = {
name: 'MyDB',
version: 1,
version: 2,
objectStoresMeta: [
{
store: 'data',
Expand All @@ -9,6 +9,15 @@ export const DBConfig = {
{ name: 'date', keypath: 'date', options: { unique: false }},
{ name: 'data', keypath: 'data', options: { unique: false }}
]
},
{
store: 'repositories',
storeConfig: { keyPath: 'id', autoIncrement: false },
storeSchema: [
{ name: 'id', keypath: 'id', options: { unique: true }},
{ name: 'data', keypath: 'data', options: { unique: false }},
{ name: 'timestamp', keypath: 'timestamp', options: { unique: false }}
]
}
]
};
6 changes: 6 additions & 0 deletions app/src/api/github-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { apiClient } from '../modules/axios';
import type { Repository } from '@til-alarm/shared';

interface Commit {
sha: string;
Expand Down Expand Up @@ -47,3 +48,8 @@ export async function getMarkdown(filename: string): Promise<string> {
const data: MarkdownResponse = response.data;
return data.content;
}

export async function getRepositories(): Promise<Repository[]> {
const response = await apiClient.get('/getRepositories');
return response.data;
}
4 changes: 4 additions & 0 deletions app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ button.primary,
width: 1px;
}

.card-player {
padding-top: 20px;
}

.card-player .slick-list {
z-index: 1;
}
Expand Down
35 changes: 28 additions & 7 deletions app/src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { signInWithPopup, signOut, onAuthStateChanged, User, GithubAuthProvider } from 'firebase/auth';
import { doc, setDoc, deleteDoc } from 'firebase/firestore';
import { doc, setDoc, updateDoc, getDoc } from 'firebase/firestore';
import { auth, githubProvider, db } from '../firebase';
import './Login.css';

Expand Down Expand Up @@ -30,10 +30,22 @@ const Login: React.FC = () => {
// Google의 at-rest encryption으로 자동 암호화됨
const credential = GithubAuthProvider.credentialFromResult(result);
if (credential && credential.accessToken && result.user) {
await setDoc(doc(db, 'users', result.user.uid), {
githubToken: credential.accessToken,
updatedAt: new Date().toISOString(),
});
const userDocRef = doc(db, 'users', result.user.uid);
const userDoc = await getDoc(userDocRef);

if (userDoc.exists()) {
// 기존 문서가 있으면 토큰만 업데이트 (설정 유지)
await updateDoc(userDocRef, {
githubToken: credential.accessToken,
updatedAt: new Date().toISOString(),
});
} else {
// 신규 사용자는 새 문서 생성
await setDoc(userDocRef, {
githubToken: credential.accessToken,
updatedAt: new Date().toISOString(),
});
}
console.log('로그인 성공 및 GitHub 토큰 저장 완료');
}

Expand All @@ -50,10 +62,19 @@ const Login: React.FC = () => {
const handleLogout = async () => {
try {
const currentUser = auth.currentUser;
await signOut(auth);
if (currentUser) {
await deleteDoc(doc(db, 'users', currentUser.uid));
// 문서가 존재하는 경우에만 githubToken 필드만 업데이트
const userDocRef = doc(db, 'users', currentUser.uid);
const userDoc = await getDoc(userDocRef);

if (userDoc.exists()) {
await updateDoc(userDocRef, {
githubToken: null,
updatedAt: new Date().toISOString(),
});
}
}
await signOut(auth);
console.log('로그아웃 성공');
} catch (error) {
console.error('로그아웃 실패:', error);
Expand Down
Loading