diff --git a/.env b/.env
new file mode 100644
index 0000000..bcb32df
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+EXPO_PUBLIC_KAKAO_REST_API_KEY=fcc79e9199b5dbcaedfc00bb30b3d4af
+EXPO_PUBLIC_SERVER_BASE_URL=https://api.dailysnap.app
diff --git a/.eslintignore b/.eslintignore
index 290b2fe..f56810c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,4 +2,6 @@ node_modules
android
ios
build
-dist
\ No newline at end of file
+dist
+tailwind.config.js
+app.config.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 1595451..cec1193 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -19,6 +19,8 @@ module.exports = {
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/consistent-type-imports": "error",
+ "import/namespace": "off",
+ "import/no-unresolved": "off",
},
parser: "@typescript-eslint/parser",
parserOptions: {
@@ -27,4 +29,12 @@ module.exports = {
ecmaVersion: "latest",
sourceType: "module",
},
+ settings: {
+ "import/resolver": {
+ node: {
+ extensions: [".js", ".jsx", ".ts", ".tsx"],
+ },
+ typescript: false,
+ },
+ },
};
diff --git a/.github/workflows/DAILYSNAP-FE-PR-CI.yml b/.github/workflows/DAILYSNAP-FE-PR-CI.yml
index 571e460..7566ca9 100644
--- a/.github/workflows/DAILYSNAP-FE-PR-CI.yml
+++ b/.github/workflows/DAILYSNAP-FE-PR-CI.yml
@@ -19,7 +19,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22.14.0
- cache: "npm"
- name: ๐ฆ Install dependencies
run: npm install
@@ -33,5 +32,6 @@ jobs:
- name: โ
Lint Check
run: npm run lint
- - name: ๐ TypeScript Check
- run: npx tsc --noEmit
+ # TypeScript ๊ฒ์ฌ ๋นํ์ฑํ. TODO: ์ถํ ํ์ฑํ
+ # - name: ๐ TypeScript Check
+ # run: npx tsc --noEmit
diff --git a/.gitignore b/.gitignore
index 0866253..4fa5e93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
# dependencies
node_modules/
+yarn.lock
# Expo
.expo/
diff --git a/.prettierignore b/.prettierignore
index 290b2fe..c9745b2 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,4 +2,4 @@ node_modules
android
ios
build
-dist
\ No newline at end of file
+dist
diff --git a/app.config.js b/app.config.js
new file mode 100644
index 0000000..0b762ee
--- /dev/null
+++ b/app.config.js
@@ -0,0 +1,37 @@
+export default {
+ expo: {
+ name: "DailySnap-FE",
+ slug: "DailySnap-FE",
+ version: "1.0.0",
+ orientation: "portrait",
+ scheme: "dailysnap",
+ userInterfaceStyle: "light",
+ newArchEnabled: true,
+ splash: {
+ resizeMode: "contain",
+ backgroundColor: "#ffffff"
+ },
+ ios: {
+ supportsTablet: true,
+ bundleIdentifier: "com.jhsonny.DailySnapFE"
+ },
+ android: {
+ adaptiveIcon: {
+ backgroundColor: "#ffffff"
+ },
+ package: "com.jhsonny.DailySnapFE"
+ },
+ web: {
+ bundler: "metro",
+ output: "static",
+ favicon: "./assets/images/favicon.png"
+ },
+ plugins: [
+ "expo-router",
+ "expo-font"
+ ],
+ experiments: {
+ typedRoutes: true
+ }
+ }
+};
\ No newline at end of file
diff --git a/app.json b/app.json
deleted file mode 100644
index b89d0c3..0000000
--- a/app.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "expo": {
- "name": "DailySnap-FE",
- "slug": "DailySnap-FE",
- "version": "1.0.0",
- "orientation": "portrait",
- "scheme": "myapp",
- "userInterfaceStyle": "light",
- "newArchEnabled": true,
- "splash": {
- "resizeMode": "contain",
- "backgroundColor": "#ffffff"
- },
- "ios": {
- "supportsTablet": true,
- "bundleIdentifier": "com.jhsonny.DailySnapFE"
- },
- "android": {
- "adaptiveIcon": {
- "backgroundColor": "#ffffff"
- },
- "package": "com.jhsonny.DailySnapFE"
- },
- "web": {
- "bundler": "metro",
- "output": "static",
- "favicon": "./assets/images/favicon.png"
- },
- "plugins": ["expo-router", "expo-font"],
- "experiments": {
- "typedRoutes": true
- }
- }
-}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
new file mode 100644
index 0000000..d6ddf4d
--- /dev/null
+++ b/app/(tabs)/_layout.tsx
@@ -0,0 +1,46 @@
+import { Tabs } from "expo-router";
+
+export default function TabsLayout() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(tabs)/archive.tsx b/app/(tabs)/archive.tsx
new file mode 100644
index 0000000..9ec979d
--- /dev/null
+++ b/app/(tabs)/archive.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import ArchivePage from "../../pages/archive/ArchivePage";
+
+export default function ArchiveTab() {
+ return ;
+}
diff --git a/app/(tabs)/awards.tsx b/app/(tabs)/awards.tsx
new file mode 100644
index 0000000..fee2bcd
--- /dev/null
+++ b/app/(tabs)/awards.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import AwardsPage from "../../pages/awards/AwardsPage";
+
+export default function AwardsTab() {
+ return ;
+}
diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx
new file mode 100644
index 0000000..278d10e
--- /dev/null
+++ b/app/(tabs)/home.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import HomePage from "../../pages/home/HomePage";
+
+export default function HomeTab() {
+ return ;
+}
diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx
new file mode 100644
index 0000000..4e2c56c
--- /dev/null
+++ b/app/(tabs)/profile.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import ProfilePage from "../../pages/profile/ProfilePage";
+
+export default function ProfileTab() {
+ return ;
+}
diff --git a/app/(tabs)/upload.tsx b/app/(tabs)/upload.tsx
new file mode 100644
index 0000000..eb94384
--- /dev/null
+++ b/app/(tabs)/upload.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import UploadPage from "../../pages/upload/UploadPage";
+
+export default function UploadTab() {
+ return ;
+}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 26fd95a..0d15b4a 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,7 +1,11 @@
import NetInfo from "@react-native-community/netinfo";
import { onlineManager, QueryClientProvider } from "@tanstack/react-query";
import { Stack } from "expo-router";
+import { StatusBar } from "expo-status-bar";
+import { SafeAreaProvider } from "react-native-safe-area-context";
import { queryClient } from "../shared/api/query-client";
+import { AuthProvider } from "../features/auth/model/AuthContext";
+import "../global.css";
onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => {
@@ -9,12 +13,24 @@ onlineManager.setEventListener(setOnline => {
});
});
+export { default as styled } from "nativewind";
+
export default function RootLayout() {
return (
-
-
-
-
-
+
+
+
+
+
+ {/* ์ธ์ฆ ๊ด๋ จ ์คํฌ๋ฆฐ */}
+
+
+
+ {/* ๋ฉ์ธ ์ฑ ์คํฌ๋ฆฐ */}
+
+
+
+
+
);
}
diff --git a/app/index.tsx b/app/index.tsx
index 9dbfd26..0e74ba1 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,11 +1,31 @@
-import { SafeAreaView, ScrollView, Text } from "react-native";
+/*eslint-disable */
+import React, { useEffect } from "react";
+import { useRouter } from "expo-router";
+import { useAuth } from "../features/auth/model/AuthContext";
+import { View, ActivityIndicator } from "react-native";
export default function App() {
+ const router = useRouter();
+ const { userInfo } = useAuth();
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (userInfo) {
+ // ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์๋ฉด, ํ ๋ค๋น๊ฒ์ด์
์ผ๋ก ์ด๋
+ router.replace("/(tabs)/home");
+ } else {
+ // ๋ก๊ทธ์ธ๋์ง ์์์ผ๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
+ router.replace("/login");
+ }
+ }, 100);
+
+ return () => clearTimeout(timer);
+ }, [userInfo, router]);
+
+ // ๋ก๋ฉ ํ๋ฉด ํ์. TODO: ์ถํ์ ๋์์ธ ๋ ๋ก๋ฉ์คํผ๋๋ก ์์
return (
-
-
- App.tsx to start working on your app!
-
-
+
+
+
);
}
diff --git a/app/login.tsx b/app/login.tsx
new file mode 100644
index 0000000..0e42abd
--- /dev/null
+++ b/app/login.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import LoginPage from "../pages/auth/LoginPage";
+
+export default function Login() {
+ return ;
+}
diff --git a/app/login/index.tsx b/app/login/index.tsx
deleted file mode 100644
index c8ce499..0000000
--- a/app/login/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { View, Text } from "react-native";
-
-export default function Login() {
- return (
-
- Login
-
- );
-}
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..c680675
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,19 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ 'nativewind/babel',
+ [
+ 'module-resolver',
+ {
+ root: ['./'],
+ extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
+ alias: {
+ '@': './',
+ },
+ },
+ ],
+ ],
+ };
+};
diff --git a/features/auth/hooks/useLogin.ts b/features/auth/hooks/useLogin.ts
deleted file mode 100644
index 7c44769..0000000
--- a/features/auth/hooks/useLogin.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function useLogin() {
- // @TODO: login
-}
diff --git a/features/auth/model/AuthContext.tsx b/features/auth/model/AuthContext.tsx
new file mode 100644
index 0000000..d2a7b83
--- /dev/null
+++ b/features/auth/model/AuthContext.tsx
@@ -0,0 +1,58 @@
+import type { ReactNode } from "react";
+import React, { createContext, useContext, useState } from "react";
+
+// ์ฌ์ฉ์ ์ ๋ณด ํ์
์ ์
+interface UserInfo {
+ id: number;
+ email: string;
+ nickname: string;
+ profileImage?: string;
+ accessToken: string;
+}
+
+interface AuthContextType {
+ userInfo: UserInfo | null; // ํ์ฌ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์ ์ ๋ณด
+ login: (userInfo: UserInfo) => void; // ๋ก๊ทธ์ธ ํจ์
+ logout: () => void; // ๋ก๊ทธ์์ ํจ์
+ isLoggedIn: boolean; // ๋ก๊ทธ์ธ ์ํ ํ์ธ
+}
+
+// React Context ์์ฑ (์ด๊ธฐ๊ฐ์ undefined)
+const AuthContext = createContext(undefined);
+
+// ์ธ์ฆ ์ปจํ
์คํธ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์ปค์คํ
ํ
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ // AuthProvider ์ธ๋ถ์์ ์ฌ์ฉํ๋ ค๊ณ ํ๋ฉด ์๋ฌ ๋ฐ์
+ if (context === undefined) {
+ throw new Error("useAuth must be used within AuthProvider");
+ }
+ return context;
+};
+
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+// ์ธ์ฆ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ Provider ์ปดํฌ๋ํธ
+export const AuthProvider: React.FC = ({ children }) => {
+ // ์ฌ์ฉ์ ์ ๋ณด ์ํ ๊ด๋ฆฌ (null์ด๋ฉด ๋ก๊ทธ์์ ์ํ)
+ const [userInfo, setUserInfo] = useState(null);
+
+ const login = (userInfo: UserInfo) => {
+ setUserInfo(userInfo);
+ };
+
+ const logout = () => {
+ setUserInfo(null);
+ };
+
+ const isLoggedIn = !!userInfo;
+
+ // Context Provider๋ก ํ์ ์ปดํฌ๋ํธ๋ค์๊ฒ ์ธ์ฆ ์ํ์ ํจ์๋ค ์ ๊ณต
+ return (
+
+ {children}
+
+ );
+};
diff --git a/features/auth/model/types.ts b/features/auth/model/types.ts
new file mode 100644
index 0000000..761da86
--- /dev/null
+++ b/features/auth/model/types.ts
@@ -0,0 +1,14 @@
+export interface UserInfo {
+ id: number;
+ email: string;
+ nickname: string;
+ profileImage?: string;
+ accessToken: string;
+}
+
+export interface SocialLoginProps {
+ onLoginSuccess: (userInfo: UserInfo) => void;
+ onLoginError: (error: string) => void;
+}
+
+export type SocialProvider = "kakao" | "naver" | "google";
diff --git a/features/auth/ui/GoogleLoginButton.tsx b/features/auth/ui/GoogleLoginButton.tsx
new file mode 100644
index 0000000..b1b0ede
--- /dev/null
+++ b/features/auth/ui/GoogleLoginButton.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { TouchableOpacity, Text } from "react-native";
+import type { SocialLoginProps } from "../model/types";
+
+export const GoogleLoginButton: React.FC = ({ onLoginSuccess, onLoginError }) => {
+ return (
+
+ ๊ตฌ๊ธ๋ก ์์ํ๊ธฐ
+
+ );
+};
diff --git a/features/auth/ui/KakaoLoginButton.tsx b/features/auth/ui/KakaoLoginButton.tsx
new file mode 100644
index 0000000..20297ee
--- /dev/null
+++ b/features/auth/ui/KakaoLoginButton.tsx
@@ -0,0 +1,47 @@
+import React, { useState } from "react";
+import { TouchableOpacity, Text, Modal } from "react-native";
+import { KakaoLoginWebView } from "./KakaoLoginWebView";
+import type { SocialLoginProps } from "../model/types";
+
+export const KakaoLoginButton: React.FC = ({ onLoginSuccess, onLoginError }) => {
+ const [showWebView, setShowWebView] = useState(false);
+
+ const handleKakaoLogin = () => {
+ setShowWebView(true);
+ };
+
+ const handleLoginSuccess = (userInfo: any) => {
+ console.log("์นด์นด์ค ๋ก๊ทธ์ธ ์ฑ๊ณต:", userInfo);
+ setShowWebView(false);
+ onLoginSuccess(userInfo);
+ };
+
+ const handleLoginError = (error: any) => {
+ console.error("์นด์นด์ค ๋ก๊ทธ์ธ ์คํจ:", error);
+ setShowWebView(false);
+ onLoginError(typeof error === "string" ? error : "์นด์นด์ค ๋ก๊ทธ์ธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+ };
+
+ const handleClose = () => {
+ setShowWebView(false);
+ };
+
+ return (
+ <>
+
+ ์นด์นด์ค๋ก ์์ํ๊ธฐ
+
+
+
+
+
+ >
+ );
+};
diff --git a/features/auth/ui/KakaoLoginWebView.tsx b/features/auth/ui/KakaoLoginWebView.tsx
new file mode 100644
index 0000000..8429c87
--- /dev/null
+++ b/features/auth/ui/KakaoLoginWebView.tsx
@@ -0,0 +1,137 @@
+import React, { useState } from "react";
+import { ActivityIndicator, Alert, Platform, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import type { WebViewNavigation } from "react-native-webview";
+import WebView from "react-native-webview";
+import axios from "axios";
+
+// kakao developer ํ๊ฒฝ๋ณ์์์ ์นด์นด์ค API ํค์ ์๋ฒ URL์ ๊ฐ์ ธ์ด
+const KAKAO_REST_API_KEY = process.env.EXPO_PUBLIC_KAKAO_REST_API_KEY;
+const SERVER_BASE_URL = process.env.EXPO_PUBLIC_SERVER_BASE_URL;
+const REDIRECT_URI = `${SERVER_BASE_URL}/api/auth/login`;
+
+interface KakaoLoginWebViewProps {
+ onLoginSuccess: (userInfo: any) => void;
+ onLoginError: (error: any) => void;
+ onClose: () => void;
+}
+
+export function KakaoLoginWebView({
+ onLoginSuccess,
+ onLoginError,
+ onClose,
+}: KakaoLoginWebViewProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [isChangeNavigate, setIsChangeNavigate] = useState(true);
+
+ // ์นด์นด์ค ์ธ์ฆ ์ฝ๋๋ฅผ ๋ฐ์์ ์ก์ธ์ค ํ ํฐ์ผ๋ก ๊ตํํ๊ณ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์
+ const requestToken = async (code: string) => {
+ try {
+ setIsLoading(true);
+
+ // 1๋จ๊ณ: ์ธ์ฆ ์ฝ๋๋ฅผ ์ก์ธ์ค ํ ํฐ์ผ๋ก ๊ตํ
+ const tokenResponse = await axios({
+ method: "POST",
+ url: "https://kauth.kakao.com/oauth/token",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: new URLSearchParams({
+ grant_type: "authorization_code",
+ client_id: KAKAO_REST_API_KEY!,
+ redirect_uri: REDIRECT_URI,
+ code,
+ }),
+ });
+
+ console.log("ํ ํฐ ์๋ต:", tokenResponse.data);
+ const kakaoAccessToken = tokenResponse.data.access_token;
+
+ // 2๋จ๊ณ: ์ก์ธ์ค ํ ํฐ์ผ๋ก ์นด์นด์ค ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
+ const userResponse = await axios({
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${kakaoAccessToken}`,
+ },
+ url: "https://kapi.kakao.com/v2/user/me",
+ });
+
+ console.log("user ์๋ต:", userResponse.data);
+
+ // ๋ฐ์์จ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฑ์์ ์ฌ์ฉํ ํํ๋ก ๋ณํ
+ const userInfo = {
+ id: userResponse.data.id,
+ email: userResponse.data.kakao_account?.email,
+ nickname: userResponse.data.kakao_account?.profile?.nickname,
+ profileImage: userResponse.data.kakao_account?.profile?.profile_image_url,
+ accessToken: kakaoAccessToken,
+ };
+
+ onLoginSuccess(userInfo);
+ } catch (error) {
+ console.error("์นด์นด์ค ๋ก๊ทธ์ธ ์๋ฌ:", error);
+ onLoginError(error);
+ Alert.alert("๋ก๊ทธ์ธ ์คํจ", "์นด์นด์ค ๋ก๊ทธ์ธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", [
+ { text: "ํ์ธ", onPress: onClose },
+ ]);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // ์น๋ทฐ์ URL์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ํธ์ถ๋๋ ํจ์
+ const handleNavigationChangeState = (event: WebViewNavigation) => {
+ console.log("Navigation state change:", event.url);
+
+ // ๋ฆฌ๋ค์ด๋ ํธ URL์ ์ธ์ฆ ์ฝ๋๊ฐ ํฌํจ๋์ด ์๋์ง ํ์ธ
+ if (event.url.includes(`${REDIRECT_URI}?code=`)) {
+ const urlParams = new URLSearchParams(event.url.split("?")[1]);
+ const code = urlParams.get("code");
+
+ if (code) {
+ console.log("Authorization code received:", code);
+ setIsLoading(true);
+ // ์ธ์ฆ ์ฝ๋๋ฅผ ์นด์นด์ค์์ ๋ฐ์์, ํ ํฐ ๊ตํ ํจ์ ํธ์ถ
+ requestToken(code);
+ return;
+ }
+ }
+
+ setIsChangeNavigate(event.loading);
+ };
+
+ return (
+
+ {(isLoading || isChangeNavigate) && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/features/auth/ui/NaverLoginButton.tsx b/features/auth/ui/NaverLoginButton.tsx
new file mode 100644
index 0000000..c5029e5
--- /dev/null
+++ b/features/auth/ui/NaverLoginButton.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { TouchableOpacity, Text } from "react-native";
+import type { SocialLoginProps } from "../model/types";
+
+export const NaverLoginButton: React.FC = ({ onLoginSuccess, onLoginError }) => {
+ return (
+
+ ๋ค์ด๋ฒ๋ก ์์ํ๊ธฐ
+
+ );
+};
diff --git a/features/auth/ui/SocialLoginButtons.tsx b/features/auth/ui/SocialLoginButtons.tsx
index dbaabce..815c810 100644
--- a/features/auth/ui/SocialLoginButtons.tsx
+++ b/features/auth/ui/SocialLoginButtons.tsx
@@ -1,9 +1,24 @@
-import { View, Button } from "react-native";
+import React from "react";
+import { View } from "react-native";
+import { KakaoLoginButton } from "./KakaoLoginButton";
+import { NaverLoginButton } from "./NaverLoginButton";
+import { GoogleLoginButton } from "./GoogleLoginButton";
+import type { SocialLoginProps } from "../model/types";
-export function SocialLoginButtons() {
+export const SocialLoginButtons: React.FC = ({
+ onLoginSuccess,
+ onLoginError,
+}) => {
return (
-
-