Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Today I Learned Alarm
# RecallBuddy

매일 학습한 내용을 정리하고 알림을 받는 앱입니다.

Expand Down
2 changes: 1 addition & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Today I Learned Alarm</title>
<title>RecallBuddy</title>
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added app/public/character.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/public/onboarding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 101 additions & 55 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useEffect, useState } from 'react';
import { initDB } from "react-indexed-db-hook";
import { onAuthStateChanged, User } from 'firebase/auth';
import { doc, getDoc } from 'firebase/firestore';

import { DBConfig } from './DBConfig';
import { auth } from './firebase';
import { auth, db } from './firebase';
import FlashCardViewer from './pages/FlashCardViewer';
import Login from './pages/Login';
import Settings from './pages/Settings';
import NoDataView from './pages/NoDataView';
import UserDropdown from './widgets/UserDropdown';
import Onboarding from './pages/Onboarding';
import Card from './components/Card';
import { useTodayFlashcards } from './hooks/useTodayFlashcards';
import { useNavigationStore } from './stores/navigationStore';

Expand All @@ -18,6 +20,8 @@ const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [authLoading, setAuthLoading] = useState<boolean>(true);
const [isScrollAtTop, setIsScrollAtTop] = useState<boolean>(true);
const [needsOnboarding, setNeedsOnboarding] = useState<boolean>(false);
const [onboardingChecked, setOnboardingChecked] = useState<boolean>(false);
const { currentPage, navigateToSettings, navigateToFlashcard } = useNavigationStore();

// 오늘의 플래시카드 데이터 로드
Expand All @@ -33,6 +37,41 @@ const App: React.FC = () => {
return () => unsubscribe();
}, []);

// 온보딩 필요 여부 확인
useEffect(() => {
const checkOnboarding = async () => {
if (!user) {
setOnboardingChecked(true);
return;
}

try {
const userDocRef = doc(db, 'users', user.uid);
const userDoc = await getDoc(userDocRef);

// 문서가 없거나, 온보딩 완료 표시가 없고 리포지토리 설정도 없으면 온보딩 필요
if (!userDoc.exists()) {
setNeedsOnboarding(true);
} else {
const data = userDoc.data();
// onboardingCompleted가 true이거나 repositoryFullName이 있으면 온보딩 불필요
if (data?.onboardingCompleted || data?.repositoryFullName) {
setNeedsOnboarding(false);
} else {
setNeedsOnboarding(true);
}
}
} catch (error) {
console.error('온보딩 확인 실패:', error);
setNeedsOnboarding(false);
} finally {
setOnboardingChecked(true);
}
};

checkOnboarding();
}, [user]);

// 스크롤 위치 감지
useEffect(() => {
const handleScroll = () => {
Expand All @@ -48,18 +87,29 @@ const App: React.FC = () => {
return () => window.removeEventListener('scroll', handleScroll);
}, []);

// 인증 로딩 중
if (authLoading) {
// 로딩 중 (인증, 온보딩 확인, 데이터 로딩)
if (authLoading || !onboardingChecked || loading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '1.2rem'
}}>
로딩 중...
</div>
<Card>
<div style={{
width: '50px',
height: '50px',
border: '3px solid rgba(255, 255, 255, 0.3)',
borderTop: '3px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
margin: '0 auto 20px'
}}></div>
<h2 style={{ marginBottom: '10px', fontSize: '1.25rem' }}>📚 플래시카드 준비 중</h2>
<p style={{ fontSize: '1rem' }}>GitHub에서 최근 커밋을 분석하고 있습니다...</p>
<p style={{ marginTop: '10px', fontSize: '0.9rem', opacity: '0.8' }}>⏱️ 데이터 양에 따라 시간이 조금 걸릴 수 있습니다</p>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</Card>
);
}

Expand All @@ -68,45 +118,15 @@ const App: React.FC = () => {
return <Login />;
}

// 데이터 로딩 중
if (loading) {
// 온보딩이 필요한 경우
if (needsOnboarding) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '1.2rem',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white'
}}>
<div style={{
textAlign: 'center',
background: 'rgba(255, 255, 255, 0.1)',
padding: '40px',
borderRadius: '20px',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.2)'
}}>
<div style={{
width: '50px',
height: '50px',
border: '3px solid rgba(255, 255, 255, 0.3)',
borderTop: '3px solid white',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
margin: '0 auto 20px'
}}></div>
<h2 style={{ marginBottom: '10px' }}>📚 플래시카드 준비 중</h2>
<p>GitHub에서 최근 커밋을 분석하고 있습니다...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
<Onboarding
onComplete={() => {
// 온보딩 완료 후 페이지 새로고침으로 깔끔하게 시작
window.location.reload();
}}
/>
);
}

Expand Down Expand Up @@ -170,10 +190,36 @@ const App: React.FC = () => {
)}
</div>

<UserDropdown
user={user}
onNavigateToSettings={navigateToSettings}
/>
{currentPage === 'flashcard' && (
<button
onClick={navigateToSettings}
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',
fontSize: '1.2rem',
transition: 'all 0.2s',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
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)';
}}
title="설정"
>
⚙️
</button>
)}
</nav>

{/* 페이지 컨텐츠 */}
Expand Down
23 changes: 23 additions & 0 deletions app/src/components/Card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.card-container {
display: flex;
flex-direction: column;
height: 100vh;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
padding: 16px;
}

.card-content {
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 40px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
}

20 changes: 20 additions & 0 deletions app/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import './Card.css';

interface CardProps {
children: React.ReactNode;
className?: string;
}

const Card: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className="card-container">
<div className={`card-content ${className}`}>
{children}
</div>
</div>
);
};

export default Card;

6 changes: 5 additions & 1 deletion app/src/hooks/useTodayFlashcards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { User } from 'firebase/auth';
import { chatCompletions } from '../api/ncloud-api';
import { getCommits, getFilename, getMarkdown, type CommitDetail } from '../api/github-api';
import { getCurrentDate } from '../modules/utils';
import { useNavigationStore } from '../stores/navigationStore';

const DATES_AGO = [1, 7, 30]; // days ago list

Expand All @@ -29,6 +30,7 @@ export function useTodayFlashcards(user: User | null) {
const { add, getByID } = useIndexedDB("data");
const [loading, setLoading] = useState<boolean>(true);
const [hasData, setHasData] = useState<boolean>(false);
const flashcardReloadTrigger = useNavigationStore((state) => state.flashcardReloadTrigger);

useEffect(() => {
// 사용자가 로그인하지 않은 경우 로딩 종료
Expand All @@ -40,6 +42,8 @@ export function useTodayFlashcards(user: User | null) {

const loadFlashcards = async () => {
try {
setLoading(true);

// 오늘 날짜의 데이터가 이미 있는지 확인
const todayData = await getByID(getCurrentDate());
if (todayData) {
Expand Down Expand Up @@ -68,7 +72,7 @@ export function useTodayFlashcards(user: User | null) {
};

loadFlashcards();
}, [add, getByID, user]);
}, [add, getByID, user, flashcardReloadTrigger]);

return { loading, hasData };
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/pages/FlashCardViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const FlashCardViewer: React.FC = () => {
onClick={flipCard}
aria-label="카드 뒤집기"
>
{flipped ? '🔙 질문 보기' : '💡 답변 보기'}
{flipped ? '질문 보기' : '카드 뒤집기'}
</button>
<button
className='button-circle'
Expand Down
50 changes: 44 additions & 6 deletions app/src/pages/Login.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One:wght@400&family=Nunito:wght@400;600;700&family=Noto+Sans+KR:wght@400;500;700&display=swap');

.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
font-family: 'Noto Sans KR', 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}
Expand All @@ -21,6 +23,26 @@
animation: slideUp 0.6s ease-out;
}

.character-image-container {
margin-bottom: 32px;
display: flex;
justify-content: center;
}

.character-image {
width: 160px;
height: auto;
border-radius: 16px;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1));
transition: transform 0.3s ease;
opacity: 0.95;
}

.character-image:hover {
transform: scale(1.05);
opacity: 1;
}

@keyframes slideUp {
from {
opacity: 0;
Expand All @@ -34,16 +56,24 @@

.login-header h1 {
color: #333;
margin-bottom: 8px;
font-size: 2rem;
margin-bottom: 20px;
font-size: 3.2rem;
font-weight: 700;
font-family: 'Fredoka One', 'Noto Sans KR', cursive, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.5px;
}

.login-header p {
.login-description p {
color: #666;
margin-bottom: 32px;
font-size: 1rem;
font-size: 1.1rem;
line-height: 1.5;
font-family: 'Noto Sans KR', 'Fredoka One', sans-serif;
font-weight: 500;
}

.github-login-button {
Expand Down Expand Up @@ -167,8 +197,16 @@
padding: 24px;
}

.character-image {
width: 140px;
}

.login-header h1 {
font-size: 1.5rem;
font-size: 2.6rem;
}

.login-description p {
font-size: 1rem;
}

.github-login-button {
Expand Down
Loading