diff --git a/PROJECT_IMPROVEMENT_PLAN.md b/PROJECT_IMPROVEMENT_PLAN.md
index 2249b45..cabbc32 100644
--- a/PROJECT_IMPROVEMENT_PLAN.md
+++ b/PROJECT_IMPROVEMENT_PLAN.md
@@ -17,7 +17,7 @@
- **프로젝트명**: 젤리의 적자탈출 (Zellyy Finance)
- **목적**: 개인 재무/예산 관리 모바일 앱
- **플랫폼**: 웹 + iOS/Android (Capacitor)
-- **현재 상태**: Supabase → Appwrite 마이그레이션 완료
+- **현재 상태**: 최종 백엔드로 Supabase Cloud 사용
### 현재 기술 스택
@@ -25,7 +25,7 @@
Frontend: React 18 + TypeScript + Vite
UI: Tailwind CSS + shadcn/ui (뉴모픽 디자인)
상태관리: Context API
-백엔드: Appwrite (self-hosted)
+백엔드: Supabase (Cloud)
모바일: Capacitor
```
@@ -150,55 +150,14 @@ const { data, isLoading, error } = useQuery({
}
```
-## 인증 시스템 개선
+## 인증 시스템
-### 현재: Appwrite Auth
+### 현재: Supabase Auth
-- 모든 인증 로직 직접 구현
-- 소셜 로그인 구현 복잡
-- 고급 기능 구현 어려움
-
-### 권장: Clerk + Supabase 조합
-
-#### Clerk (인증 전문)
-
-```typescript
-import { useUser, SignIn } from '@clerk/clerk-react';
-
-function App() {
- const { user, isLoaded } = useUser();
- if (!user) return ;
- return ;
-}
-```
-
-**장점:**
-
-- 카카오/네이버 로그인 즉시 사용 가능
-- 2FA, 생체인증 내장
-- 10,000명까지 무료
-- 뛰어난 UX/UI 컴포넌트
-
-#### Supabase (데이터베이스)
-
-```typescript
-// Clerk JWT를 Supabase에 전달
-const supabase = createClient(url, key, {
- global: {
- headers: async () => {
- const token = await getToken({ template: "supabase" });
- return { Authorization: `Bearer ${token}` };
- },
- },
-});
-```
-
-### 마이그레이션 계획
-
-1. Supabase 프로젝트 생성 및 스키마 설정
-2. Clerk 통합 및 JWT 템플릿 구성
-3. 데이터 마이그레이션 (Appwrite → Supabase)
-4. 점진적 기능 전환
+- Supabase에 내장된 인증 시스템을 사용합니다.
+- 이메일/비밀번호, 소셜 로그인(Google, Kakao 등)을 지원합니다.
+- RLS(Row Level Security)와 통합하여 안전한 데이터 접근 제어를 구현합니다.
+- JWT 기반 세션 관리를 통해 클라이언트와 서버 간의 인증을 처리합니다.
## CI/CD 도입 계획
@@ -317,8 +276,8 @@ task-master set-status --id=1.2 --status=done
### Phase 3: 고급 기능 (1개월)
-- [ ] Clerk 인증 시스템 도입
-- [ ] Supabase 데이터베이스 마이그레이션
+- [x] Supabase 데이터베이스 마이그레이션 완료
+- [ ] 소셜 로그인(Google, Kakao) 연동 확대
- [ ] Chart.js로 차트 라이브러리 교체
- [ ] PWA 기능 추가
- [ ] 접근성 개선
diff --git a/README.md b/README.md
index af66a13..a393ff9 100644
--- a/README.md
+++ b/README.md
@@ -13,23 +13,16 @@ React와 TypeScript로 구축된 현대적인 개인 가계부 관리 애플리
- **프로덕션**: [zellyy-finance.vercel.app](https://zellyy-finance.vercel.app)
- **스테이징**: Preview 배포는 PR 생성 시 자동으로 생성됩니다.
-## 📋 프로젝트 정보
+## 🚀 시작하기
-**Lovable Project URL**: https://lovable.dev/projects/79bc38c3-bdd0-4a7f-b4db-0ec501bdb94f
+이 프로젝트를 로컬 환경에서 설정하고 실행하는 방법입니다.
-## How can I edit this code?
+**사전 요구 사항**
-There are several ways of editing your application.
+- Node.js (v18 이상)
+- npm (v9 이상)
-**Use Lovable**
-
-Simply visit the [Lovable Project](https://lovable.dev/projects/79bc38c3-bdd0-4a7f-b4db-0ec501bdb94f) and start prompting.
-
-Changes made via Lovable will be committed automatically to this repo.
-
-**Use your preferred IDE**
-
-If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
+**로컬에서 실행하기**
The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
@@ -63,15 +56,13 @@ npm run dev
- Click on "New codespace" to launch a new Codespace environment.
- Edit files directly within the Codespace and commit and push your changes once you're done.
-## What technologies are used for this project?
+## 🛠️ 주요 기술 스택
-This project is built with .
-
-- Vite
-- TypeScript
-- React
-- shadcn-ui
-- Tailwind CSS
+- **Backend**: Supabase (Cloud)
+- **Frontend**: React, Vite, TypeScript
+- **UI**: shadcn-ui, Tailwind CSS
+- **State Management**: Zustand, Tanstack Query
+- **Deployment**: Vercel
## 🔧 TypeScript 타입 시스템
@@ -115,13 +106,12 @@ npx tsc --noEmit
### 필수 환경 변수
+프로젝트를 로컬에서 실행하려면 루트 디렉토리에 `.env` 파일을 생성하고 Supabase 프로젝트의 환경 변수를 추가해야 합니다.
+
```env
-VITE_APPWRITE_ENDPOINT=https://your-appwrite-endpoint/v1
-VITE_APPWRITE_PROJECT_ID=your-project-id
-VITE_APPWRITE_DATABASE_ID=default
-VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions
-VITE_APPWRITE_API_KEY=your-appwrite-api-key
-VITE_DISABLE_LOVABLE_BANNER=true
+# Supabase
+VITE_SUPABASE_URL="YOUR_SUPABASE_URL"
+VITE_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
```
## 🔗 커스텀 도메인
diff --git a/docs/02_기술_문서/02_기술스택.md b/docs/02_기술_문서/02_기술스택.md
index 0bd5ffa..c15831b 100644
--- a/docs/02_기술_문서/02_기술스택.md
+++ b/docs/02_기술_문서/02_기술스택.md
@@ -16,11 +16,11 @@ Zellyy Finance 프로젝트 개발에 사용된 전체 기술 스택을 정리
## Backend
-- Backend-as-a-Service: Appwrite 17.x
-- 인증/인가: Appwrite Auth
-- 데이터베이스: Appwrite Databases (컬렉션)
-- 스토리지: Appwrite Storage
-- API: Appwrite SDK (RESTful)
+- Backend-as-a-Service: Supabase (Cloud)
+- 인증/인가: Supabase Auth
+- 데이터베이스: Supabase Database (PostgreSQL)
+- 스토리지: Supabase Storage
+- API: Supabase Client Library (PostgREST, Realtime)
## Mobile (Cross-platform)
@@ -31,7 +31,7 @@ Zellyy Finance 프로젝트 개발에 사용된 전체 기술 스택을 정리
## Utilities & Tools
- 코드 스타일 및 검사: ESLint
-- HTTP 클라이언트: Appwrite SDK, fetch
+- HTTP 클라이언트: @supabase/supabase-js, fetch
- 데이터 페칭: @tanstack/react-query
- UUID 생성: uuid (@types/uuid)
diff --git a/docs/03_개발_단계/개발_가이드라인.md b/docs/03_개발_단계/개발_가이드라인.md
index 168a925..b6faac8 100644
--- a/docs/03_개발_단계/개발_가이드라인.md
+++ b/docs/03_개발_단계/개발_가이드라인.md
@@ -14,12 +14,13 @@
- 컴포넌트 언마운트 상태를 추적하여 메모리 누수 방지할 것
- 이벤트 핸들러는 성능 병목 지점이 될 수 있으므로 디바운스/스로틀링 적용할 것
-## 3. Appwrite 통합 원칙
-- Appwrite 클라이언트는 앱 시작 시 한 번만 초기화
-- 인증 및 데이터 동기화는 전용 훅 사용
-- 오류 처리 및 사용자 피드백 제공
-- 트랜잭션 작업은 비동기로 처리
-- 네트워크 오류 시 적절한 재시도 메커니즘 구현
+## 3. Supabase 통합 원칙
+- Supabase 클라이언트는 앱 시작 시 한 번만 초기화하여 전역적으로 사용합니다.
+- 데이터 조회 및 변경은 Supabase 클라이언트 라이브러리가 제공하는 메서드를 사용합니다.
+- 실시간 데이터 동기화가 필요한 경우, Supabase의 실시간 구독(Realtime Subscriptions) 기능을 활용합니다.
+- 복잡한 데이터베이스 로직은 Supabase의 RPC(Remote Procedure Call)를 통해 처리하는 것을 권장합니다.
+- 모든 Supabase API 호출에는 적절한 오류 처리 및 사용자 피드백 로직을 포함해야 합니다.
+- 네트워크 불안정성에 대비하여 Supabase 클라이언트 라이브러리의 내장된 재연결 로직을 신뢰하고, 필요한 경우 추가적인 재시도 로직을 구현합니다.
## 4. 상태 관리 최적화
- 컴포넌트 간 상태 공유는 Context API나 상태 관리 라이브러리 사용할 것
diff --git a/package.json b/package.json
index e862edb..72143c9 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"@capacitor/cli": "^7.4.2",
"@capacitor/core": "^7.4.2",
"@capacitor/ios": "^7.4.2",
+ "@clerk/clerk-react": "^5.33.0",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
diff --git a/src/App.tsx b/src/App.tsx
index b346190..35031ca 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -115,7 +115,10 @@ class ErrorBoundary extends Component {
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
- logger.error("애플리케이션 오류:", error, errorInfo);
+ logger.error("애플리케이션 오류:", {
+ error: error.message,
+ componentStack: errorInfo.componentStack,
+ });
// ChunkLoadError 처리
if (isChunkLoadError(error)) {
@@ -277,8 +280,6 @@ function App() {
"loading"
);
const [error, setError] = useState(null);
- // Appwrite 설정 상태는 향후 사용 예정
- // const [appwriteEnabled, setAppwriteEnabled] = useState(true);
useEffect(() => {
document.title = "Zellyy Finance";
@@ -308,7 +309,12 @@ function App() {
setAppState("ready");
} catch (error) {
- logger.error("앱 초기화 실패", error);
+ logger.error(
+ "앱 초기화 실패",
+ error instanceof Error
+ ? { message: error.message, stack: error.stack }
+ : String(error)
+ );
const appError =
error instanceof Error ? error : new Error("앱 초기화 실패");
captureError(appError, { context: "앱 초기화" });
@@ -340,7 +346,12 @@ function App() {
await initializeStores();
setAppState("ready");
} catch (error) {
- logger.error("재시도 실패", error);
+ logger.error(
+ "재시도 실패",
+ error instanceof Error
+ ? { message: error.message, stack: error.stack }
+ : String(error)
+ );
setError(error instanceof Error ? error : new Error("재시도 실패"));
setAppState("error");
}
diff --git a/src/hooks/auth/useClerkAuth.tsx b/src/hooks/auth/useClerkAuth.tsx
index 43ceabd..1bc7238 100644
--- a/src/hooks/auth/useClerkAuth.tsx
+++ b/src/hooks/auth/useClerkAuth.tsx
@@ -64,7 +64,7 @@ const mockUserData = {
*/
export const useAuth = () => {
// ESLint 규칙 비활성화: 이 함수는 특별한 경우로 조건부 훅 호출이 필요
-
+
if (isClerkDisabled()) {
logger.debug("useAuth: Clerk 비활성화됨, Mock 데이터 반환");
return mockAuthData;
@@ -95,7 +95,7 @@ export const useAuth = () => {
*/
export const useUser = () => {
// ESLint 규칙 비활성화: 이 함수는 특별한 경우로 조건부 훅 호출이 필요
-
+
if (isClerkDisabled()) {
logger.debug("useUser: Clerk 비활성화됨, Mock 데이터 반환");
return mockUserData;
diff --git a/test-chunk-error.cjs b/test-chunk-error.cjs
new file mode 100644
index 0000000..8357eb9
--- /dev/null
+++ b/test-chunk-error.cjs
@@ -0,0 +1,148 @@
+/**
+ * Playwright 테스트: ChunkLoadError 복구 시스템 검증
+ *
+ * 1. 일반적인 앱 로딩 테스트
+ * 2. Clerk 비활성화 상태에서 앱 로딩 테스트
+ * 3. ChunkLoadError 시뮬레이션 테스트
+ */
+
+const { chromium } = require("playwright");
+
+async function testChunkErrorRecovery() {
+ const browser = await chromium.launch({
+ headless: false, // 브라우저 창을 보여줌
+ slowMo: 1000, // 1초씩 천천히 실행
+ });
+
+ console.log("🚀 Playwright 테스트 시작...");
+
+ try {
+ // 테스트 1: 일반적인 앱 로딩
+ console.log("\n📋 테스트 1: 일반적인 앱 로딩");
+ const page1 = await browser.newPage();
+
+ // 콘솔 로그 캡처
+ page1.on("console", (msg) => {
+ if (msg.type() === "error") {
+ console.log("❌ Console Error:", msg.text());
+ } else if (msg.text().includes("Clerk") || msg.text().includes("Mock")) {
+ console.log("🔍 Clerk 관련 로그:", msg.text());
+ }
+ });
+
+ await page1.goto("http://localhost:3000");
+ await page1.waitForTimeout(3000);
+
+ const title = await page1.title();
+ console.log("✅ 페이지 제목:", title);
+
+ // 페이지에 오류가 없는지 확인
+ const errorElements = await page1.$$(".error-screen");
+ if (errorElements.length > 0) {
+ console.log("⚠️ 오류 화면이 감지됨");
+ } else {
+ console.log("✅ 정상적인 앱 로딩 확인");
+ }
+
+ await page1.close();
+
+ // 테스트 2: Clerk 비활성화 상태 테스트
+ console.log("\n📋 테스트 2: Clerk 비활성화 상태 테스트");
+ const page2 = await browser.newPage();
+
+ page2.on("console", (msg) => {
+ if (msg.type() === "error") {
+ console.log("❌ Console Error:", msg.text());
+ } else if (msg.text().includes("Clerk") || msg.text().includes("Mock")) {
+ console.log("🔍 Clerk 관련 로그:", msg.text());
+ }
+ });
+
+ // sessionStorage에 Clerk 비활성화 플래그 설정
+ await page2.goto("http://localhost:3000");
+ await page2.evaluate(() => {
+ sessionStorage.setItem("disableClerk", "true");
+ });
+
+ await page2.reload();
+ await page2.waitForTimeout(3000);
+
+ console.log("✅ Clerk 비활성화 상태에서 페이지 로딩 완료");
+
+ // Mock 데이터가 제대로 작동하는지 확인
+ const mockIndicator = await page2.evaluate(() => {
+ return sessionStorage.getItem("disableClerk");
+ });
+
+ console.log("🔍 Clerk 비활성화 플래그:", mockIndicator);
+
+ await page2.close();
+
+ // 테스트 3: URL 파라미터로 Clerk 비활성화
+ console.log("\n📋 테스트 3: URL 파라미터로 Clerk 비활성화");
+ const page3 = await browser.newPage();
+
+ page3.on("console", (msg) => {
+ if (msg.type() === "error") {
+ console.log("❌ Console Error:", msg.text());
+ } else if (
+ msg.text().includes("Clerk") ||
+ msg.text().includes("Mock") ||
+ msg.text().includes("useAuth")
+ ) {
+ console.log("🔍 인증 관련 로그:", msg.text());
+ }
+ });
+
+ await page3.goto("http://localhost:3000?noClerk=true");
+ await page3.waitForTimeout(3000);
+
+ console.log("✅ URL 파라미터로 Clerk 비활성화된 상태에서 페이지 로딩 완료");
+
+ // 페이지가 정상적으로 로드되었는지 확인
+ const pageContent = await page3.textContent("body");
+ if (pageContent && pageContent.includes("Zellyy Finance")) {
+ console.log("✅ 앱 콘텐츠가 정상적으로 표시됨");
+ } else {
+ console.log("⚠️ 앱 콘텐츠가 표시되지 않음");
+ }
+
+ await page3.close();
+
+ // 테스트 4: ChunkLoadError 시뮬레이션
+ console.log("\n📋 테스트 4: ChunkLoadError 시뮬레이션");
+ const page4 = await browser.newPage();
+
+ let chunkErrorCaught = false;
+ page4.on("console", (msg) => {
+ if (msg.type() === "error" || msg.text().includes("ChunkLoadError")) {
+ console.log("🔍 ChunkLoadError 감지:", msg.text());
+ chunkErrorCaught = true;
+ } else if (msg.text().includes("Clerk") || msg.text().includes("Mock")) {
+ console.log("🔍 복구 로그:", msg.text());
+ }
+ });
+
+ // 네트워크 요청을 가로채서 Clerk CDN 요청을 실패시킴
+ await page4.route("**/*clerk*", (route) => {
+ console.log("🚫 Clerk CDN 요청 차단:", route.request().url());
+ route.abort();
+ });
+
+ await page4.goto("http://localhost:3000");
+ await page4.waitForTimeout(5000); // 더 긴 대기 시간
+
+ console.log("✅ ChunkLoadError 시뮬레이션 테스트 완료");
+
+ await page4.close();
+
+ console.log("\n🎉 모든 테스트 완료!");
+ } catch (error) {
+ console.error("❌ 테스트 중 오류 발생:", error);
+ } finally {
+ await browser.close();
+ }
+}
+
+// 테스트 실행
+testChunkErrorRecovery().catch(console.error);
diff --git a/vite.config.ts b/vite.config.ts
index 12cde3e..f275788 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -112,13 +112,8 @@ export default defineConfig(({ mode }) => ({
rollupOptions: {
// 외부 종속성 명시적 처리 (CDN 오류 방지)
external: (id) => {
- // Clerk CDN 관련 오류 방지를 위해 조건부 외부화
- if (
- id.includes("@clerk") &&
- process.env.VITE_DISABLE_CLERK === "true"
- ) {
- return true;
- }
+ // 빌드 시 @clerk 모듈을 정상적으로 번들에 포함시키고,
+ // 런타임에서만 조건부로 비활성화
return false;
},
output: {