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
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"react-slick": "^0.30.2",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"slick-carousel": "^1.8.1"
"slick-carousel": "^1.8.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/react": "^18.2.0",
Expand Down
Binary file added app/public/nodata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 44 additions & 51 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import React, { useEffect, useState } from 'react';
import { initDB, useIndexedDB } from "react-indexed-db-hook";
import { initDB } from "react-indexed-db-hook";
import { onAuthStateChanged, User } from 'firebase/auth';

import { DBConfig } from './DBConfig';
import { auth } from './firebase';
import { chatCompletions } from './api/ncloud-api';
import { getCurrentDate } from './modules/utils';
import FlashCardViewer from './pages/FlashCardViewer';
import Login from './pages/Login';
import Settings from './pages/Settings';
import { getGithubData } from './services/github-service';
import NoDataView from './pages/NoDataView';
import UserDropdown from './widgets/UserDropdown';
import { useTodayFlashcards } from './hooks/useTodayFlashcards';
import { useNavigationStore } from './stores/navigationStore';

initDB(DBConfig);
const dates = [1, 7, 30]; // days ago list

type Page = 'flashcard' | 'settings';

const App: React.FC = () => {
const { add, getByID } = useIndexedDB("data");
const [loading, setLoading] = useState<boolean>(true);
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);
const { currentPage, navigateToSettings, navigateToFlashcard } = useNavigationStore();

// 오늘의 플래시카드 데이터 로드
const { loading, hasData } = useTodayFlashcards(user);

// 인증 상태 감지
useEffect(() => {
Expand All @@ -50,43 +48,6 @@ const App: React.FC = () => {
return () => window.removeEventListener('scroll', handleScroll);
}, []);

// 사용자가 로그인한 경우에만 데이터 로드
useEffect(() => {
if (!user) {
setLoading(false);
return;
}

(async () => {
const todayData = await getByID(getCurrentDate());
if (todayData) {
setLoading(false);
return;
}

let list: Array<{question: string, answer: string}> = [];
for (const ago of dates) {
try {
const githubData = await getGithubData(ago);
if (githubData) {
const result = await chatCompletions(githubData);
const questions = JSON.parse(result.body.result.message.content);
for (let ncloudData of questions) {
list.push({question: ncloudData, answer: githubData});
}
}
} catch (error) {
console.error(`Error fetching data for ${ago}:`, error);
}
}

if (list.length > 0) {
add({date: getCurrentDate(), data: list });
}
setLoading(false);
})();
}, [add, getByID, user]);

// 인증 로딩 중
if (authLoading) {
return (
Expand Down Expand Up @@ -115,13 +76,45 @@ const App: React.FC = () => {
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '1.2rem'
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>
);
}

// 데이터가 없는 경우 - 하지만 Settings 페이지는 허용
if (!hasData && currentPage === 'flashcard') {
return <NoDataView />;
}

// 메인 앱 렌더링
return (
<main>
Expand All @@ -145,7 +138,7 @@ const App: React.FC = () => {
<div>
{currentPage === 'settings' && (
<button
onClick={() => setCurrentPage('flashcard')}
onClick={navigateToFlashcard}
style={{
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.95)',
Expand Down Expand Up @@ -179,7 +172,7 @@ const App: React.FC = () => {

<UserDropdown
user={user}
onNavigateToSettings={() => setCurrentPage('settings')}
onNavigateToSettings={navigateToSettings}
/>
</nav>

Expand Down
31 changes: 27 additions & 4 deletions app/src/api/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ interface Commit {
};
}

interface CommitDetail {
files: Array<{
filename: string;
}>;
export interface FileChange {
filename: string;
status: 'added' | 'modified' | 'removed' | 'renamed';
additions: number;
deletions: number;
changes: number;
patch?: string;
}

export interface CommitDetail {
sha: string;
commit: {
message: string;
};
files: FileChange[];
}

interface MarkdownResponse {
Expand Down Expand Up @@ -53,3 +64,15 @@ export async function getRepositories(): Promise<Repository[]> {
const response = await apiClient.get('/getRepositories');
return response.data;
}

export interface Branch {
name: string;
protected: boolean;
}

export async function getBranches(owner: string, repo: string): Promise<Branch[]> {
const response = await apiClient.get('/getBranches', {
params: { owner, repo }
});
return response.data;
}
44 changes: 38 additions & 6 deletions app/src/api/ncloud-api.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
import { apiClient } from '../modules/axios';

interface ChatCompletionResponse {
body: {
result: {
message: {
content: string;
status: {
code: string;
message: string;
};
result: {
message: {
role: string;
content: string;
thinkingContent?: string;
};
finishReason: string;
created: number;
seed: number;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
completionTokensDetails?: {
thinkingTokens: number;
};
};
aiFilter?: Array<{
groupName: string;
name: string;
score: string;
result: string;
}>;
};
}

/**
* CLOVA Studio - Firebase Functions를 통해 호출
*/
export async function chatCompletions(text: string): Promise<ChatCompletionResponse> {
export async function chatCompletions(
text: string,
contentType: 'markdown' | 'code-diff' = 'markdown'
): Promise<ChatCompletionResponse> {
try {
let prompt: string;

if (contentType === 'markdown') {
prompt = "마크다운 파일을 읽고 질문을 만들어주세요. 질문은 다음과 같은 형식으로 출력해주세요. [\"첫 번째 질문\", \"두 번째 질문\", ...]";
} else {
prompt = "코드 변경 내용(diff)을 보고 질문을 만들어주세요. 변경된 코드의 목적, 동작, 영향 등에 대해 물어보세요. 질문은 다음과 같은 형식으로 출력해주세요. [\"첫 번째 질문\", \"두 번째 질문\", ...]";
}

const response = await apiClient.post('/chatCompletions', {
prompt: "마크다운 파일을 읽고 질문을 만들어주세요. 질문은 다음과 같은 형식으로 출력해주세요. [\"첫 번째 질문\", \"두 번째 질문\", ...]",
prompt: prompt,
text: text,
});

Expand Down
Loading