diff --git a/.expo/devices.json b/.expo/devices.json
index 5efff6c..b85be94 100644
--- a/.expo/devices.json
+++ b/.expo/devices.json
@@ -1,3 +1,8 @@
{
- "devices": []
+ "devices": [
+ {
+ "installationId": "F0B2F989-D042-4D9B-BE7C-8D09B00BD4C5",
+ "lastUsed": 1758012043849
+ }
+ ]
}
diff --git a/.expo/settings.json b/.expo/settings.json
new file mode 100644
index 0000000..92bc513
--- /dev/null
+++ b/.expo/settings.json
@@ -0,0 +1,8 @@
+{
+ "hostType": "lan",
+ "lanType": "ip",
+ "dev": true,
+ "minify": false,
+ "urlRandomness": null,
+ "https": false
+}
diff --git a/.expo/web/cache/production/images/iconsuniversal-icon/iconsuniversal-icon-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-cover-#ffffff/App-Icon-1024x1024@1x.png b/.expo/web/cache/production/images/iconsuniversal-icon/iconsuniversal-icon-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-cover-#ffffff/App-Icon-1024x1024@1x.png
new file mode 100644
index 0000000..faa06ef
Binary files /dev/null and b/.expo/web/cache/production/images/iconsuniversal-icon/iconsuniversal-icon-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-cover-#ffffff/App-Icon-1024x1024@1x.png differ
diff --git a/.expo/web/cache/production/images/splash-ios/splash-ios-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-contain/icon_undefined.png b/.expo/web/cache/production/images/splash-ios/splash-ios-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-contain/icon_undefined.png
new file mode 100644
index 0000000..93365c6
Binary files /dev/null and b/.expo/web/cache/production/images/splash-ios/splash-ios-b4d49e7392f44b0e1b4fd25a5b828b2bf3fd57e856a1c968620b71d61659e519-contain/icon_undefined.png differ
diff --git a/App.js b/App.js
index d2e0ac3..30b8baa 100644
--- a/App.js
+++ b/App.js
@@ -1,38 +1,186 @@
-import 'react-native-gesture-handler';
-import React, { useEffect, useRef } from 'react';
-import { NavigationContainer } from '@react-navigation/native';
-import StackNavigator from './src/navigation/StackNavigator';
-import { setupNotificationListeners } from './src/services/PushNotificationService';
+// App.js
+import "react-native-gesture-handler";
+import React, { useEffect, useRef } from "react";
+import { Platform } from "react-native";
+import { NavigationContainer } from "@react-navigation/native";
+import * as Device from "expo-device";
+import * as Notifications from "expo-notifications";
+import StackNavigator from "./src/navigation/StackNavigator";
+
+// Push ์๋น์ค
+import {
+ setupNotificationListeners,
+ registerExpoPushToken,
+} from "./src/services/PushNotificationService";
+
+// ๐จ ThemeProvider ์ถ๊ฐ
+import { ThemeProvider } from "./src/utils/ThemeContext";
+
+const SHOW_WELCOME_ON_LAUNCH = true;
+
+// ============================================
+// ์ ์ญ Notification Handler
+// ============================================
+console.log("[Push] setNotificationHandler: init");
+Notifications.setNotificationHandler({
+ handleNotification: async () => {
+ console.log("[Push] handleNotification called (foreground display enabled)");
+ return {
+ shouldShowBanner: true,
+ shouldShowList: true,
+ shouldPlaySound: true,
+ shouldSetBadge: true,
+ };
+ },
+});
export default function App() {
- const navigationRef = useRef();
- const cleanupRef = useRef();
+ const navigationRef = useRef(null);
+ const cleanupRef = useRef(null);
+ const localListeners = useRef({ received: null, response: null });
+ const shownWelcomeRef = useRef(false);
useEffect(() => {
+ let timer;
+
+ const initializeNotifications = async () => {
+ console.log("[Push] initializeNotifications: start");
+ console.log("[Push] Platform:", Platform.OS);
+
+ if (Platform.OS !== "ios") {
+ console.log("[Push] (skipped) iOS ์ ์ฉ ๋ก์ง. ํ์ฌ:", Platform.OS);
+ return;
+ }
+
+ if (!Device.isDevice) {
+ console.log("[Push][WARN] ์๋ฎฌ๋ ์ดํฐ๋ ์๊ฒฉ ํธ์ ์์ ๋ถ๊ฐ. ์ค๊ธฐ๊ธฐ ํ์.");
+ return;
+ }
- const initializeNotifications = () => {
- if (navigationRef.current) {
- console.log('expo ํธ์์๋ฆผ ๋ฆฌ์ค๋ ์ด๊ธฐํ');
- const cleanup = setupNotificationListeners(navigationRef.current);
- cleanupRef.current = cleanup;
-
- return cleanup;
+ // ===== ๊ถํ ํ์ธ =====
+ try {
+ const existing = await Notifications.getPermissionsAsync();
+ console.log("[Push] permissions(existing):", existing?.status, existing);
+ if (existing.status !== "granted") {
+ const { status } = await Notifications.requestPermissionsAsync({
+ ios: { allowAlert: true, allowSound: true, allowBadge: true },
+ });
+ console.log("[Push] permission after request:", status);
+ if (status !== "granted") {
+ console.warn("[Push] permission denied; skip token");
+ return;
+ }
+ }
+ } catch (e) {
+ console.log("[Push][ERR] getPermissionsAsync failed:", e?.message || e);
+ return;
}
+
+ // ===== ๋ฆฌ์ค๋ ๋ฑ๋ก =====
+ console.log("[Push] setupNotificationListeners() ํธ์ถ");
+ const serviceCleanup = setupNotificationListeners?.();
+ cleanupRef.current = serviceCleanup;
+
+ try {
+ if (!localListeners.current.received) {
+ localListeners.current.received =
+ Notifications.addNotificationReceivedListener((n) => {
+ try {
+ console.log(
+ "[Push][recv] (fg) notification:",
+ JSON.stringify(n, null, 2)
+ );
+ } catch {
+ console.log("[Push][recv] (fg) notification received");
+ }
+ });
+ }
+ if (!localListeners.current.response) {
+ localListeners.current.response =
+ Notifications.addNotificationResponseReceivedListener((r) => {
+ try {
+ console.log("[Push][tap] response:", JSON.stringify(r, null, 2));
+ } catch {
+ console.log("[Push][tap] notification tapped");
+ }
+ });
+ }
+ } catch (e) {
+ console.log("[Push][ERR] add listeners failed:", e?.message || e);
+ }
+
+ // ===== ํ ํฐ ๋ฐ๊ธ + ์๋ฒ ์
์ํธ =====
+ try {
+ console.log("[Push] registerExpoPushToken() ํธ์ถ");
+ const res = await registerExpoPushToken();
+ console.log("[Push] registerExpoPushToken() result:", res);
+
+ if (res?.success && res?.expoPushToken) {
+ console.log("โ
[Push] ExpoPushToken:", res.expoPushToken);
+ } else {
+ console.warn("โ [Push] Expo ํ ํฐ ๋ฐ๊ธ ์คํจ:", res?.error);
+ }
+ } catch (e) {
+ console.warn("[Push][ERR] registerExpoPushToken error:", e?.message || e);
+ }
+
+ // ===== ํ์ ๋ฐฐ๋ =====
+ if (SHOW_WELCOME_ON_LAUNCH && !shownWelcomeRef.current) {
+ shownWelcomeRef.current = true;
+ try {
+ const now = new Date();
+ const time = new Intl.DateTimeFormat("ko-KR", {
+ hour: "2-digit",
+ minute: "2-digit",
+ }).format(now);
+ await Notifications.scheduleNotificationAsync({
+ content: {
+ title: "๐ ํ์ํฉ๋๋ค!",
+ body: `${time} ์ ์ํ์ต๋๋ค.`,
+ data: { _meta: "welcome" },
+ sound: "default",
+ },
+ trigger: null,
+ });
+ } catch (e) {
+ console.log("[Push][ERR] schedule welcome failed:", e?.message || e);
+ }
+ }
+
+ console.log("[Push] initializeNotifications: done");
};
- const timer = setTimeout(initializeNotifications, 1000);
+ timer = setTimeout(initializeNotifications, 500);
return () => {
clearTimeout(timer);
- if (cleanupRef.current) {
- cleanupRef.current();
- }
+ try {
+ cleanupRef.current?.();
+ } catch {}
+ try {
+ if (localListeners.current.received) {
+ Notifications.removeNotificationSubscription(localListeners.current.received);
+ localListeners.current.received = null;
+ }
+ if (localListeners.current.response) {
+ Notifications.removeNotificationSubscription(localListeners.current.response);
+ localListeners.current.response = null;
+ }
+ } catch {}
+ console.log("[Push] cleanup completed");
};
}, []);
return (
-
-
-
+
+ {
+ navigationRef.current = r;
+ if (r) console.log("[Nav] navigationRef ready");
+ }}
+ >
+
+
+
);
}
\ No newline at end of file
diff --git a/app.config.js b/app.config.js
deleted file mode 100644
index bb00842..0000000
--- a/app.config.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default ({ config }) => {
- const baseConfig = {
- ...config,
- extra: {
- eas: {
- projectId: process.env.EAS_PROJECT_ID || "d831fa11-69a9-40eb-a916-ae0d22291e92"
- }
- },
- owner: process.env.EAS_OWNER || "r8ol7z"
- };
-
- return baseConfig;
- };
-
- // ์ผ๋จ ๊ณ์ ์ ๋ณด ๊ธฐ๋ณธ๊ฐ -> ์น์ฐ
\ No newline at end of file
diff --git a/app.json b/app.json
index a30f655..c244bd0 100644
--- a/app.json
+++ b/app.json
@@ -1,7 +1,8 @@
{
"expo": {
- "name": "Doodook",
+ "name": "๋๋",
"slug": "doodook",
+ "owner": "yehyeon",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./src/assets/images/logo-d-app-icon-1500.png",
@@ -18,7 +19,7 @@
],
"ios": {
"bundleIdentifier": "com.lipeuoo.doodook",
- "buildNumber": "7",
+ "buildNumber": "22",
"supportsTablet": true,
"infoPlist": {
"UIBackgroundModes": [
@@ -46,7 +47,11 @@
"color": "#F074BA"
}
]
- ]
-
+ ],
+ "extra": {
+ "eas": {
+ "projectId": "57b0a621-af5d-4605-b6e0-cc46a6c474ec"
+ }
+ }
}
}
diff --git a/package-lock.json b/package-lock.json
index 4d78ed6..9e346fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,14 +14,17 @@
"@react-navigation/stack": "^7.4.7",
"expo": "~53.0.0",
"expo-asset": "~11.1.7",
- "expo-constants": "^17.1.7",
+ "expo-constants": "*",
"expo-device": "~7.1.4",
"expo-font": "*",
+ "expo-haptics": "~14.1.4",
+ "expo-linear-gradient": "~14.1.5",
"expo-linking": "*",
- "expo-notifications": "^0.31.4",
+ "expo-notifications": "~0.31.4",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "*",
"expo-status-bar": "~2.2.3",
+ "lucide-react-native": "^0.545.0",
"punycode": "^2.3.1",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -32,7 +35,7 @@
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
- "react-native-svg": "15.11.2",
+ "react-native-svg": "^15.11.2",
"react-native-vector-icons": "^10.3.0",
"react-native-view-shot": "4.0.3"
},
@@ -481,12 +484,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
- "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.2"
+ "@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -856,9 +859,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
- "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz",
+ "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1095,16 +1098,16 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
- "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
+ "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.7",
- "@babel/traverse": "^7.28.0"
+ "@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -1288,9 +1291,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz",
- "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
+ "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1481,17 +1484,17 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
- "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.3",
+ "@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
- "@babel/types": "^7.28.2",
+ "@babel/types": "^7.28.4",
"debug": "^4.3.1"
},
"engines": {
@@ -1518,9 +1521,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
- "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1543,9 +1546,9 @@
}
},
"node_modules/@expo/cli": {
- "version": "0.24.20",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.20.tgz",
- "integrity": "sha512-uF1pOVcd+xizNtVTuZqNGzy7I6IJon5YMmQidsURds1Ww96AFDxrR/NEACqeATNAmY60m8wy1VZZpSg5zLNkpw==",
+ "version": "0.24.22",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.22.tgz",
+ "integrity": "sha512-cEg6/F8ZWjoVkEwm0rXKReWbsCUROFbLFBYht+d5RzHnDwJoTX4QWJKx4m+TGNDPamRUIGw36U4z41Fvev0XmA==",
"license": "MIT",
"dependencies": {
"@0no-co/graphql.web": "^1.0.8",
@@ -1561,11 +1564,12 @@
"@expo/osascript": "^2.2.5",
"@expo/package-manager": "^1.8.6",
"@expo/plist": "^0.3.5",
- "@expo/prebuild-config": "^9.0.11",
+ "@expo/prebuild-config": "^9.0.12",
+ "@expo/schema-utils": "^0.1.0",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
- "@react-native/dev-middleware": "0.79.5",
+ "@react-native/dev-middleware": "0.79.6",
"@urql/core": "^5.0.6",
"@urql/exchange-retry": "^1.3.0",
"accepts": "^1.3.8",
@@ -1614,6 +1618,55 @@
"expo-internal": "build/bin/cli"
}
},
+ "node_modules/@expo/cli/node_modules/@react-native/debugger-frontend": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.6.tgz",
+ "integrity": "sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@expo/cli/node_modules/@react-native/dev-middleware": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.6.tgz",
+ "integrity": "sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/ttlcache": "^1.4.1",
+ "@react-native/debugger-frontend": "0.79.6",
+ "chrome-launcher": "^0.15.2",
+ "chromium-edge-launcher": "^0.2.0",
+ "connect": "^3.6.5",
+ "debug": "^2.2.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "open": "^7.0.3",
+ "serve-static": "^1.16.2",
+ "ws": "^6.2.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@expo/cli/node_modules/@react-native/dev-middleware/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/@expo/cli/node_modules/@react-native/dev-middleware/node_modules/ws": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
+ "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
+ "license": "MIT",
+ "dependencies": {
+ "async-limiter": "~1.0.0"
+ }
+ },
"node_modules/@expo/cli/node_modules/ansi-regex": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
@@ -1715,6 +1768,12 @@
"node": ">=4"
}
},
+ "node_modules/@expo/cli/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/@expo/cli/node_modules/onetime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
@@ -1727,6 +1786,22 @@
"node": ">=4"
}
},
+ "node_modules/@expo/cli/node_modules/open": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+ "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@expo/cli/node_modules/ora": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz",
@@ -1772,9 +1847,9 @@
}
},
"node_modules/@expo/cli/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2064,9 +2139,9 @@
}
},
"node_modules/@expo/osascript": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.2.5.tgz",
- "integrity": "sha512-Bpp/n5rZ0UmpBOnl7Li3LtM7la0AR3H9NNesqL+ytW5UiqV/TbonYW3rDZY38u4u/lG7TnYflVIVQPD+iqZJ5w==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.7.tgz",
+ "integrity": "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.7.2",
@@ -2077,12 +2152,12 @@
}
},
"node_modules/@expo/package-manager": {
- "version": "1.8.6",
- "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.8.6.tgz",
- "integrity": "sha512-gcdICLuL+nHKZagPIDC5tX8UoDDB8vNA5/+SaQEqz8D+T2C4KrEJc2Vi1gPAlDnKif834QS6YluHWyxjk0yZlQ==",
+ "version": "1.9.8",
+ "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.8.tgz",
+ "integrity": "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA==",
"license": "MIT",
"dependencies": {
- "@expo/json-file": "^9.1.5",
+ "@expo/json-file": "^10.0.7",
"@expo/spawn-async": "^1.7.2",
"chalk": "^4.0.0",
"npm-package-arg": "^11.0.0",
@@ -2090,6 +2165,25 @@
"resolve-workspace-root": "^2.0.0"
}
},
+ "node_modules/@expo/package-manager/node_modules/@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/@expo/package-manager/node_modules/@expo/json-file": {
+ "version": "10.0.7",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz",
+ "integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "~7.10.4",
+ "json5": "^2.2.3"
+ }
+ },
"node_modules/@expo/package-manager/node_modules/ansi-regex": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
@@ -2283,9 +2377,9 @@
}
},
"node_modules/@expo/prebuild-config": {
- "version": "9.0.11",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.11.tgz",
- "integrity": "sha512-0DsxhhixRbCCvmYskBTq8czsU0YOBsntYURhWPNpkl0IPVpeP9haE5W4OwtHGzXEbmHdzaoDwNmVcWjS/mqbDw==",
+ "version": "9.0.12",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.12.tgz",
+ "integrity": "sha512-AKH5Scf+gEMgGxZZaimrJI2wlUJlRoqzDNn7/rkhZa5gUTnO4l6slKak2YdaH+nXlOWCNfAQWa76NnpQIfmv6Q==",
"license": "MIT",
"dependencies": {
"@expo/config": "~11.0.13",
@@ -2293,13 +2387,19 @@
"@expo/config-types": "^53.0.5",
"@expo/image-utils": "^0.7.6",
"@expo/json-file": "^9.1.5",
- "@react-native/normalize-colors": "0.79.5",
+ "@react-native/normalize-colors": "0.79.6",
"debug": "^4.3.1",
"resolve-from": "^5.0.0",
"semver": "^7.6.0",
"xml2js": "0.6.0"
}
},
+ "node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.6.tgz",
+ "integrity": "sha512-0v2/ruY7eeKun4BeKu+GcfO+SHBdl0LJn4ZFzTzjHdWES0Cn+ONqKljYaIv8p9MV2Hx/kcdEvbY4lWI34jC/mQ==",
+ "license": "MIT"
+ },
"node_modules/@expo/prebuild-config/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -2312,6 +2412,12 @@
"node": ">=10"
}
},
+ "node_modules/@expo/schema-utils": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.7.tgz",
+ "integrity": "sha512-jWHoSuwRb5ZczjahrychMJ3GWZu54jK9ulNdh1d4OzAEq672K9E5yOlnlBsfIHWHGzUAT+0CL7Yt1INiXTz68g==",
+ "license": "MIT"
+ },
"node_modules/@expo/sdk-runtime-versions": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz",
@@ -3013,22 +3119,86 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.79.5",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.5.tgz",
- "integrity": "sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w==",
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.6.tgz",
+ "integrity": "sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==",
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.3",
- "@react-native/codegen": "0.79.5"
+ "@react-native/codegen": "0.79.6"
},
"engines": {
"node": ">=18"
}
},
+ "node_modules/@react-native/babel-plugin-codegen/node_modules/@react-native/codegen": {
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.6.tgz",
+ "integrity": "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/parser": "^7.25.3",
+ "glob": "^7.1.1",
+ "hermes-parser": "0.25.1",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "yargs": "^17.6.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/@react-native/babel-plugin-codegen/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@react-native/babel-plugin-codegen/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@react-native/babel-plugin-codegen/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/@react-native/babel-preset": {
- "version": "0.79.5",
- "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.5.tgz",
- "integrity": "sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA==",
+ "version": "0.79.6",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.6.tgz",
+ "integrity": "sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -3072,7 +3242,7 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
- "@react-native/babel-plugin-codegen": "0.79.5",
+ "@react-native/babel-plugin-codegen": "0.79.6",
"babel-plugin-syntax-hermes-parser": "0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
@@ -3286,36 +3456,13 @@
"integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==",
"license": "MIT"
},
- "node_modules/@react-native/virtualized-lists": {
- "version": "0.79.5",
- "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz",
- "integrity": "sha512-EUPM2rfGNO4cbI3olAbhPkIt3q7MapwCwAJBzUfWlZ/pu0PRNOnMQ1IvaXTf3TpeozXV52K1OdprLEI/kI5eUA==",
- "license": "MIT",
- "dependencies": {
- "invariant": "^2.2.4",
- "nullthrows": "^1.1.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/react": "^19.0.0",
- "react": "*",
- "react-native": "*"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@react-navigation/bottom-tabs": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.6.tgz",
- "integrity": "sha512-f4khxwcL70O5aKfZFbxyBo5RnzPFnBNSXmrrT7q9CRmvN4mHov9KFKGQ3H4xD5sLonsTBtyjvyvPfyEC4G7f+g==",
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.7.tgz",
+ "integrity": "sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==",
"license": "MIT",
"dependencies": {
- "@react-navigation/elements": "^2.6.3",
+ "@react-navigation/elements": "^2.6.4",
"color": "^4.2.3"
},
"peerDependencies": {
@@ -3345,9 +3492,9 @@
}
},
"node_modules/@react-navigation/elements": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.3.tgz",
- "integrity": "sha512-hcPXssZg5bFD5oKX7FP0D9ZXinRgPUHkUJbTegpenSEUJcPooH1qzWJkEP22GrtO+OPDLYrCVZxEX8FcMrn4pA==",
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.4.tgz",
+ "integrity": "sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3",
@@ -3394,12 +3541,12 @@
}
},
"node_modules/@react-navigation/stack": {
- "version": "7.4.7",
- "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.4.7.tgz",
- "integrity": "sha512-1VDxuou+iZXEK7o+ZtCIU3b+upAwLFolptEKX+GldUy78GR66hltPxmu8bJeb2ZcgUSowBTHw03kNY1gyOdW3g==",
+ "version": "7.4.8",
+ "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.4.8.tgz",
+ "integrity": "sha512-zZsX52Nw1gsq33Hx4aNgGV2RmDJgVJM71udomCi3OdlntqXDguav3J2t5oe/Acf/9uU8JiJE9W8JGtoRZ6nXIg==",
"license": "MIT",
"dependencies": {
- "@react-navigation/elements": "^2.6.3",
+ "@react-navigation/elements": "^2.6.4",
"color": "^4.2.3"
},
"peerDependencies": {
@@ -4334,9 +4481,9 @@
}
},
"node_modules/babel-preset-expo": {
- "version": "13.2.3",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.3.tgz",
- "integrity": "sha512-wQJn92lqj8GKR7Ojg/aW4+GkqI6ZdDNTDyOqhhl7A9bAqk6t0ukUOWLDXQb4p0qKJjMDV1F6gNWasI2KUbuVTQ==",
+ "version": "13.2.4",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.4.tgz",
+ "integrity": "sha512-3IKORo3KR+4qtLdCkZNDj8KeA43oBn7RRQejFGWfiZgu/NeaRUSri8YwYjZqybm7hn3nmMv9OLahlvXBX23o5Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -4353,7 +4500,7 @@
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
- "@react-native/babel-preset": "0.79.5",
+ "@react-native/babel-preset": "0.79.6",
"babel-plugin-react-native-web": "~0.19.13",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
@@ -4427,6 +4574,15 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
+ "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/better-opn": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz",
@@ -4568,9 +4724,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.25.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
- "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
+ "version": "4.26.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"funding": [
{
"type": "opencollective",
@@ -4587,9 +4743,10 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001735",
- "electron-to-chromium": "^1.5.204",
- "node-releases": "^2.0.19",
+ "baseline-browser-mapping": "^2.8.9",
+ "caniuse-lite": "^1.0.30001746",
+ "electron-to-chromium": "^1.5.227",
+ "node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
"bin": {
@@ -4756,9 +4913,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001736",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz",
- "integrity": "sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==",
+ "version": "1.0.30001750",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
+ "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
"funding": [
{
"type": "opencollective",
@@ -5099,12 +5256,12 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
- "version": "3.45.1",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz",
- "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==",
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
+ "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==",
"license": "MIT",
"dependencies": {
- "browserslist": "^4.25.3"
+ "browserslist": "^4.26.3"
},
"funding": {
"type": "opencollective",
@@ -5277,9 +5434,9 @@
"license": "CC0-1.0"
},
"node_modules/dayjs": {
- "version": "1.11.13",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
- "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "version": "1.11.18",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
+ "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"devOptional": true,
"license": "MIT"
},
@@ -5555,9 +5712,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.208",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz",
- "integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==",
+ "version": "1.5.237",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
+ "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -5764,19 +5921,19 @@
}
},
"node_modules/expo": {
- "version": "53.0.20",
- "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.20.tgz",
- "integrity": "sha512-Nh+HIywVy9KxT/LtH08QcXqrxtUOA9BZhsXn3KCsAYA+kNb80M8VKN8/jfQF+I6CgeKyFKJoPNsWgI0y0VBGrA==",
+ "version": "53.0.23",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.23.tgz",
+ "integrity": "sha512-6TOLuNCP3AsSkXBJA5W6U/7wpZUop3Q6BxHMtRD2OOgT7CCPvnYgJdnTzqU+gD1hMfcryD8Ejq9RdHbLduXohg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "0.24.20",
+ "@expo/cli": "0.24.22",
"@expo/config": "~11.0.13",
"@expo/config-plugins": "~10.1.2",
"@expo/fingerprint": "0.13.4",
"@expo/metro-config": "0.20.17",
"@expo/vector-icons": "^14.0.0",
- "babel-preset-expo": "~13.2.3",
+ "babel-preset-expo": "~13.2.4",
"expo-asset": "~11.1.7",
"expo-constants": "~17.1.7",
"expo-file-system": "~18.1.11",
@@ -5884,6 +6041,15 @@
"react": "*"
}
},
+ "node_modules/expo-haptics": {
+ "version": "14.1.4",
+ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.1.4.tgz",
+ "integrity": "sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-keep-awake": {
"version": "14.1.4",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz",
@@ -5894,6 +6060,17 @@
"react": "*"
}
},
+ "node_modules/expo-linear-gradient": {
+ "version": "14.1.5",
+ "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
+ "integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-linking": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz",
@@ -7834,6 +8011,17 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react-native": {
+ "version": "0.545.0",
+ "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.545.0.tgz",
+ "integrity": "sha512-v9MC1CJ5jJkBbGvO+jrq9Iqe58pS8FvmdIO1NEr7mzQEx7E2VGfXkl1iaJsKpDSqofYhv4Xrks2I2ewF66HIDA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-native": "*",
+ "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
+ }
+ },
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -8449,9 +8637,9 @@
}
},
"node_modules/minizlib": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
- "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
@@ -8559,9 +8747,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.25",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz",
+ "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==",
"license": "MIT"
},
"node_modules/node-stream-zip": {
@@ -8603,9 +8791,9 @@
}
},
"node_modules/npm-package-arg/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -9736,6 +9924,29 @@
"react-native": "*"
}
},
+ "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
+ "version": "0.79.5",
+ "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz",
+ "integrity": "sha512-EUPM2rfGNO4cbI3olAbhPkIt3q7MapwCwAJBzUfWlZ/pu0PRNOnMQ1IvaXTf3TpeozXV52K1OdprLEI/kI5eUA==",
+ "license": "MIT",
+ "dependencies": {
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/react": "^19.0.0",
+ "react": "*",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-native/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -10991,37 +11202,21 @@
"license": "CC0-1.0"
},
"node_modules/tar": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
- "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
+ "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
- "minizlib": "^3.0.1",
- "mkdirp": "^3.0.1",
+ "minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
- "node_modules/tar/node_modules/mkdirp": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
- "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
- "license": "MIT",
- "bin": {
- "mkdirp": "dist/cjs/src/bin.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -11278,9 +11473,9 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "6.21.3",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
- "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
+ "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
diff --git a/package.json b/package.json
index ade42b9..8ed9687 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
- "android": "expo start --android",
- "ios": "expo start --ios",
+ "android": "expo run:android",
+ "ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
@@ -15,14 +15,17 @@
"@react-navigation/stack": "^7.4.7",
"expo": "~53.0.0",
"expo-asset": "~11.1.7",
- "expo-constants": "^17.1.7",
+ "expo-constants": "*",
"expo-device": "~7.1.4",
"expo-font": "*",
+ "expo-haptics": "~14.1.4",
+ "expo-linear-gradient": "~14.1.5",
"expo-linking": "*",
- "expo-notifications": "^0.31.4",
+ "expo-notifications": "~0.31.4",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "*",
"expo-status-bar": "~2.2.3",
+ "lucide-react-native": "^0.545.0",
"punycode": "^2.3.1",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -33,7 +36,7 @@
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
- "react-native-svg": "15.11.2",
+ "react-native-svg": "^15.11.2",
"react-native-vector-icons": "^10.3.0",
"react-native-view-shot": "4.0.3"
},
@@ -42,9 +45,5 @@
"@babel/core": "^7.28.3",
"@react-native-community/cli": "^20.0.1",
"react-native-svg-transformer": "^1.5.1"
- },
- "overrides": {
- "react": "$react",
- "react-dom": "$react-dom"
}
}
diff --git a/src/assets/fonts/Pretendard-Bold.otf b/src/assets/fonts/Pretendard-Bold.otf
new file mode 100644
index 0000000..8e5e30a
Binary files /dev/null and b/src/assets/fonts/Pretendard-Bold.otf differ
diff --git a/src/assets/fonts/Pretendard-Medium.otf b/src/assets/fonts/Pretendard-Medium.otf
new file mode 100644
index 0000000..0575069
Binary files /dev/null and b/src/assets/fonts/Pretendard-Medium.otf differ
diff --git a/src/assets/fonts/Pretendard-Regular.otf b/src/assets/fonts/Pretendard-Regular.otf
new file mode 100644
index 0000000..08bf4cf
Binary files /dev/null and b/src/assets/fonts/Pretendard-Regular.otf differ
diff --git a/src/assets/fonts/Pretendard-SemiBold.otf b/src/assets/fonts/Pretendard-SemiBold.otf
new file mode 100644
index 0000000..e7e36ab
Binary files /dev/null and b/src/assets/fonts/Pretendard-SemiBold.otf differ
diff --git a/src/assets/fonts/Pretendard-Thin.otf b/src/assets/fonts/Pretendard-Thin.otf
new file mode 100644
index 0000000..77e792d
Binary files /dev/null and b/src/assets/fonts/Pretendard-Thin.otf differ
diff --git a/src/assets/rainbow.png b/src/assets/rainbow.png
index 6136229..e4689cc 100644
Binary files a/src/assets/rainbow.png and b/src/assets/rainbow.png differ
diff --git a/src/components/LearningProgressBar.js b/src/components/LearningProgressBar.js
index 18c4c7c..d992b59 100644
--- a/src/components/LearningProgressBar.js
+++ b/src/components/LearningProgressBar.js
@@ -4,17 +4,27 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from '../utils/ThemeContext';
+
const LearningProgressBar = ({ current = 12, total = 20 }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const progress = Math.min(current / total, 1);
return (
- {/* */}
-
-
+
+
-
+
{current} / {total}
@@ -25,32 +35,26 @@ const styles = StyleSheet.create({
wrapper: {
flexDirection: 'row',
alignItems: 'center',
- //backgroundColor: '#5DB99610', // ์ ์ฒด ๋ฐฐ๊ฒฝ์
padding: 8,
borderRadius: 15,
marginHorizontal: 5,
},
-// icon: {
-// marginRight: 8,
-// },
progressContainer: {
flex: 1,
flexDirection: 'row',
- backgroundColor: '#5DB99630', // ๋จ์ ๋ถ๋ถ ์
borderRadius: 15,
overflow: 'hidden',
height: 12,
marginRight: 8,
},
progressFill: {
- backgroundColor: '#5DB996E0', // ์งํ๋ ๋ถ๋ถ ์
+ // ๋์ ์์ ์ ์ฉ๋จ
},
progressText: {
- color: '#fff',
fontWeight: 'bold',
minWidth: 50,
textAlign: 'right',
},
});
-export default LearningProgressBar;
+export default LearningProgressBar;
\ No newline at end of file
diff --git a/src/navigation/MainTab.js b/src/navigation/MainTab.js
index 8c7e4d0..cc676cb 100644
--- a/src/navigation/MainTab.js
+++ b/src/navigation/MainTab.js
@@ -1,58 +1,77 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Platform, Text } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import MainScreen from '../screens/Main/MainScreen';
import GuideScreen from '../screens/Guide/GuideScreen';
import ChatbotScreen from '../screens/Chatbot/ChatbotScreen';
import MyPageScreen from '../screens/MyPage/MyPageScreen';
-// SVG imports
-import HomeIcon from '../assets/icons/home.svg';
-import HomeSelectedIcon from '../assets/icons/home-selected.svg';
-import ChatbotIcon from '../assets/icons/chatbot.svg';
-import ChatbotSelectedIcon from '../assets/icons/chatbot-selected.svg';
-import MyPageIcon from '../assets/icons/mypage.svg';
-import MyPageSelectedIcon from '../assets/icons/mypage-selected.svg';
-import GuideIcon from '../assets/icons/guide.svg';
-import GuideSelectedIcon from '../assets/icons/guide-selected.svg';
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from '../utils/ThemeContext';
+
+// ๐ Lucide ์์ด์ฝ import
+import { Home, Pencil, MessageCircle, User } from 'lucide-react-native';
+
const Tab = createBottomTabNavigator();
const MainTab = () => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
console.log('MainTab ๋ํ๋ฌ์');
+ const insets = useSafeAreaInsets();
+
+ const getTabBarHeight = () => {
+ const baseHeight = 60;
+ const paddingBottom = Platform.OS === 'android' ?
+ Math.max(insets.bottom, 10) :
+ insets.bottom + 10;
+
+ return baseHeight + paddingBottom;
+ };
+
return (
({
+ screenOptions={{
headerShown: false,
tabBarStyle: {
- backgroundColor: '#003340',
+ backgroundColor: theme.background.primary,
borderTopColor: 'transparent',
- height: 65,
- paddingBottom: 15,
+ height: getTabBarHeight(),
+ paddingBottom: Platform.OS === 'android' ?
+ Math.max(insets.bottom, 10) :
+ insets.bottom + 10,
+ paddingTop: 8,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
elevation: 0,
- zIndex: 999,
+ shadowOpacity: 0,
},
- tabBarIcon: ({ focused, size }) => {
- console.log(`Tab pressed: ${route.name}, focused: ${focused}`);
- let Icon;
- if (route.name === 'Home') {
- Icon = focused ? HomeSelectedIcon : HomeIcon;
- } else if (route.name === 'Guide') {
- Icon = focused ? GuideSelectedIcon : GuideIcon;
- } else if (route.name === 'Chatbot') {
- Icon = focused ? ChatbotSelectedIcon : ChatbotIcon;
- } else {
- Icon = focused ? MyPageSelectedIcon : MyPageIcon;
- }
- return ;
+ tabBarActiveTintColor: theme.accent.primary,
+ tabBarInactiveTintColor: theme.accent.primary,
+ tabBarLabelStyle: {
+ fontSize: 11,
+ fontWeight: '600',
+ marginTop: 4,
},
- tabBarShowLabel: false,
- })}>
+ }}>
(
+
+ ),
+ }}
listeners={{
tabPress: e => {
console.log('Home tab pressed');
@@ -62,6 +81,17 @@ const MainTab = () => {
(
+
+ ),
+ }}
listeners={{
tabPress: e => {
console.log('Guide tab pressed');
@@ -71,6 +101,17 @@ const MainTab = () => {
(
+
+ ),
+ }}
listeners={{
tabPress: e => {
console.log('Chatbot tab pressed');
@@ -80,6 +121,17 @@ const MainTab = () => {
(
+
+ ),
+ }}
listeners={{
tabPress: e => {
console.log('MyPage tab pressed');
diff --git a/src/navigation/StackNavigator.js b/src/navigation/StackNavigator.js
index b0db2e7..77017ed 100644
--- a/src/navigation/StackNavigator.js
+++ b/src/navigation/StackNavigator.js
@@ -5,7 +5,6 @@ import SignUp1Screen from "../screens/Auth/SignUp1Screen";
import SignUp2Screen from "../screens/Auth/SignUp2Screen";
import SignUp3Screen from "../screens/Auth/SignUp3Screen";
import SignUp4Screen from "../screens/Auth/SignUp4Screen";
-import FindIdScreen from "../screens/Auth/FindIdScreen";
import FindPasswordScreen from "../screens/Auth/FindPasswordScreen";
import ResetPasswordScreen from "../screens/Auth/ResetPasswordScreen";
import SplashScreen from "../screens/Auth/SplashScreen";
@@ -25,7 +24,7 @@ import TypeResultScreen from "../screens/Guide/TypeResultScreen";
import TradingBuyScreen from "../screens/Main/TradingBuyScreen";
import TradingSellScreen from "../screens/Main/TradingSellScreen";
-import EditUserInfoScreen from "../screens/MyPage/EditUserInfoScreen";
+// import EditUserInfoScreen from "../screens/MyPage/EditUserInfoScreen";
import NoticeScreen from "../screens/MyPage/NoticeScreen";
import FAQScreen from "../screens/MyPage/FAQScreen";
import ChangePasswordScreen from "../screens/MyPage/ChangePasswordScreen";
@@ -33,6 +32,8 @@ import ChangePasswordScreen from "../screens/MyPage/ChangePasswordScreen";
import AssetDetailScreen from "../screens/Main/AssetDetailScreen";
import RouletteScreen from "../screens/MyPage/RouletteScreen";
+import ThemeSelectorScreen from '../screens/MyPage/ThemeSelectorScreen';
+
const Stack = createStackNavigator();
export default function StackNavigator() {
@@ -45,7 +46,6 @@ export default function StackNavigator() {
-
@@ -68,8 +68,15 @@ export default function StackNavigator() {
-
+ {/* */}
+
+
diff --git a/src/screens/Auth/FindIdScreen.js b/src/screens/Auth/FindIdScreen.js
deleted file mode 100644
index 235962b..0000000
--- a/src/screens/Auth/FindIdScreen.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import React, { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
-
-const FindIdScreen = ({ navigation }) => {
- const [phoneNumber, setPhoneNumber] = useState('');
- const [verificationCode, setVerificationCode] = useState('');
- const [isVerified, setIsVerified] = useState(false);
-
- const handleSendCode = () => {
- console.log(`Sending verification code to: ${phoneNumber}`);
- };
-
- const handleVerifyCode = () => {
- setIsVerified(true);
- };
-
- return (
-
- {/* ๐ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ */}
- navigation.goBack()} style={styles.backButton}>
- {'<'}
-
-
- {/* ๐ท ํ์ดํ */}
- ์ด๋ฉ์ผ ์ฐพ๊ธฐ
-
- {/* ๐ฑ ํด๋์ ํ ์
๋ ฅ */}
- ํด๋์ ํ ๋ฒํธ
-
-
-
- ์ ์ก
-
-
-
- {/* ๐ข ์ธ์ฆ๋ฒํธ ์
๋ ฅ */}
- ์ธ์ฆ๋ฒํธ ์
๋ ฅ
-
-
-
- ํ์ธ
-
-
-
- {/* โ
์ด๋ฉ์ผ ์ฐพ๊ธฐ ๋ฒํผ */}
-
- ์ด๋ฉ์ผ ์ฐพ๊ธฐ
-
-
- );
-};
-
-// โ
์คํ์ผ ์ ์
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#003340',
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: 30,
- },
-
- // ๐ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ ์คํ์ผ
- backButton: {
- position: 'absolute',
- top: 50,
- left: 20,
- zIndex: 10,
- },
- backText: {
- fontSize: 36,
- color: '#F074BA',
- },
-
- // ๐ท ํ์ดํ ์คํ์ผ
- title: {
- fontSize: 24,
- fontWeight: 'bold',
- color: '#F074BA',
- position: 'absolute',
- top: 150,
- left: 30,
- },
-
- // ๐ท ๋ผ๋ฒจ ์คํ์ผ
- label: {
- fontSize: 16,
- color: '#F074BA',
- alignSelf: 'flex-start',
- marginTop: 10,
- marginBottom: 10,
- },
-
- // ๐ฑ ์
๋ ฅ ํ๋ ์คํ์ผ
- inputContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- width: '100%',
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- backgroundColor: '#f9f9f9',
- marginBottom: 10,
- paddingHorizontal: 10,
- },
- input: {
- flex: 1,
- height: 50,
- fontSize: 16,
- color: 'black',
- },
-
- // ๐จ ์ ์ก ๋ฒํผ
- sendButton: {
- width: 60,
- height: 35,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#CCCDD0',
- borderRadius: 16,
- marginLeft: 10,
- },
- sendButtonText: {
- fontSize: 14,
- color: 'black',
- },
-
- // โ
์ธ์ฆ๋ฒํธ ํ์ธ ๋ฒํผ
- verifyButton: {
- width: 60,
- height: 35,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#CCCDD0',
- borderRadius: 16,
- marginLeft: 10,
- },
- verifyButtonText: {
- fontSize: 14,
- color: 'black',
- },
-
- // ๐ ์ด๋ฉ์ผ ์ฐพ๊ธฐ ๋ฒํผ
- findButton: {
- width: '100%',
- height: 50,
- backgroundColor: '#F074BA',
- borderRadius: 8,
- alignItems: 'center',
- justifyContent: 'center',
- marginTop: 40,
- },
-
- // ๐ซ ๋นํ์ฑํ๋ ๋ฒํผ ์คํ์ผ
- disabledButton: {
- backgroundColor: '#F8C7CC',
- },
-
- findButtonText: {
- color: '#fff',
- fontSize: 18,
- fontWeight: 'bold',
- },
-});
-
-export default FindIdScreen;
diff --git a/src/screens/Auth/FindPasswordScreen.js b/src/screens/Auth/FindPasswordScreen.js
index bc79ecf..18f54ab 100644
--- a/src/screens/Auth/FindPasswordScreen.js
+++ b/src/screens/Auth/FindPasswordScreen.js
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useMemo, useRef, useState, useEffect } from "react";
import {
View,
Text,
@@ -6,144 +6,514 @@ import {
TouchableOpacity,
Alert,
StyleSheet,
+ ScrollView,
+ Dimensions,
+ ActivityIndicator,
+ KeyboardAvoidingView,
+ Platform,
+ SafeAreaView,
+ Keyboard,
} from "react-native";
import { API_BASE_URL } from "../../utils/apiConfig";
+import EyeOpen from "../../components/EyeOpen";
+import EyeClosed from "../../components/EyeClosed";
+import { useTheme } from "../../utils/ThemeContext";
+
+const { height, width } = Dimensions.get("window");
const FindPasswordScreen = ({ navigation }) => {
+ const { theme } = useTheme();
+
+ const [step, setStep] = useState(1); // 1: ์ด๋ฉ์ผ ์
๋ ฅ, 2: ์ธ์ฆ๋ฒํธ + ์ ๋น๋ฐ๋ฒํธ
+
+ // ์ด๋ฉ์ผ ์
๋ ฅ ๋จ๊ณ
const [email, setEmail] = useState("");
- const [loading, setLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ // ์ธ์ฆ ๋จ๊ณ
+ const [code, setCode] = useState(["", "", "", "", "", ""]);
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [seeNewPassword, setSeeNewPassword] = useState(true);
+ const [seeConfirmPassword, setSeeConfirmPassword] = useState(true);
+ const [isVerifying, setIsVerifying] = useState(false);
+
+ // refs
+ const codeInputs = useRef([]);
+ const refNewPw = useRef(null);
+ const refConfirmPw = useRef(null);
+ const scrollRef = useRef(null);
+
+ // ํค๋ณด๋ ์ฒ๋ฆฌ
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
+
+ useEffect(() => {
+ const showSub = Keyboard.addListener("keyboardDidShow", (e) => {
+ setKeyboardVisible(true);
+ setKeyboardHeight(e?.endCoordinates?.height ?? 0);
+ });
+ const hideSub = Keyboard.addListener("keyboardDidHide", () => {
+ setKeyboardVisible(false);
+ setKeyboardHeight(0);
+ });
+ return () => {
+ showSub.remove();
+ hideSub.remove();
+ };
+ }, []);
- const handleSendCode = async () => {
- if (!email) {
- Alert.alert("์ค๋ฅ", "์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.");
+ const bottomSpacer = useMemo(() => {
+ if (!keyboardVisible) return 120;
+ return Math.max(220, keyboardHeight + 140);
+ }, [keyboardVisible, keyboardHeight]);
+
+ // validators
+ const validateEmail = (e) =>
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((e || "").trim().toLowerCase());
+
+ const passwordValid = (p) => {
+ const s = p || "";
+ if (s.length < 8 || s.length > 32) return false;
+ const kinds =
+ (/[A-Za-z]/.test(s) ? 1 : 0) + (/\d/.test(s) ? 1 : 0) + (/[^\w\s]/.test(s) ? 1 : 0);
+ return kinds >= 2;
+ };
+
+ // ๋น๋ฐ๋ฒํธ ๊ฐ๋(0~3)
+ const passwordStrength = useMemo(() => {
+ if (!newPassword) return 0;
+ const lenScore = newPassword.length >= 12 ? 1 : 0;
+ const kinds =
+ (/[A-Z]/.test(newPassword) ? 1 : 0) +
+ (/[a-z]/.test(newPassword) ? 1 : 0) +
+ (/\d/.test(newPassword) ? 1 : 0) +
+ (/[^\w\s]/.test(newPassword) ? 1 : 0);
+ if (newPassword.length >= 8 && kinds >= 2) {
+ if (lenScore && kinds >= 3) return 3; // ๊ฐ
+ return 2; // ๋ณดํต
+ }
+ return 1; // ์ฝ
+ }, [newPassword]);
+
+ const strengthText = ["", "์ฝํจ", "๋ณดํต", "๊ฐํจ"][passwordStrength];
+
+ // 1๋จ๊ณ: ์ด๋ฉ์ผ๋ก ์ธ์ฆ๋ฒํธ ์์ฒญ
+ const handleRequestCode = async () => {
+ if (isLoading) return;
+
+ if (!validateEmail(email)) {
+ Alert.alert("์ค๋ฅ", "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ ์
๋ ฅํด์ฃผ์ธ์.");
return;
}
- setLoading(true);
+ setIsLoading(true);
+
try {
- const response = await fetch(
- `${API_BASE_URL}users/password_reset/request/`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ email }),
- }
- );
-
- // ์๋ต ํ์
ํ์ธ
- const contentType = response.headers.get("content-type");
- if (!contentType || !contentType.includes("application/json")) {
- const textResponse = await response.text();
- console.error("์๋ฒ ์๋ต์ด JSON์ด ์๋:", textResponse);
- Alert.alert("์ค๋ฅ", "์๋ฒ ์๋ต ํ์ ์ค๋ฅ");
- setLoading(false);
- return;
+ const response = await fetch(`${API_BASE_URL}users/password_reset/request/`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ email: email.trim().toLowerCase(),
+ }),
+ });
+
+ const data = await response.json();
+ console.log("โ
์ธ์ฆ๋ฒํธ ์์ฒญ ์๋ต:", data);
+
+ if (response.status === 200 || data.message) {
+ Alert.alert(
+ "์ธ์ฆ๋ฒํธ ์ ์ก",
+ "์ด๋ฉ์ผ๋ก ์ธ์ฆ๋ฒํธ๊ฐ ์ ์ก๋์์ต๋๋ค.\n6์๋ฆฌ ์ธ์ฆ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.",
+ [{ text: "ํ์ธ", onPress: () => setStep(2) }]
+ );
+ } else {
+ Alert.alert("์ค๋ฅ", data.message || "์ธ์ฆ๋ฒํธ ์ ์ก์ ์คํจํ์ต๋๋ค.");
}
+ } catch (error) {
+ console.error("๐จ Network Error:", error);
+ Alert.alert("์ค๋ฅ", "๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // ์ธ์ฆ๋ฒํธ ์
๋ ฅ ์ฒ๋ฆฌ
+ const handleCodeChange = (text, index) => {
+ if (/^\d$/.test(text)) {
+ const newCode = [...code];
+ newCode[index] = text;
+ setCode(newCode);
+
+ if (index < 5) {
+ codeInputs.current[index + 1].focus();
+ } else {
+ // 6์๋ฆฌ ์
๋ ฅ ์๋ฃ
+ codeInputs.current[index].blur();
+ }
+ } else if (text === "") {
+ const newCode = [...code];
+ newCode[index] = "";
+ setCode(newCode);
+ }
+ };
+
+ // 2๋จ๊ณ: ์ธ์ฆ๋ฒํธ + ์ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
+ const handleResetPassword = async () => {
+ if (isVerifying) return;
+
+ const enteredCode = code.join("");
+
+ if (enteredCode.length !== 6) {
+ Alert.alert("์ค๋ฅ", "6์๋ฆฌ ์ธ์ฆ๋ฒํธ๋ฅผ ๋ชจ๋ ์
๋ ฅํด์ฃผ์ธ์.");
+ return;
+ }
+
+ if (!passwordValid(newPassword)) {
+ Alert.alert("์ค๋ฅ", "๋น๋ฐ๋ฒํธ๋ 8~32์์ด๋ฉฐ, ์๋ฌธ/์ซ์/ํน์ ์ค 2๊ฐ์ง ์ด์์ ํฌํจํด์ผ ํฉ๋๋ค.");
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ Alert.alert("์ค๋ฅ", "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
+ return;
+ }
+
+ setIsVerifying(true);
+
+ try {
+ const response = await fetch(`${API_BASE_URL}users/password_reset/verify/`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ email: email.trim().toLowerCase(),
+ code: enteredCode,
+ new_password: newPassword,
+ }),
+ });
const data = await response.json();
+ console.log("โ
๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ ์๋ต:", data);
- if (response.ok) {
+ if (response.status === 200 || data.status === "success") {
Alert.alert(
"์ฑ๊ณต",
- data.message || "๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋งํฌ๋ฅผ ์ด๋ฉ์ผ๋ก ๋ณด๋์ต๋๋ค.",
- [
- {
- text: "๋ค์",
- onPress: () =>
- navigation.navigate("ResetPassword", { email: email }),
- },
- ]
+ "๋น๋ฐ๋ฒํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค.\n์ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธํด์ฃผ์ธ์.",
+ [{ text: "ํ์ธ", onPress: () => navigation.navigate("Login") }]
);
} else {
- Alert.alert("์ค๋ฅ", data.message || "๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ์ ์คํจํ์ต๋๋ค.");
+ Alert.alert("์ค๋ฅ", data.message || "๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ ์คํจํ์ต๋๋ค.");
+ // ์ธ์ฆ๋ฒํธ ์ด๊ธฐํ
+ setCode(["", "", "", "", "", ""]);
+ if (codeInputs.current[0]) {
+ codeInputs.current[0].focus();
+ }
}
} catch (error) {
- console.error("๐จ Network Error:", error);
+ console.error("๐จ ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ ์ค๋ฅ:", error);
Alert.alert("์ค๋ฅ", "๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
} finally {
- setLoading(false);
+ setIsVerifying(false);
}
};
+ // ์ธ์ฆ๋ฒํธ ์ฌ์ ์ก
+ const handleResendCode = async () => {
+ setCode(["", "", "", "", "", ""]);
+ await handleRequestCode();
+ };
+
+ // ๋จ๊ณ๋ณ ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
+ const canSubmitStep1 = validateEmail(email);
+ const canSubmitStep2 =
+ code.join("").length === 6 &&
+ passwordValid(newPassword) &&
+ newPassword === confirmPassword;
+
return (
-
- {/* ๐ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ */}
- navigation.goBack()}
- style={styles.backButton}
+
+
- {"<"}
-
-
- {/* ๐ท ํ์ดํ */}
- ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
-
- {/* ๐ง ์ด๋ฉ์ผ ์
๋ ฅ */}
- ์ด๋ฉ์ผ
-
-
-
+ step === 1 ? navigation.goBack() : setStep(1)}
+ style={styles.backButton}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+ {"<"}
+
+ ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
+
+
+
+
-
- {loading ? "์ ์ก ์ค..." : "์ ์ก"}
-
-
-
-
-
- ๊ฐ์
ํ์ ์ด๋ฉ์ผ๋ก ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋งํฌ๊ฐ ๋ฐ์ก๋ฉ๋๋ค.
-
-
+ {step === 1 ? (
+ // 1๋จ๊ณ: ์ด๋ฉ์ผ ์
๋ ฅ
+ <>
+
+ ์ด๋ฉ์ผ ์ฃผ์ ์
๋ ฅ
+
+
+ ๊ฐ์
์ ์ฌ์ฉํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํ์๋ฉด{"\n"}
+ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ธ์ฆ๋ฒํธ๋ฅผ ๋ณด๋ด๋๋ฆฝ๋๋ค.
+
+
+ ์ด๋ฉ์ผ
+ 0 && !validateEmail(email)
+ ? theme.status.error
+ : theme.border.medium
+ }
+ ]}
+ placeholder="์ด๋ฉ์ผ ์
๋ ฅ"
+ placeholderTextColor={theme.text.tertiary}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ value={email}
+ onChangeText={(t) => setEmail((t || "").trimStart())}
+ returnKeyType="done"
+ onSubmitEditing={handleRequestCode}
+ />
+ {email.length > 0 && !validateEmail(email) && (
+
+ ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.
+
+ )}
+
+
+ >
+ ) : (
+ // 2๋จ๊ณ: ์ธ์ฆ๋ฒํธ + ์ ๋น๋ฐ๋ฒํธ
+ <>
+
+ ์ธ์ฆ๋ฒํธ ๋ฐ ์ ๋น๋ฐ๋ฒํธ ์
๋ ฅ
+
+
+ {email} ์ฃผ์๋ก ์ ์ก๋{"\n"}
+ ์ธ์ฆ๋ฒํธ 6์๋ฆฌ์ ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
+
+
+ {/* ์ธ์ฆ๋ฒํธ ์
๋ ฅ */}
+ ์ธ์ฆ๋ฒํธ
+
+ {code.map((digit, index) => (
+ (codeInputs.current[index] = ref)}
+ style={[
+ styles.codeInput,
+ {
+ backgroundColor: theme.background.card,
+ color: theme.text.primary,
+ borderColor: theme.border.medium
+ }
+ ]}
+ value={digit}
+ onChangeText={(text) => handleCodeChange(text, index)}
+ keyboardType="number-pad"
+ maxLength={1}
+ textAlign="center"
+ />
+ ))}
+
+
+
+
+ ์ธ์ฆ๋ฒํธ ๋ค์ ๋ณด๋ด๊ธฐ
+
+
+
+ {/* ์ ๋น๋ฐ๋ฒํธ */}
+ ์ ๋น๋ฐ๋ฒํธ
+ 0 && !passwordValid(newPassword)
+ ? theme.status.error
+ : theme.border.medium
+ }
+ ]}>
+ refConfirmPw.current && refConfirmPw.current.focus()}
+ />
+ setSeeNewPassword((v) => !v)} style={styles.icon}>
+ {seeNewPassword ? : }
+
+
+
+ {/* ๊ฐ๋ ํ์ */}
+ {newPassword.length > 0 && (
+
+ = 1 && { backgroundColor: theme.accent.primary }
+ ]} />
+ = 2 && { backgroundColor: theme.accent.primary }
+ ]} />
+ = 3 && { backgroundColor: theme.accent.primary }
+ ]} />
+
+ {strengthText}
+
+
+ )}
+ {newPassword.length > 0 && !passwordValid(newPassword) && (
+
+ ์๋ฌธ/์ซ์/ํน์ ์ค 2์ข
์ด์, 8~32์
+
+ )}
+
+ ์๋ฌธ ๋/์๋ฌธ์ยท์ซ์ยทํน์ ์ค 2๊ฐ์ง ์ด์, 8~32์
+
+
+ {/* ๋น๋ฐ๋ฒํธ ํ์ธ */}
+
+ ์ ๋น๋ฐ๋ฒํธ ํ์ธ
+
+ 0 && confirmPassword !== newPassword
+ ? theme.status.error
+ : theme.border.medium
+ }
+ ]}>
+
+ setSeeConfirmPassword((v) => !v)} style={styles.icon}>
+ {seeConfirmPassword ? : }
+
+
+ {confirmPassword.length > 0 && confirmPassword !== newPassword && (
+
+ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์์.
+
+ )}
+ {confirmPassword.length > 0 && newPassword === confirmPassword && (
+
+ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํฉ๋๋ค.
+
+ )}
+
+
+ >
+ )}
+
+
+ {/* ์ ์ถ ๋ฒํผ */}
+
+
+ {(step === 1 ? isLoading : isVerifying) ? (
+
+ ) : (
+
+ {step === 1 ? "์ธ์ฆ๋ฒํธ ์ ์ก" : "๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ"}
+
+ )}
+
+
+
+
);
};
-// โ
์คํ์ผ ์ ์
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#003340",
+ safe: { flex: 1 },
+ flex: { flex: 1 },
+
+ header: {
+ height: 56,
+ flexDirection: "row",
alignItems: "center",
- justifyContent: "center",
- paddingHorizontal: 30,
+ justifyContent: "space-between",
+ paddingHorizontal: 16,
},
+ backButton: { width: 36, height: 36, alignItems: "center", justifyContent: "center" },
+ backText: { fontSize: 28, marginTop: -2 },
+ title: { fontSize: 20, fontWeight: "bold" },
- backButton: {
- position: "absolute",
- top: 50,
- left: 20,
- zIndex: 10,
- },
- backText: {
- fontSize: 36,
- color: "#F074BA",
- },
+ scroll: { flex: 1 },
+ scrollContent: { paddingHorizontal: 24, paddingTop: 8, paddingBottom: 8 },
- title: {
- fontSize: 24,
+ stepTitle: {
+ fontSize: 22,
fontWeight: "bold",
- color: "#F074BA",
- position: "absolute",
- top: 150,
- left: 30,
+ textAlign: "center",
+ marginTop: 20,
+ marginBottom: 10,
+ },
+ stepDescription: {
+ fontSize: 15,
+ textAlign: "center",
+ marginBottom: 30,
+ lineHeight: 22,
},
- label: {
- fontSize: 16,
- color: "#F074BA",
- alignSelf: "flex-start",
- marginTop: 10,
+ label: { fontSize: 15, marginTop: 12, marginBottom: 8 },
+
+ input: {
+ width: "100%",
+ height: 50,
+ borderWidth: 1,
+ borderRadius: 10,
+ paddingHorizontal: 14,
marginBottom: 10,
+ fontSize: 16,
},
inputContainer: {
@@ -151,46 +521,64 @@ const styles = StyleSheet.create({
alignItems: "center",
width: "100%",
borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- backgroundColor: "#f9f9f9",
+ borderRadius: 10,
marginBottom: 10,
- paddingHorizontal: 10,
+ paddingHorizontal: 6,
},
+ inputField: { flex: 1, height: 50, fontSize: 16, paddingHorizontal: 8 },
+ icon: { padding: 10 },
- input: {
- flex: 1,
- height: 50,
- fontSize: 16,
- color: "black",
- },
-
- sendButton: {
- width: 60,
- height: 35,
- alignItems: "center",
+ // ์ธ์ฆ๋ฒํธ ์
๋ ฅ
+ codeContainer: {
+ flexDirection: "row",
justifyContent: "center",
- backgroundColor: "#CCCDD0",
- borderRadius: 16,
- marginLeft: 10,
+ gap: 8,
+ marginBottom: 20,
},
-
- disabledButton: {
- backgroundColor: "#A0A0A0",
+ codeInput: {
+ width: 45,
+ height: 50,
+ borderWidth: 1,
+ borderRadius: 8,
+ fontSize: 20,
+ textAlign: "center",
},
- sendButtonText: {
+ resendButton: {
+ alignSelf: "center",
+ marginBottom: 20,
+ },
+ resendText: {
fontSize: 14,
- color: "black",
+ textDecorationLine: "underline",
},
- infoText: {
- fontSize: 14,
- color: "#F074BA",
- textAlign: "center",
- marginTop: 20,
- opacity: 0.7,
+ errorText: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
+ passwordGuide: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
+ passwordMatch: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
+
+ // ๋น๋ฐ๋ฒํธ ๊ฐ๋
+ strengthRow: { flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: 2 },
+ strengthBar: { width: 32, height: 6, borderRadius: 4 },
+ strengthText: { fontSize: 12, marginLeft: 6 },
+
+ // ํธํฐ ๋ฒํผ
+ footer: {
+ position: "absolute",
+ left: 0,
+ right: 0,
+ bottom: 0,
+ paddingHorizontal: 24,
+ paddingTop: 8,
+ paddingBottom: 16,
+ },
+ button: {
+ height: 52,
+ borderRadius: 12,
+ alignItems: "center",
+ justifyContent: "center",
},
+ buttonText: { fontSize: 18, fontWeight: "bold" },
});
-export default FindPasswordScreen;
+export default FindPasswordScreen;
\ No newline at end of file
diff --git a/src/screens/Auth/LoginScreen.js b/src/screens/Auth/LoginScreen.js
index 7e353fa..490a797 100644
--- a/src/screens/Auth/LoginScreen.js
+++ b/src/screens/Auth/LoginScreen.js
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import {
StyleSheet,
Text,
@@ -6,7 +6,11 @@ import {
View,
TouchableOpacity,
Alert,
- Image,
+ KeyboardAvoidingView,
+ Platform,
+ Keyboard,
+ ScrollView,
+ Animated,
} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import EyeOpen from "../../components/EyeOpen";
@@ -14,13 +18,69 @@ import EyeClosed from "../../components/EyeClosed";
import { API_BASE_URL, API_ENDPOINTS } from "../../utils/apiConfig";
import { registerPushToken } from "../../services/PushNotificationService";
-
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
const LoginScreen = ({ navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const [seePassword, setSeePassword] = useState(true);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
+
+ const titleOpacity = useState(new Animated.Value(1))[0];
+ const titleTranslateY = useState(new Animated.Value(0))[0];
+
+ useEffect(() => {
+ const keyboardDidShowListener = Keyboard.addListener(
+ "keyboardDidShow",
+ handleKeyboardShow
+ );
+ const keyboardDidHideListener = Keyboard.addListener(
+ "keyboardDidHide",
+ handleKeyboardHide
+ );
+
+ return () => {
+ keyboardDidShowListener?.remove();
+ keyboardDidHideListener?.remove();
+ };
+ }, []);
+
+ const handleKeyboardShow = () => {
+ setKeyboardVisible(true);
+ Animated.parallel([
+ Animated.timing(titleOpacity, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.timing(titleTranslateY, {
+ toValue: -30,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
+
+ const handleKeyboardHide = () => {
+ setKeyboardVisible(false);
+ Animated.parallel([
+ Animated.timing(titleOpacity, {
+ toValue: 1,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.timing(titleTranslateY, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
const handleLogin = async () => {
if (!email || !password) {
@@ -67,13 +127,13 @@ const LoginScreen = ({ navigation }) => {
await AsyncStorage.setItem("accessToken", access);
await AsyncStorage.setItem("refreshToken", refresh);
await AsyncStorage.setItem("userEmail", email);
- await AsyncStorage.setItem("userPassword", password); // โ ์๋ ๋ก๊ทธ์ธ์ ์ํด password๋ ์ ์ฅ
+ await AsyncStorage.setItem("userPassword", password);
-
await AsyncStorage.setItem(
"hasCompletedTutorial",
has_completed_tutorial.toString()
);
+
try {
const pushTokenSuccess = await registerPushToken(navigation);
if (pushTokenSuccess) {
@@ -83,11 +143,8 @@ const LoginScreen = ({ navigation }) => {
}
} catch (pushError) {
console.error(" Push Token ๋ฑ๋ก ์ค ์ค๋ฅ:", pushError);
- // Push Token ๋ฑ๋ก ์คํจํด๋ ๋ก๊ทธ์ธ์ ๊ณ์ ์งํ
}
- // ํํ ๋ฆฌ์ผ ์๋ฃ ์ฌ๋ถ ์ ์ฅ
-
if (has_completed_tutorial) {
console.log("๐น ํํ ๋ฆฌ์ผ ์๋ฃ โ MainTab ์ด๋");
navigation.navigate("MainTab");
@@ -95,12 +152,8 @@ const LoginScreen = ({ navigation }) => {
console.log("๐น ํํ ๋ฆฌ์ผ ๋ฏธ์๋ฃ โ TutorialScreen ์ด๋");
navigation.navigate("TutorialScreen", { fromLogin: true });
}
-
- // console.log("๐น ๋ก๊ทธ์ธ ์ฑ๊ณต, MainTab์ผ๋ก ์ด๋ ์๋");
- // navigation.navigate("MainTab");
} else {
console.log("โ ๋ก๊ทธ์ธ ์คํจ:", data);
-
Alert.alert(
"์ค๋ฅ",
"๋ก๊ทธ์ธ ์ ๋ณด๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.\n๋ค์ ํ์ธํด ์ฃผ์ธ์."
@@ -115,175 +168,249 @@ const LoginScreen = ({ navigation }) => {
};
return (
-
- ๋ก๊ทธ์ธ
-
- ์ด๋ฉ์ผ
-
+
+
+
+
+ ๋ก๊ทธ์ธ
+
+
- ๋น๋ฐ๋ฒํธ
-
-
- setSeePassword(!seePassword)}
- style={styles.icon}
+
- {seePassword ? : }
-
-
+
+
+ ์ด๋ฉ์ผ
+
+
+
+
+
-
-
- {isLoading ? "๋ก๊ทธ์ธ ์ค..." : "๋ก๊ทธ์ธ"}
-
-
+
+
+ ๋น๋ฐ๋ฒํธ
+
+
+
+
+ setSeePassword(!seePassword)}
+ style={styles.eyeIcon}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+ {seePassword ? : }
+
+
+
+
-
- {/* navigation.navigate("FindId")}
- style={styles.findIdButton}
- >
- ์ด๋ฉ์ผ ์ฐพ๊ธฐ
- */}
+
+
+ {isLoading ? "๋ก๊ทธ์ธ ์ค..." : "๋ก๊ทธ์ธ"}
+
+
- navigation.navigate("FindPassword")}
- style={styles.findPasswordButton}
- >
- ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
-
+
+ navigation.navigate("FindPassword")}
+ style={styles.linkButton}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+ ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
+
+
- navigation.navigate("SignUp1")}
- style={styles.signUpButton}
- >
- ํ์๊ฐ์
-
-
-
+
+
+ navigation.navigate("SignUp1")}
+ style={styles.linkButton}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+ ํ์๊ฐ์
+
+
+
+
+
+
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
- alignItems: "center",
- justifyContent: "center",
+ },
+ scrollContainer: {
+ flexGrow: 1,
paddingHorizontal: 30,
},
+ titleContainer: {
+ paddingTop: 130,
+ paddingBottom: 50,
+ alignItems: "flex-start",
+ },
title: {
- fontSize: 24,
+ fontSize: 25,
fontWeight: "bold",
- color: "#F074BA",
- position: "absolute",
- top: 150,
- left: 30,
+ letterSpacing: 0.5,
+ },
+ inputSection: {
+ flex: 1,
+ justifyContent: "center",
+ paddingBottom: 60,
+ },
+ inputSectionKeyboard: {
+ justifyContent: "flex-start",
+ paddingTop: 20,
+ },
+ inputGroup: {
+ marginBottom: 24,
},
label: {
fontSize: 16,
- color: "#F074BA",
- alignSelf: "flex-start",
- marginTop: 10,
- marginBottom: 10,
+ marginBottom: 8,
+ fontWeight: "500",
+ },
+ inputWrapper: {
+ borderRadius: 12,
+ shadowColor: "#000",
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 3.84,
+ elevation: 5,
},
input: {
- width: "100%",
- height: 50,
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- paddingHorizontal: 15,
- marginBottom: 5,
+ height: 52,
+ paddingHorizontal: 16,
fontSize: 16,
- backgroundColor: "#f9f9f9",
- color: "black",
+ backgroundColor: "transparent",
},
- inputContainer: {
+ passwordContainer: {
flexDirection: "row",
alignItems: "center",
- width: "100%",
- borderWidth: 1,
- borderColor: "#ddd",
- borderRadius: 8,
- backgroundColor: "#f9f9f9",
- marginBottom: 15,
- paddingHorizontal: 10,
+ height: 52,
},
- inputField: {
+ passwordInput: {
flex: 1,
- height: 50,
+ paddingHorizontal: 16,
fontSize: 16,
- color: "black",
},
- icon: {
- padding: 10,
+ eyeIcon: {
+ paddingHorizontal: 16,
+ paddingVertical: 16,
},
- button: {
- width: "100%",
- height: 50,
- backgroundColor: "#F074BA",
- borderRadius: 8,
+ loginButton: {
+ height: 52,
+ borderRadius: 12,
alignItems: "center",
justifyContent: "center",
- position: "absolute",
- bottom: 80,
+ marginTop: 16,
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 8,
},
- buttonDisabled: {
+ loginButtonDisabled: {
backgroundColor: "#d3d3d3",
+ shadowOpacity: 0.1,
},
- buttonText: {
- color: "#fff",
+ loginButtonText: {
fontSize: 18,
- fontWeight: "bold",
+ fontWeight: "600",
+ letterSpacing: 0.5,
},
- buttonContainer: {
+ linkContainer: {
flexDirection: "row",
- marginTop: 10,
- paddingHorizontal: 10,
- },
- findIdButton: {
+ justifyContent: "center",
alignItems: "center",
- marginRight: 10,
- },
- findIdText: {
- color: "#EFF1F5",
- fontSize: 16,
+ marginTop: 24,
+ paddingBottom: 20,
},
- findPasswordButton: {
- alignItems: "flex-start",
- marginRight: 180,
+ linkButton: {
+ paddingVertical: 8,
+ paddingHorizontal: 12,
},
- findPasswordText: {
- color: "#EFF1F5",
+ linkText: {
fontSize: 16,
+ textDecorationLine: "underline",
},
- signUpButton: {
- alignItems: "center",
- },
- signUpText: {
- color: "#EFF1F5",
- fontSize: 16,
+ linkSeparator: {
+ width: 1,
+ height: 16,
+ marginHorizontal: 16,
+ opacity: 0.5,
},
});
-export default LoginScreen;
+export default LoginScreen;
\ No newline at end of file
diff --git a/src/screens/Auth/ResetPasswordScreen.js b/src/screens/Auth/ResetPasswordScreen.js
index 945a254..64b84f2 100644
--- a/src/screens/Auth/ResetPasswordScreen.js
+++ b/src/screens/Auth/ResetPasswordScreen.js
@@ -9,8 +9,11 @@ import {
ScrollView,
} from "react-native";
import { API_BASE_URL } from "../../utils/apiConfig";
+import { useTheme } from "../../utils/ThemeContext";
const ResetPasswordScreen = ({ route, navigation }) => {
+ const { theme } = useTheme();
+
// route.params๊ฐ ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
const { email = "" } = route?.params || {};
const [resetToken, setResetToken] = useState("");
@@ -87,42 +90,57 @@ const ResetPasswordScreen = ({ route, navigation }) => {
};
return (
-
-
+
+
{/* ๐ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ */}
navigation.goBack()}
style={styles.backButton}
>
- {"<"}
+ {"<"}
{/* ๐ท ํ์ดํ */}
- ๋น๋ฐ๋ฒํธ ์ฌ์ค์
+ ๋น๋ฐ๋ฒํธ ์ฌ์ค์
{/* ์ด๋ฉ์ผ ํ์ */}
- {email}
+ {email}
{/* ํ ํฐ ์
๋ ฅ */}
- ์ฌ์ค์ ํ ํฐ
-
+ ์ฌ์ค์ ํ ํฐ
+
{/* ์ ๋น๋ฐ๋ฒํธ ์
๋ ฅ */}
- ์ ๋น๋ฐ๋ฒํธ
-
+ ์ ๋น๋ฐ๋ฒํธ
+
{
{/* ๋น๋ฐ๋ฒํธ ํ์ธ ์
๋ ฅ */}
- ๋น๋ฐ๋ฒํธ ํ์ธ
-
+ ๋น๋ฐ๋ฒํธ ํ์ธ
+
{
{/* ๋ณ๊ฒฝ ๋ฒํผ */}
-
+
{loading ? "์ฒ๋ฆฌ ์ค..." : "๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ"}
- ์ด๋ฉ์ผ๋ก ๋ฐ์ ํ ํฐ์ ์
๋ ฅํ ํ,
- ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ค์ ํด ์ฃผ์ธ์.
+
+ ์ด๋ฉ์ผ๋ก ๋ฐ์ ํ ํฐ์ ์
๋ ฅํ ํ,
+
+
+ ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ค์ ํด ์ฃผ์ธ์.
+
);
};
-// โ
์คํ์ผ ์ ์
const styles = StyleSheet.create({
scrollContainer: {
flexGrow: 1,
},
container: {
flex: 1,
- backgroundColor: "#003340",
alignItems: "center",
paddingHorizontal: 30,
paddingBottom: 40,
@@ -182,13 +214,11 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 36,
- color: "#F074BA",
},
title: {
fontSize: 24,
fontWeight: "bold",
- color: "#F074BA",
position: "absolute",
top: 150,
left: 30,
@@ -202,14 +232,12 @@ const styles = StyleSheet.create({
emailText: {
fontSize: 18,
- color: "#fff",
marginBottom: 20,
- alignSelf: "left",
+ alignSelf: "flex-start",
},
label: {
fontSize: 16,
- color: "#F074BA",
alignSelf: "flex-start",
marginTop: 15,
marginBottom: 10,
@@ -218,9 +246,7 @@ const styles = StyleSheet.create({
inputContainer: {
width: "100%",
borderWidth: 1,
- borderColor: "#ddd",
borderRadius: 8,
- backgroundColor: "#f9f9f9",
marginBottom: 10,
paddingHorizontal: 10,
},
@@ -228,7 +254,6 @@ const styles = StyleSheet.create({
input: {
height: 50,
fontSize: 16,
- color: "black",
},
resetButton: {
@@ -236,30 +261,26 @@ const styles = StyleSheet.create({
height: 50,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#F074BA",
borderRadius: 8,
marginTop: 30,
marginBottom: 50,
- },
-
- disabledButton: {
- backgroundColor: "#A0A0A0",
+ elevation: 4,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
},
resetButtonText: {
fontSize: 16,
fontWeight: "bold",
- color: "white",
},
infoText: {
fontSize: 14,
- color: "#F074BA",
textAlign: "center",
- // marginTop: 20,
opacity: 0.7,
fontWeight: "bold",
},
});
-export default ResetPasswordScreen;
+export default ResetPasswordScreen;
\ No newline at end of file
diff --git a/src/screens/Auth/SignUp1Screen.js b/src/screens/Auth/SignUp1Screen.js
index f4f980b..61b0802 100644
--- a/src/screens/Auth/SignUp1Screen.js
+++ b/src/screens/Auth/SignUp1Screen.js
@@ -2,91 +2,256 @@ import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
import CheckBoxChecked from '../../components/CheckBoxChecked';
import CheckBoxUnchecked from '../../components/CheckBoxUnchecked';
+import { useTheme } from "../../utils/ThemeContext";
const SignUp1Screen = ({ navigation }) => {
+ const { theme } = useTheme();
+
const [agreements, setAgreements] = useState({
all: false,
required1: false,
- required2: false,
- required3: false,
- required4: false,
- required5: false,
required6: false,
- optional1: false,
optional2: false,
});
+ const [expandedStates, setExpandedStates] = useState({
+ required1: false,
+ required6: false,
+ optional2: false,
+ });
+
+ const termsData = {
+ required1: {
+ title: '[ํ์] ๋๋ ์ด์ฉ ์ฝ๊ด',
+ content: `**์ 1์กฐ (๋ชฉ์ )**
+์ด ์ฝ๊ด์ ํ๋ญ๋๋ค(์ดํ "ํ์ฌ")๊ฐ ์ ๊ณตํ๋ '๋๋(Doodook)' ์๋น์ค์ ์ด์ฉ์กฐ๊ฑด ๋ฐ ์ ์ฐจ, ํ์ฌ์ ํ์ ๊ฐ์ ๊ถ๋ฆฌยท์๋ฌด ๋ฐ ์ฑ
์์ฌํญ์ ๊ท์ ํจ์ ๋ชฉ์ ์ผ๋ก ํฉ๋๋ค.
+
+**์ 2์กฐ (์ ์)**
+"์๋น์ค"๋ ์ฌ์ฉ์๊ฐ ๊ฐ์์ ์์ฐ์ ํ์ฉํด ํฌ์ ์๋ฎฌ๋ ์ด์
์ ๊ฒฝํํ๊ณ , ๊ธ์ตํ์ต ๋ฐ ํฌํธํด๋ฆฌ์ค ๋ถ์ ๊ธฐ๋ฅ์ ์ด์ฉํ ์ ์๋ ํ๋ซํผ์ ์๋ฏธํฉ๋๋ค.
+"ํ์"์ด๋ ๋ณธ ์ฝ๊ด์ ๋์ํ๊ณ ์๋น์ค์ ๊ฐ์
ํ ์๋ฅผ ์๋ฏธํฉ๋๋ค.
+"AI ์ฑ๋ด"์ด๋ OpenAI ๊ธฐ๋ฐ ํฌ์ ์ ๋ณด ์๋ต ๊ธฐ๋ฅ์ ๋งํ๋ฉฐ, ์ ๋ณด ์ ๊ณต ๋ชฉ์ ์ ํ์ ๋ฉ๋๋ค.
+
+**์ 3์กฐ (์ฝ๊ด์ ํจ๋ ฅ ๋ฐ ๋ณ๊ฒฝ)**
+๋ณธ ์ฝ๊ด์ ์๋น์ค ์ด๊ธฐํ๋ฉด ๋๋ ์ค์ ํ๋ฉด ๋ฑ์ ๊ฒ์ํ์ฌ ๊ณต์งํจ์ผ๋ก์จ ํจ๋ ฅ์ ๋ฐ์ํฉ๋๋ค.
+ํ์ฌ๋ ํ์ ์ ๊ด๋ จ ๋ฒ๋ น์ ์๋ฐฐํ์ง ์๋ ๋ฒ์์์ ์ฝ๊ด์ ๊ฐ์ ํ ์ ์์ผ๋ฉฐ, ๋ณ๊ฒฝ ์ ์ฌ์ ๊ณ ์งํฉ๋๋ค.
+
+**์ 4์กฐ (ํ์๊ฐ์
๋ฐ ์ด์ฉ ๊ณ์ฝ ์ฒด๊ฒฐ)**
+ํ์๊ฐ์
์ ๋ง 14์ธ ์ด์์ธ ์์ ํํด ๊ฐ๋ฅํฉ๋๋ค.
+์ฌ์ฉ์๋ ๋ณธ์ธ์ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ํตํด ๊ณ์ ์ ์์ฑํด์ผ ํ๋ฉฐ, ํ์ ์ ๋ณด ์
๋ ฅ ์ ์ด์ฉ์ด ์ ํ๋ ์ ์์ต๋๋ค.
+ํ์๊ฐ์
์ ์์คํ
์์ ์ ๊ณตํ๋ ์ธ์ฆ ์ ์ฐจ(์ด๋ฉ์ผ ํ ํฐ, ์ฝ๋ ์ธ์ฆ ๋ฑ)๋ฅผ ์๋ฃํด์ผ ํฉ๋๋ค.
+
+**์ 5์กฐ (์๋น์ค์ ์ ๊ณต ๋ฐ ๊ธฐ๋ฅ)**
+ํ์ฌ๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
+- ๊ฐ์์ ๊ธ์ก์ผ๋ก ๋งค์/๋งค๋ ๊ฐ๋ฅํ ํฌ์ ์๋ฎฌ๋ ์ด์
๊ธฐ๋ฅ
+- ๊ฐ์ ํฌํธํด๋ฆฌ์ค ๊ด๋ฆฌ ๋ฐ ์์ต๋ฅ ์กฐํ
+- ๊ด์ฌ ์ข
๋ชฉ ์ ์ฅ ๋ฐ ์ข
๋ชฉ ๊ฒ์ ๊ธฐ๋ฅ
+- ํฌ์ ์ฑํฅ ๋ถ์ ํ
์คํธ
+- AI ์ฑ๋ด์ ํตํ ํฌ์ ์ ๋ณด ์๋ด (OpenAI ๊ธฐ๋ฐ)
+- ํ์ต ๊ฐ์ด๋ ๊ธฐ๋ฅ ๋ฐ ๋จ๊ณ๋ณ ์ฝํ
์ธ
+- ์ฃผ๊ฐ ๋ณ๋ ๋ฑ ์ฃผ์ ์ด๋ฒคํธ์ ๋ํ ํธ์ ์๋ฆผ ์ ๊ณต
+- ์ธ๋ถ API ์ฐ๋: ํ๊ตญํฌ์์ฆ๊ถ API, OpenAI API ๋ฑ
+
+**์ 6์กฐ (์๋น์ค ์ด์ฉ ์กฐ๊ฑด)**
+์๋น์ค๋ ๋ฌด๋ฃ๋ก ์ ๊ณต๋๋ฉฐ, ์ ๋ฃ ๊ธฐ๋ฅ์ ํ์ฌ ์ ๊ณต๋์ง ์์ต๋๋ค.
+ํฌ์ ์๋ฎฌ๋ ์ด์
์ ์ค์ ์ฃผ์ ํฌ์๋ ๊ธ์ต ์์ฐ ์ด์ฉ๊ณผ๋ ๋ฌด๊ดํ๋ฉฐ, ์ ๋ณด ์ ๊ณต ๋ฐ ํ์ต ๋ชฉ์ ์ผ๋ก๋ง ํ์ฉ๋์ด์ผ ํฉ๋๋ค.
+AI ์ฑ๋ด์ ์๋ต์ ์ฐธ๊ณ ์ฉ์ผ๋ก๋ง ์ ๊ณต๋๋ฉฐ, ํน์ ์ข
๋ชฉ์ ๋ํ ํฌ์ ๊ถ์ ๋๋ ๊ธ์ต ์๋ฌธ์ ํด๋นํ์ง ์์ต๋๋ค.
+
+**์ 7์กฐ (ํ์์ ์๋ฌด)**
+ํ์์ ์๋น์ค ์ด์ฉ ์ ๊ด๊ณ ๋ฒ๋ น, ๋ณธ ์ฝ๊ด์ ๊ท์ , ํ์ฌ์ ์๋ด์ฌํญ ๋ฑ์ ์ค์ํด์ผ ํฉ๋๋ค.
+ํ์์ ๋ณธ์ธ์ ๊ณ์ ์ ์ 3์์๊ฒ ์๋ํ๊ฑฐ๋ ๊ณต์ ํ ์ ์์ต๋๋ค.
+์๋น์ค์ ๊ธฐ๋ฅ์ ๋ถ์ ํ๊ฒ ์ด์ฉํ๊ฑฐ๋, ์์คํ
์ ๋น์ ์์ ์ผ๋ก ์ ๊ทผยท๋ณ์กฐํ๋ ํ์๋ ๊ธ์ง๋ฉ๋๋ค.
+
+**์ 8์กฐ (๊ฐ์ธ์ ๋ณด์ ์์ง ๋ฐ ๋ณดํธ)**
+ํ์ฌ๋ ํ์๊ฐ์
๋ฐ ์๋น์ค ์ ๊ณต์ ํ์ํ ์ต์ํ์ ๊ฐ์ธ์ ๋ณด๋ฅผ ์์งํฉ๋๋ค.
+์์ง ํญ๋ชฉ: ์ด๋ฉ์ผ, ๋๋ค์, ์๋
์์ผ, ์ฑ๋ณ, ์ฃผ์, ์๊ณ , ํํ ๋ฆฌ์ผ ์งํ ์ฌ๋ถ ๋ฑ
+๊ฐ์ธ์ ๋ณด๋ ์ํธํ ๋ฐ ๋ณด์ ์กฐ์น๋ฅผ ํตํด ์์ ํ๊ฒ ๊ด๋ฆฌ๋๋ฉฐ, ์์ธํ ๋ด์ฉ์ ๋ณ๋์ [๊ฐ์ธ์ ๋ณด์ฒ๋ฆฌ๋ฐฉ์นจ]์ ๋ฐ๋ฆ
๋๋ค.
+
+**์ 9์กฐ (๊ณ์ ํด์ง ๋ฐ ๋ฐ์ดํฐ ์ญ์ )**
+ํ์์ ์ธ์ ๋ ์ง ์ฑ ๋ด ์ ๊ณต๋๋ ํํด ๊ธฐ๋ฅ์ ํตํด ๊ณ์ ์ ์ญ์ ํ ์ ์์ต๋๋ค.
+ํ์ ํํด ์ ํด๋น ๊ณ์ ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ ์ฆ์ ์ญ์ ๋ฉ๋๋ค. ๋จ, ๊ด๋ จ ๋ฒ๋ น์ ๋ฐ๋ฅธ ์์ธ ๋ณด๊ด ํญ๋ชฉ์ ์ ์ธํฉ๋๋ค.
+
+**์ 10์กฐ (์๋น์ค ๋ณ๊ฒฝ ๋ฐ ์ข
๋ฃ)**
+ํ์ฌ๋ ๊ธฐ๋ฅ ๊ฐ์ , ์์คํ
์ ์ง๋ณด์, ์ธ๋ถ ์ ์ฑ
๋ณ๊ฒฝ ๋ฑ์ ๋ฐ๋ผ ์๋น์ค์ ์ผ๋ถ ๋๋ ์ ๋ถ๋ฅผ ๋ณ๊ฒฝยท์ค๋จํ ์ ์์ต๋๋ค.
+์๋น์ค ์ข
๋ฃ ์ ์ฌ์ ๊ณ ์งํ๋ฉฐ, ํ์ ๋ฐ์ดํฐ๋ ๊ด๋ จ ๋ฒ๋ น์ ๋ฐ๋ผ ์์ ํ๊ฒ ์ฒ๋ฆฌ๋ฉ๋๋ค.
+
+**์ 11์กฐ (์ฑ
์์ ํ๊ณ)**
+ํ์ฌ๋ ํ์์ด ์๋น์ค ๋ด ์ ๊ณต๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ค์ ํฌ์ ๊ฒฐ์ ์ ๋ด๋ฆฐ ๊ฒฝ์ฐ, ๊ทธ์ ๋ฐ๋ฅธ ์์ค์ ๋ํด ์ฑ
์์ง์ง ์์ต๋๋ค.
+์ธ๋ถ API(ํ๊ตญํฌ์์ฆ๊ถ, OpenAI ๋ฑ)๋ก๋ถํฐ ์ ๊ณต๋๋ ๋ฐ์ดํฐ์ ์ ํ์ฑ ๋ฐ ์ค์๊ฐ์ฑ์ ๋ณด์ฅ๋์ง ์์ ์ ์์ต๋๋ค.
+
+**์ 12์กฐ (์ง์์ฌ์ฐ๊ถ)**
+์๋น์ค ๋ด ์ฝํ
์ธ , ์์ค์ฝ๋, UI ๋ฑ ๋ชจ๋ ์ ์๊ถ์ ํ์ฌ์ ๊ท์๋ฉ๋๋ค.
+ํ์์ ํ์ฌ์ ๋ช
์์ ํ๋ฝ ์์ด ์ฝํ
์ธ ๋ฅผ ๋ณต์ , ์ ์ก, ๋ฐฐํฌ, ๊ฐ๊ณต, ํ๋งคํ ์ ์์ต๋๋ค.
+
+**์ 13์กฐ (๋ถ์ ํด๊ฒฐ ๋ฐ ์ค๊ฑฐ๋ฒ)**
+ํ์ฌ์ ํ์ ๊ฐ ๋ฐ์ํ ๋ถ์์ ๋ํด ์๋งํ ํด๊ฒฐ์ ์ํด ์ฑ์คํ ํ์ํฉ๋๋ค.
+๋ถ์์ด ํด๊ฒฐ๋์ง ์์ ๊ฒฝ์ฐ, ๋ํ๋ฏผ๊ตญ ๋ฒ๋ น์ ์ ์ฉํ๋ฉฐ, ๋ฏผ์ฌ์์ก์ ๊ดํ ๋ฒ์์ ์์ธ์ค์์ง๋ฐฉ๋ฒ์์ผ๋ก ํฉ๋๋ค.
+
+**๋ถ์น**
+๋ณธ ์ฝ๊ด์ 2025๋
9์ 30์ผ๋ถํฐ ์ํ๋ฉ๋๋ค.`
+ },
+ required6: {
+ title: '[ํ์] ๋ง 14์ธ ์ด์์
๋๋ค.',
+ content: `๋ณธ ์๋น์ค๋ ๋ง 14์ธ ์ด์์ ์ฌ์ฉ์๋ง ์ด์ฉํ ์ ์์ต๋๋ค.
+
+**ํ์ธ ์ฌํญ**
+- ๋ง 14์ธ ๋ฏธ๋ง์ ๊ฒฝ์ฐ ๋ฒ์ ๋๋ฆฌ์ธ์ ๋์๊ฐ ํ์ํฉ๋๋ค.
+- ํ์ ์ฐ๋ น ์ ๋ณด ์
๋ ฅ ์ ์๋น์ค ์ด์ฉ์ด ์ ํ๋ ์ ์์ต๋๋ค.
+
+**๊ด๋ จ ๋ฒ๋ น**
+ใ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฒใ, ใ์ ๋ณดํต์ ๋ง ์ด์ฉ์ด์ง ๋ฐ ์ ๋ณด๋ณดํธ ๋ฑ์ ๊ดํ ๋ฒ๋ฅ ใ์ ๋ฐ๋ฅธ ๋ง 14์ธ ๋ฏธ๋ง ์๋์ ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ ์ ํ ๊ท์ ์ ์ค์ํฉ๋๋ค.`
+ },
+ optional2: {
+ title: '[์ ํ] ๊ด๊ณ ์ฑ ์ ๋ณด ์์ ๋์',
+ content: `**๊ด๊ณ ์ฑ ์ ๋ณด ์์ ๋ชฉ์ **
+๋๋ ์๋น์ค์ ์๋ก์ด ๊ธฐ๋ฅ์ด๋ ๊ณต์ง์ฌํญ ์
๋ฐ์ดํธ ๋ฑ์ ์ ๋ณด๋ฅผ Push ์๋ฆผ์ผ๋ก ๋ณด๋ด๋๋ฆฝ๋๋ค.
+
+**์์ ๋ฐฉ๋ฒ**
+- ์ฑ ํธ์ ์๋ฆผ
+
+**์์ ๊ฑฐ๋ถ**
+- ๊ธฐ๊ธฐ ๋ด ์ค์ > ๋๋ ์ฑ ๊ถํ ์ค์ ์์ Push ์๋ฆผ์ ํ์ฉํ์ง ์์ต๋๋ค.
+
+**๋์ ๊ฑฐ๋ถ ์ ๋ถ์ด์ต**
+๊ด๊ณ ์ฑ ์ ๋ณด ์์ ์ ๋์ํ์ง ์์๋ ๋๋ ์๋น์ค์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ ์ด์ฉ์๋ ์ ํ ์ํฅ์ด ์์ต๋๋ค.`
+ }
+ };
+
const toggleAll = () => {
const newState = !agreements.all;
setAgreements({
all: newState,
required1: newState,
- required2: newState,
- required3: newState,
- required4: newState,
- required5: newState,
required6: newState,
- optional1: newState,
optional2: newState,
});
+ setExpandedStates({
+ required1: false,
+ required6: false,
+ optional2: false,
+ });
+ };
+
+ const toggleExpanded = (key) => {
+ setExpandedStates(prev => ({
+ ...prev,
+ [key]: !prev[key]
+ }));
};
- const toggleItem = (key) => {
+ const toggleAgreement = (key) => {
setAgreements((prevAgreements) => {
const newAgreements = { ...prevAgreements, [key]: !prevAgreements[key] };
newAgreements.all =
newAgreements.required1 &&
- newAgreements.required2 &&
- newAgreements.required3 &&
- newAgreements.required4 &&
- newAgreements.required5 &&
newAgreements.required6 &&
- newAgreements.optional1 &&
newAgreements.optional2;
return newAgreements;
});
};
+
+ const handleAgreeAndCollapse = (key) => {
+ if (!agreements[key]) {
+ toggleAgreement(key);
+ }
+ setExpandedStates(prev => ({ ...prev, [key]: false }));
+ };
+
+ const handleCheckboxPress = (key) => {
+ toggleAgreement(key);
+ setExpandedStates(prev => ({ ...prev, [key]: false }));
+ };
+
+ const renderFormattedContent = (content) => {
+ const parts = content.split('**');
+ return (
+
+ {parts.map((part, index) =>
+ index % 2 === 1 ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ );
+ };
+
+ const renderTermsItem = (key) => {
+ const term = termsData[key];
+ const isExpanded = expandedStates[key];
+ const isChecked = agreements[key];
+
+ return (
+
+
+ handleCheckboxPress(key)}>
+ {isChecked ? : }
+
+ toggleExpanded(key)} style={styles.titleContainer}>
+ {term.title}
+
+ {isExpanded ? 'โฒ' : 'โผ'}
+
+
+
+
+ {isExpanded && (
+
+
+ {renderFormattedContent(term.content)}
+
+ handleAgreeAndCollapse(key)}
+ >
+ ๋์ํ๊ธฐ
+
+
+ )}
+
+ );
+ };
+
+ const allRequiredAgreed = agreements.required1 && agreements.required6;
return (
-
+
navigation.goBack()} style={styles.backButton}>
- {'<'}
+ {'<'}
- ์ด์ฉ ์ฝ๊ด์{"\n"}๋์ํด ์ฃผ์ธ์
-
+
+ ์ด์ฉ ์ฝ๊ด์{"\n"}๋์ํด ์ฃผ์ธ์
+
+
{agreements.all ? : }
- ์ ์ฒด ๋์
+ ์ ์ฒด ๋์
+
- {[
- { key: 'required1', label: '[ํ์] ๋๋
์ด์ฉ ์ฝ๊ด' },
- { key: 'required2', label: '[ํ์] ๊ฐ์ธ์ ๋ณด ์์งยท์ด์ฉ ๋์' },
- { key: 'required3', label: '[ํ์] ๋ฏผ๊ฐ์ ๋ณด ์์งยท์ด์ฉ ๋์' },
- { key: 'required4', label: '[ํ์] ๊ฐ์ธ์ ๋ณด ์ 3์ ์ ๊ณต ๋์' },
- { key: 'required5', label: '[ํ์] ๊ฐ์ธ์ ๋ณด ๊ตญ์ธ ์ด์ ๋์' },
- { key: 'required6', label: '[ํ์] ๋ง 14์ธ ์ด์์
๋๋ค.' },
- { key: 'optional1', label: '[์ ํ] ๋ง์ผํ
ํ์ฉ ๋์' },
- { key: 'optional2', label: '[์ ํ] ๊ด๊ณ ์ฑ ์ ๋ณด ์์ ๋์' },
- ].map((item) => (
- toggleItem(item.key)} style={styles.agreeItem}>
- {agreements[item.key] ? : }
- {item.label}
-
- ))}
+ {Object.keys(termsData).map(key => renderTermsItem(key))}
+
navigation.navigate('SignUp2')}
>
- ๋์ํ๊ธฐ
+ ๋์ํ๊ธฐ
);
@@ -95,85 +260,106 @@ const SignUp1Screen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#003340',
paddingHorizontal: 30,
- paddingTop: 60,
+ paddingTop: 30,
},
backButton: {
position: 'absolute',
- top: 50,
- left: 20,
+ top: 30,
+ left: 30,
zIndex: 10,
},
backText: {
fontSize: 36,
- color: '#F074BA',
},
title: {
fontSize: 24,
fontWeight: 'bold',
- color: '#F074BA',
- position: 'absolute',
- top: 150,
- left: 30,
+ marginTop: 90,
+ marginBottom: 20,
},
allAgree: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 15,
borderBottomWidth: 2,
- borderBottomColor: '#F074BA',
- top: 150,
- marginTop: 20,
marginBottom: 10,
},
allAgreeText: {
fontSize: 16,
- color: '#F074BA',
fontWeight: 'bold',
marginLeft: 10,
},
scrollView: {
- flex: 1,
- marginTop: 150,
+ flex: 1,
marginBottom: 20,
- maxHeight: 400,
+ },
+ termsContainer: {
+ marginBottom: 10,
},
agreeItem: {
flexDirection: 'row',
alignItems: 'center',
height: 50,
- backgroundColor: '#FFFFFF',
borderRadius: 15,
paddingHorizontal: 15,
- marginBottom: 10,
- justifyContent: 'flex-start',
+ },
+ agreeItemExpanded: {
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ titleContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginLeft: 10,
},
agreeText: {
+ flex: 1,
fontSize: 14,
- color: '#000',
fontWeight: '500',
+ },
+ expandIcon: {
+ fontSize: 12,
marginLeft: 10,
},
+ termsContent: {
+ borderBottomLeftRadius: 15,
+ borderBottomRightRadius: 15,
+ },
+ termsScrollView: {
+ maxHeight: 200,
+ paddingHorizontal: 15,
+ paddingTop: 15,
+ },
+ termsText: {
+ fontSize: 12,
+ lineHeight: 18,
+ marginBottom: 15,
+ },
+ agreeButton: {
+ marginHorizontal: 15,
+ marginVertical: 15,
+ paddingVertical: 12,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ agreeButtonText: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
button: {
width: '100%',
height: 50,
- backgroundColor: '#F074BA',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
- position: 'absolute',
- bottom: 80,
- alignSelf: 'center',
- },
- buttonDisabled: {
- backgroundColor: '#F8C7CC',
+ marginBottom: 30,
},
buttonText: {
- color: '#fff',
fontSize: 18,
fontWeight: 'bold',
},
});
-export default SignUp1Screen;
+export default SignUp1Screen;
\ No newline at end of file
diff --git a/src/screens/Auth/SignUp2Screen.js b/src/screens/Auth/SignUp2Screen.js
index 230a5cb..e3b703a 100644
--- a/src/screens/Auth/SignUp2Screen.js
+++ b/src/screens/Auth/SignUp2Screen.js
@@ -8,19 +8,20 @@ import {
StyleSheet,
Pressable,
ScrollView,
- Dimensions,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
SafeAreaView,
+ Keyboard,
} from "react-native";
import { API_BASE_URL } from "../../utils/apiConfig";
import EyeOpen from "../../components/EyeOpen";
import EyeClosed from "../../components/EyeClosed";
-
-const { height, width } = Dimensions.get("window");
+import { useTheme } from "../../utils/ThemeContext";
const SignUp2Screen = ({ navigation }) => {
+ const { theme } = useTheme();
+
const [seePassword, setSeePassword] = useState(true);
const [seeConfirmPassword, setSeeConfirmPassword] = useState(true);
@@ -28,25 +29,22 @@ const SignUp2Screen = ({ navigation }) => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
- const [gender, setGender] = useState(""); // "male" | "female"
+ const [gender, setGender] = useState("");
const [nickname, setNickname] = useState("");
- // โ
์๋
์์ผ: 3์นธ ์
๋ ฅ + ์๋ฒ ์ ์ก์ YYYY-MM-DD
const [birthY, setBirthY] = useState("");
const [birthM, setBirthM] = useState("");
const [birthD, setBirthD] = useState("");
- const [birthdate, setBirthdate] = useState(""); // ์กฐํฉ๋ ๊ฐ(์๋ฒ ์ ์ก์ฉ)
+ const [birthdate, setBirthdate] = useState("");
- // ์ฃผ์ (ํ์ 2 + ์ ํ 2)
- const [addrRegion, setAddrRegion] = useState(""); // ๋/ํน๋ณ์/๊ด์ญ์
- const [addrCity, setAddrCity] = useState(""); // ์/๊ตฐ/๊ตฌ
- const [addrTown, setAddrTown] = useState(""); // ์/๋ฉด/๋ (์ ํ)
- const [addrDetail, setAddrDetail] = useState(""); // ์์ธ (์ ํ)
+ const [addrRegion, setAddrRegion] = useState("");
+ const [addrCity, setAddrCity] = useState("");
+ const [addrTown, setAddrTown] = useState("");
+ const [addrDetail, setAddrDetail] = useState("");
const [showMoreAddr, setShowMoreAddr] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- // refs: ํฌ์ปค์ค ์ด๋
const refPw = useRef(null);
const refPw2 = useRef(null);
const refNick = useRef(null);
@@ -57,14 +55,36 @@ const SignUp2Screen = ({ navigation }) => {
const refCity = useRef(null);
const refTown = useRef(null);
const refDetail = useRef(null);
+ const scrollRef = useRef(null);
+
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
+
+ useEffect(() => {
+ const showSub = Keyboard.addListener("keyboardDidShow", (e) => {
+ setKeyboardVisible(true);
+ setKeyboardHeight(e?.endCoordinates?.height ?? 0);
+ });
+ const hideSub = Keyboard.addListener("keyboardDidHide", () => {
+ setKeyboardVisible(false);
+ setKeyboardHeight(0);
+ });
+ return () => {
+ showSub.remove();
+ hideSub.remove();
+ };
+ }, []);
+
+ const bottomSpacer = useMemo(() => {
+ if (!keyboardVisible) return 120;
+ return Math.max(220, keyboardHeight + 140);
+ }, [keyboardVisible, keyboardHeight]);
- // validators
const validateEmail = (e) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((e || "").trim().toLowerCase());
const isValidDate = (d) => /^\d{4}-\d{2}-\d{2}$/.test((d || "").trim());
- // ๋น๋ฐ๋ฒํธ: 8~32์ & (์๋ฌธ/์ซ์/ํน์) 2์ข
์ด์
const passwordValid = (p) => {
const s = p || "";
if (s.length < 8 || s.length > 32) return false;
@@ -73,7 +93,6 @@ const SignUp2Screen = ({ navigation }) => {
return kinds >= 2;
};
- // ๋น๋ฐ๋ฒํธ ๊ฐ๋(0~3)
const passwordStrength = useMemo(() => {
if (!password) return 0;
const lenScore = password.length >= 12 ? 1 : 0;
@@ -83,17 +102,15 @@ const SignUp2Screen = ({ navigation }) => {
(/\d/.test(password) ? 1 : 0) +
(/[^\w\s]/.test(password) ? 1 : 0);
if (password.length >= 8 && kinds >= 2) {
- if (lenScore && kinds >= 3) return 3; // ๊ฐ
- return 2; // ๋ณดํต
+ if (lenScore && kinds >= 3) return 3;
+ return 2;
}
- return 1; // ์ฝ
+ return 1;
}, [password]);
- // ํ๊ตญ์ ๋ผ์ดํธ ์ฒดํฌ(๋๊ธ์ ๊ธฐ์ค)
const looksLikeRegion = (s) => /(๋|ํน๋ณ์|๊ด์ญ์)$/.test((s || "").trim());
const looksLikeCity = (s) => /(์|๊ตฐ|๊ตฌ)$/.test((s || "").trim());
- // ์๋ฒ ์ ์ก์ฉ address ํฉ์น๊ธฐ
const mergedAddress = useMemo(
() =>
[addrRegion, addrCity, addrTown, addrDetail]
@@ -103,7 +120,6 @@ const SignUp2Screen = ({ navigation }) => {
[addrRegion, addrCity, addrTown, addrDetail]
);
- // ์ ์ถ ๊ฐ๋ฅ ์ฌ๋ถ
const canSubmit = useMemo(() => {
return (
validateEmail(email) &&
@@ -119,7 +135,6 @@ const SignUp2Screen = ({ navigation }) => {
);
}, [email, password, confirmPassword, gender, nickname, birthdate, addrRegion, addrCity]);
- // ํ๋๋ณ ์๋ฌ
const fieldError = {
email: email.length > 0 && !validateEmail(email) ? "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋์์." : "",
password:
@@ -144,20 +159,14 @@ const SignUp2Screen = ({ navigation }) => {
: "",
};
- // โ
์๋
์์ผ 3์นธ โ YYYY-MM-DD๋ก ์๋ ํฉ์น๊ธฐ
useEffect(() => {
- const y = (birthY || "").padStart(4, ""); // ๊ทธ๋๋ก
- const m = (birthM || "").padStart(2, "");
- const d = (birthD || "").padStart(2, "");
if (birthY.length === 4 && birthM.length === 2 && birthD.length === 2) {
setBirthdate(`${birthY}-${birthM}-${birthD}`);
} else {
- // ๋ถ์์ ํ ๋ ๋น ๊ฐ์ผ๋ก ๋์ด ๊ฒ์ฆ์ ๊ฑธ๋ฆฌ๊ฒ
setBirthdate("");
}
}, [birthY, birthM, birthD]);
- // โ
๊ฐ ์นธ ์
๋ ฅ ์ ์ซ์๋ง, ๊ธ์์ ์ฑ์ฐ๋ฉด ์๋์ผ๋ก ๋ค์ ์นธ ํฌ์ปค์ค
const onChangeBirthY = (t) => {
const v = (t || "").replace(/[^\d]/g, "").slice(0, 4);
setBirthY(v);
@@ -167,7 +176,6 @@ const SignUp2Screen = ({ navigation }) => {
};
const onChangeBirthM = (t) => {
let v = (t || "").replace(/[^\d]/g, "").slice(0, 2);
- // 13์ ๋ฐฉ์ง(UX ์ฐจ์์์ ๋ณด์ )
if (v.length === 2) {
const n = Math.max(1, Math.min(12, parseInt(v, 10) || 0));
v = String(n).padStart(2, "0");
@@ -179,20 +187,17 @@ const SignUp2Screen = ({ navigation }) => {
};
const onChangeBirthD = (t) => {
let v = (t || "").replace(/[^\d]/g, "").slice(0, 2);
- // 32์ผ ๋ฐฉ์ง(๋๋ต์ ๋ณด์ )
if (v.length === 2) {
const n = Math.max(1, Math.min(31, parseInt(v, 10) || 0));
v = String(n).padStart(2, "0");
}
setBirthD(v);
- // ๋ง์ง๋ง ์นธ์ ์๋ ์ด๋ ์์ (์ํฐ๋ก ๋ค์ ํ๋ ์ด๋)
};
const handleSignUp = async () => {
if (isLoading) return;
setIsLoading(true);
- // ๋ฐฉ์ด ๊ฒ์ฆ
if (!validateEmail(email)) {
Alert.alert("์ค๋ฅ", "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ ์
๋ ฅํด์ฃผ์ธ์.");
setIsLoading(false); return;
@@ -235,7 +240,7 @@ const SignUp2Screen = ({ navigation }) => {
password,
gender,
nickname: (nickname || "").trim(),
- birthdate: birthdate.trim(), // โ
์๋ฒ์๋ YYYY-MM-DD ์ ๋ฌ
+ birthdate: birthdate.trim(),
address: mergedAddress,
}),
});
@@ -266,37 +271,43 @@ const SignUp2Screen = ({ navigation }) => {
const strengthText = ["", "์ฝํจ", "๋ณดํต", "๊ฐํจ"][passwordStrength];
return (
-
+
- {/* ํค๋ */}
-
+
navigation.goBack()}
style={styles.backButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
- {"<"}
+ {"<"}
- ํ์๊ฐ์
+ ํ์๊ฐ์
- {/* ํผ */}
{/* ์ด๋ฉ์ผ */}
- ์ด๋ฉ์ผ
+ ์ด๋ฉ์ผ
{
returnKeyType="next"
onSubmitEditing={() => refPw.current && refPw.current.focus && refPw.current.focus()}
/>
- {!!fieldError.email && {fieldError.email}}
+ {!!fieldError.email && {fieldError.email}}
{/* ๋น๋ฐ๋ฒํธ */}
- ๋น๋ฐ๋ฒํธ
-
+ ๋น๋ฐ๋ฒํธ
+
{
- {/* ๊ฐ๋ ํ์ */}
{password.length > 0 && (
- = 1 && styles.strengthOn]} />
- = 2 && styles.strengthOn]} />
- = 3 && styles.strengthOn]} />
- {strengthText}
+ = 1 && { backgroundColor: theme.accent.primary }
+ ]} />
+ = 2 && { backgroundColor: theme.accent.primary }
+ ]} />
+ = 3 && { backgroundColor: theme.accent.primary }
+ ]} />
+ {strengthText}
)}
- {!!fieldError.password && {fieldError.password}}
- ์๋ฌธ ๋/์๋ฌธ์ยท์ซ์ยทํน์ ์ค 2๊ฐ์ง ์ด์, 8~32์
+ {!!fieldError.password && {fieldError.password}}
+ ์๋ฌธ ๋/์๋ฌธ์ยท์ซ์ยทํน์ ์ค 2๊ฐ์ง ์ด์, 8~32์
{/* ๋น๋ฐ๋ฒํธ ํ์ธ */}
- ๋น๋ฐ๋ฒํธ ํ์ธ
-
+ ๋น๋ฐ๋ฒํธ ํ์ธ
+
{
{seeConfirmPassword ? : }
- {!!fieldError.confirm && {fieldError.confirm}}
+ {!!fieldError.confirm && {fieldError.confirm}}
{confirmPassword.length > 0 && password === confirmPassword && (
- ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํฉ๋๋ค.
+ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํฉ๋๋ค.
)}
{/* ์ฑ๋ณ */}
- ์ฑ๋ณ
-
+ ์ฑ๋ณ
+
setGender("male")}
- style={[styles.segmentItem, gender === "male" && styles.segmentItemOn]}
+ style={[
+ styles.segmentItem,
+ { borderColor: theme.border.medium },
+ gender === "male" && { backgroundColor: theme.accent.primary, borderColor: theme.accent.primary }
+ ]}
hitSlop={8}
>
-
+
๋จ์ฑ
setGender("female")}
- style={[styles.segmentItem, gender === "female" && styles.segmentItemOn]}
+ style={[
+ styles.segmentItem,
+ { borderColor: theme.border.medium },
+ gender === "female" && { backgroundColor: theme.accent.primary, borderColor: theme.accent.primary }
+ ]}
hitSlop={8}
>
-
+
์ฌ์ฑ
{/* ๋๋ค์ */}
- ๋๋ค์
+ ๋๋ค์
refBirthY.current && refBirthY.current.focus && refBirthY.current.focus()}
/>
- {/* โ
์๋
์์ผ: 3์นธ ์
๋ ฅ */}
- ์๋
์์ผ
+ {/* ์๋
์์ผ */}
+ ์๋
์์ผ
{
returnKeyType="next"
onSubmitEditing={() => refBirthM.current && refBirthM.current.focus && refBirthM.current.focus()}
/>
- -
+ -
{
returnKeyType="next"
onSubmitEditing={() => refBirthD.current && refBirthD.current.focus && refBirthD.current.focus()}
/>
- -
+ -
{
onSubmitEditing={() => refRegion.current && refRegion.current.focus && refRegion.current.focus()}
/>
- {!!fieldError.birth && {fieldError.birth}}
+ {!!fieldError.birth && {fieldError.birth}}
{/* ์ฃผ์ */}
- ์ฃผ์
- (๋/๊ด์ญ์ + ์/๊ตฐ/๊ตฌ ํ์)
+ ์ฃผ์
+ (๋/๊ด์ญ์ + ์/๊ตฐ/๊ตฌ ํ์)
{
/>
setShowMoreAddr(true)}
/>
- {!!fieldError.region && {fieldError.region}}
- {!!fieldError.city && {fieldError.city}}
- ์) ๊ฒฝ๊ธฐ๋ ํ์ฑ์
+ {!!fieldError.region && {fieldError.region}}
+ {!!fieldError.city && {fieldError.city}}
+ ์) ๊ฒฝ๊ธฐ๋ ํ์ฑ์
- {/* ์ ํ ์์ฝ๋์ธ: ๐บ ํ์ดํ ํฌ๊ฒ */}
+ {/* ์ถ๊ฐ ์ฃผ์ ์์ฝ๋์ธ */}
setShowMoreAddr((v) => !v)}
style={styles.accordionHeader}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
- ์ถ๊ฐ ์ฃผ์ ์
๋ ฅ (์ ํ)
- {showMoreAddr ? "โด" : "โพ"}
+ ์ถ๊ฐ ์ฃผ์ ์
๋ ฅ (์ ํ)
+ {showMoreAddr ? "โด" : "โพ"}
{showMoreAddr && (
@@ -487,40 +584,74 @@ const SignUp2Screen = ({ navigation }) => {
refDetail.current && refDetail.current.focus && refDetail.current.focus()}
+ onFocus={() => {
+ if (!showMoreAddr) setShowMoreAddr(true);
+ requestAnimationFrame(() => {
+ scrollRef.current?.scrollToEnd({ animated: true });
+ });
+ }}
/>
{
+ if (!showMoreAddr) setShowMoreAddr(true);
+ requestAnimationFrame(() => {
+ scrollRef.current?.scrollToEnd({ animated: true });
+ });
+ }}
/>
)}
- {/* ํ๋จ ์ฌ๋ฐฑ: ๋ฒํผ ๊ณต๊ฐ ํ๋ณด */}
-
+
{/* ์ ์ถ ๋ฒํผ */}
-
+
- {isLoading ? : ์ธ์ฆํ๊ธฐ}
+ {isLoading ? (
+
+ ) : (
+ ์ธ์ฆํ๊ธฐ
+ )}
@@ -529,7 +660,7 @@ const SignUp2Screen = ({ navigation }) => {
};
const styles = StyleSheet.create({
- safe: { flex: 1, backgroundColor: "#003340" },
+ safe: { flex: 1 },
flex: { flex: 1 },
header: {
@@ -538,56 +669,47 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
- backgroundColor: "#003340",
},
backButton: { width: 36, height: 36, alignItems: "center", justifyContent: "center" },
- backText: { fontSize: 28, color: "#F074BA", marginTop: -2 },
- title: { fontSize: 20, fontWeight: "bold", color: "#F074BA" },
+ backText: { fontSize: 28, marginTop: -2 },
+ title: { fontSize: 20, fontWeight: "bold" },
scroll: { flex: 1 },
scrollContent: { paddingHorizontal: 24, paddingTop: 8, paddingBottom: 8 },
- label: { fontSize: 15, color: "#F074BA", marginTop: 12, marginBottom: 8 },
- labelRow: { fontSize: 16, color: "#F074BA", marginTop: 12, marginBottom: 8 },
- requiredBadge: { fontSize: 12, color: "#ffcae4" },
+ label: { fontSize: 15, marginTop: 12, marginBottom: 8 },
+ labelRow: { fontSize: 16, marginTop: 12, marginBottom: 8 },
+ requiredBadge: { fontSize: 12 },
input: {
width: "100%",
height: 50,
borderWidth: 1,
- borderColor: "#87a9b1",
borderRadius: 10,
paddingHorizontal: 14,
marginBottom: 10,
fontSize: 16,
- backgroundColor: "#f1f6f7",
- color: "#0a0a0a",
},
- inputError: { borderColor: "#ff8a8a" },
inputContainer: {
flexDirection: "row",
alignItems: "center",
width: "100%",
borderWidth: 1,
- borderColor: "#87a9b1",
borderRadius: 10,
- backgroundColor: "#f1f6f7",
marginBottom: 10,
paddingHorizontal: 6,
},
- inputField: { flex: 1, height: 50, fontSize: 16, color: "#0a0a0a", paddingHorizontal: 8 },
+ inputField: { flex: 1, height: 50, fontSize: 16, paddingHorizontal: 8 },
icon: { padding: 10 },
- errorText: { color: "tomato", fontSize: 12, marginBottom: 6, marginLeft: 2 },
- passwordGuide: { fontSize: 12, color: "#cfe7ec", marginBottom: 6, marginLeft: 2 },
- passwordMatch: { fontSize: 12, color: "#00e676", marginBottom: 6, marginLeft: 2 },
+ errorText: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
+ passwordGuide: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
+ passwordMatch: { fontSize: 12, marginBottom: 6, marginLeft: 2 },
- // ์ฑ๋ณ ์ธ๊ทธ๋จผํธ
segment: {
flexDirection: "row",
gap: 10,
- backgroundColor: "#0e4652",
padding: 6,
borderRadius: 12,
alignSelf: "flex-start",
@@ -597,42 +719,32 @@ const styles = StyleSheet.create({
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 10,
- backgroundColor: "transparent",
borderWidth: 1,
- borderColor: "transparent",
},
- segmentItemOn: { backgroundColor: "#F074BA" },
- segmentText: { color: "#d2eef3", fontSize: 14, fontWeight: "600" },
- segmentTextOn: { color: "#fff" },
+ segmentText: { fontSize: 14, fontWeight: "600" },
- // ๋น๋ฐ๋ฒํธ ๊ฐ๋
strengthRow: { flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: 2 },
- strengthBar: { width: 32, height: 6, borderRadius: 4, backgroundColor: "#6e8f98" },
- strengthOn: { backgroundColor: "#F074BA" },
- strengthText: { color: "#cfe7ec", fontSize: 12, marginLeft: 6 },
+ strengthBar: { width: 32, height: 6, borderRadius: 4 },
+ strengthText: { fontSize: 12, marginLeft: 6 },
- // ์๋
์์ผ 3์นธ
birthRow: { flexDirection: "row", alignItems: "center", marginBottom: 6 },
birthColY: { flex: 1.2, textAlign: "center" },
birthCol: { flex: 0.9, textAlign: "center" },
- birthDash: { color: "#cfe7ec", marginHorizontal: 6, fontSize: 18, marginBottom: 4 },
+ birthDash: { marginHorizontal: 6, fontSize: 18, marginBottom: 4 },
- // ์ฃผ์
rowTwoCols: { flexDirection: "row", gap: 10 },
col: { flex: 1 },
- inlineHint: { fontSize: 12, color: "#9bbcc4", marginTop: -2, marginBottom: 6, marginLeft: 2 },
+ inlineHint: { fontSize: 12, marginTop: -2, marginBottom: 6, marginLeft: 2 },
- // ๐บ ์ปค์ง ์์ฝ๋์ธ ํ์ดํ
accordionHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 12,
},
- accordionTitle: { fontSize: 15, color: "#cfe7ec" },
- accordionChevron: { fontSize: 22, color: "#cfe7ec" }, // โ ๊ธฐ์กด๋ณด๋ค ํฌ๊ฒ
+ accordionTitle: { fontSize: 15 },
+ accordionChevron: { fontSize: 22 },
- // ํธํฐ ๋ฒํผ
footer: {
position: "absolute",
left: 0,
@@ -641,7 +753,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
paddingTop: 8,
paddingBottom: 16,
- backgroundColor: "rgba(0, 51, 64, 0.92)",
},
button: {
height: 52,
@@ -649,7 +760,7 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
},
- buttonText: { color: "#fff", fontSize: 18, fontWeight: "bold" },
+ buttonText: { fontSize: 18, fontWeight: "bold" },
});
-export default SignUp2Screen;
+export default SignUp2Screen;
\ No newline at end of file
diff --git a/src/screens/Auth/SignUp3Screen.js b/src/screens/Auth/SignUp3Screen.js
index a77a458..78327d5 100644
--- a/src/screens/Auth/SignUp3Screen.js
+++ b/src/screens/Auth/SignUp3Screen.js
@@ -8,8 +8,10 @@ import {
Alert,
} from "react-native";
import { API_BASE_URL } from "../../utils/apiConfig";
+import { useTheme } from "../../utils/ThemeContext";
const SignUp3Screen = ({ route, navigation }) => {
+ const { theme } = useTheme();
const { email, id } = route.params;
const [code, setCode] = useState(["", "", "", "", "", ""]);
@@ -24,7 +26,6 @@ const SignUp3Screen = ({ route, navigation }) => {
if (index < 5) {
inputs.current[index + 1].focus();
} else {
- // 6์๋ฆฌ ์
๋ ฅ ์๋ฃ โ API ํธ์ถ
verifyCode(newCode.join(""));
}
} else if (text === "") {
@@ -46,7 +47,7 @@ const SignUp3Screen = ({ route, navigation }) => {
});
const data = await response.json();
- console.log("๐ ์ธ์ฆ ์๋ต:", data);
+ console.log("๐ ์ธ์ฆ ์๋ต:", data);
if (response.status === 200 || data.status === "success") {
Alert.alert("์ฑ๊ณต", "ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค!", [
@@ -64,9 +65,9 @@ const SignUp3Screen = ({ route, navigation }) => {
};
return (
-
- ์ธ์ฆ๋ฒํธ ์
๋ ฅ
-
+
+ ์ธ์ฆ๋ฒํธ ์
๋ ฅ
+
{email} ์ฃผ์๋ก ์ ์ก๋ ์ธ์ฆ๋ฒํธ 6์๋ฆฌ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
@@ -75,7 +76,14 @@ const SignUp3Screen = ({ route, navigation }) => {
(inputs.current[index] = ref)}
- style={styles.codeInput}
+ style={[
+ styles.codeInput,
+ {
+ borderColor: theme.accent.primary,
+ backgroundColor: theme.background.secondary,
+ color: theme.text.primary,
+ },
+ ]}
value={digit}
onChangeText={(text) => handleChange(text, index)}
keyboardType="number-pad"
@@ -89,7 +97,9 @@ const SignUp3Screen = ({ route, navigation }) => {
style={styles.resendButton}
onPress={() => Alert.alert("๋ฏธ๊ตฌํ", "์ฌ์ ์ก ๊ธฐ๋ฅ์ ์ถํ ๊ตฌํ ์์ ์
๋๋ค.")}
>
- ์ธ์ฆ๋ฒํธ ๋ค์ ๋ณด๋ด๊ธฐ
+
+ ์ธ์ฆ๋ฒํธ ๋ค์ ๋ณด๋ด๊ธฐ
+
);
@@ -98,19 +108,16 @@ const SignUp3Screen = ({ route, navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
paddingHorizontal: 30,
justifyContent: "center",
alignItems: "center",
},
title: {
- color: "#F074BA",
fontSize: 24,
fontWeight: "bold",
marginBottom: 10,
},
subtitle: {
- color: "#fff",
fontSize: 16,
textAlign: "center",
marginBottom: 30,
@@ -124,21 +131,17 @@ const styles = StyleSheet.create({
width: 45,
height: 50,
borderWidth: 1,
- borderColor: "#F074BA",
borderRadius: 8,
fontSize: 24,
- color: "#fff",
- backgroundColor: "#002830",
marginHorizontal: 4,
},
resendButton: {
marginTop: 30,
},
resendText: {
- color: "#F074BA",
fontSize: 16,
textDecorationLine: "underline",
},
});
-export default SignUp3Screen;
+export default SignUp3Screen;
\ No newline at end of file
diff --git a/src/screens/Auth/SignUp4Screen.js b/src/screens/Auth/SignUp4Screen.js
index b11fbf2..8bdab63 100644
--- a/src/screens/Auth/SignUp4Screen.js
+++ b/src/screens/Auth/SignUp4Screen.js
@@ -7,24 +7,44 @@ import {
ScrollView,
Image,
} from "react-native";
+import { useTheme } from "../../utils/ThemeContext";
const SignUp4Screen = ({ navigation }) => {
+ const { theme } = useTheme();
+
const handleGoToLogin = () => {
navigation.replace("Login");
};
return (
-
- {/* ๐ ์ผ๋ฌ์คํธ ์ด๋ฏธ์ง๋ก ๊ต์ฒด */}
+
- ๊ฐ์
์ด ์๋ฃ๋์์ด์!
-
+
+ ๊ฐ์
์ด ์๋ฃ๋์์ด์!
+
+
๋๋์ ์ค์ ๊ฑธ ํ์ํฉ๋๋ค!{`\n`}์ง๊ธ ๋ฐ๋ก ๋ก๊ทธ์ธํด๋ณผ๊น์?
-
- ๋ก๊ทธ์ธํ๋ฌ ๊ฐ๊ธฐ
+
+
+ ๋ก๊ทธ์ธํ๋ฌ ๊ฐ๊ธฐ
+
);
@@ -33,7 +53,6 @@ const SignUp4Screen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flexGrow: 1,
- backgroundColor: "#003340",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 30,
@@ -47,14 +66,12 @@ const styles = StyleSheet.create({
title: {
fontSize: 26,
fontWeight: "bold",
- color: "#F074BA",
marginTop: 18,
marginBottom: 12,
textAlign: "center",
},
subtitle: {
fontSize: 16,
- color: "#fff",
textAlign: "center",
marginBottom: 40,
lineHeight: 24,
@@ -62,21 +79,18 @@ const styles = StyleSheet.create({
button: {
width: "100%",
height: 52,
- backgroundColor: "#F074BA",
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
- elevation: 4, // Android ๊ทธ๋ฆผ์
- shadowColor: "#000", // iOS ๊ทธ๋ฆผ์
+ elevation: 4,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
buttonText: {
- color: "#fff",
fontSize: 18,
fontWeight: "bold",
},
});
-export default SignUp4Screen;
+export default SignUp4Screen;
\ No newline at end of file
diff --git a/src/screens/Chatbot/ChatbotScreen.js b/src/screens/Chatbot/ChatbotScreen.js
index 174b58c..e33dd63 100644
--- a/src/screens/Chatbot/ChatbotScreen.js
+++ b/src/screens/Chatbot/ChatbotScreen.js
@@ -1,4 +1,5 @@
-import React, { useState, useRef } from 'react';
+// ChatbotScreen.js
+import React, { useState, useRef, useCallback, useEffect } from "react";
import {
View,
Text,
@@ -9,337 +10,720 @@ import {
Platform,
ScrollView,
ActivityIndicator,
-} from 'react-native';
-
-import { chatbotReply } from '../../utils/chatbotReply';
-import SearchIcon from "../../assets/icons/search.svg";
-
-// const ChatbotScreen = () => {
-// console.log('ChatbotScreen ๋ ๋๋ง');
-// return (
-//
-// Chatbot Screen
-//
-// );
-// };
-
-// const ChatbotScreen = () => {
-// return (
-
-//
-//
-//
-// ์๋
ํ์ธ์! ๋ฌด์์ ๋์๋๋ฆด๊น์?
-//
-
-//
-// ETF ํฌ์๊ฐ ๋ญ์ผ?
-//
-
-//
-//
-// ETF ํฌ์๋ฅผ ์๊ฐํด๋ณด๋ฉด, ์ด๊ฒ ๋ง์น ์ผํ๋ชฐ์์ ์ฅ๋ฐ๊ตฌ๋์ ์ฌ๋ฌ ๊ฐ์ง ์ํ์ ๋ด๋ ๊ฒ๊ณผ ๋น์ทํด์. ETF๋ ์ฃผ์, ์ฑ๊ถ ๋ฑ ๋ค์ํ ์์ฐ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์์ด์. ์ด๊ฑธ ์ฌ๋ ๊ฒ์ ๊ทธ ์ฅ๋ฐ๊ตฌ๋ ์ ์ฒด๋ฅผ ํ ๋ฒ์ ์ฌ๋ ๊ฒ์ด๋ ๊ฐ์์. ๊ทธ๋์ ์์ฆ์ ์ด๋ฐ ETF ํฌ์๋ฅผ ๋ง์ด ์ถ์ฒํ๋๋ฐ, ๊ทธ ์ด์ ๋ ํ ๋ฒ์ ๋ง์ ์ข
๋ชฉ์ ์ฃผ์์ ์ฌ๋ ๊ฒ๋ณด๋ค ์ํ์ ๋ถ์ฐ์ํฌ ์ ์๊ธฐ ๋๋ฌธ์ด์์. ์ด๋ ๊ฒ ํด์ ์ฌ๋ฌ ์ข
๋ชฉ์ ์ฃผ์์ ํ ๋ฒ์ ๊ด๋ฆฌํ ์ ์์ด์!
-//
-//
-
-//
-// ETF ํฌ์๊ฐ ๋ญ์ผ? ETF ํฌ์๊ฐ ๋ญ์ผ?
-//
-
-//
-//
-// ์ด๋ ๊ฐ์์. ๊ทธ๋์ ์์ฆ์ ์ด๋ฐ ETF ํฌ์๋ฅผ ๋ง์ด ์ถ์ฒํ๋๋ฐ, ๊ทธ ์ด์ ๋ ํ ๋ฒ์ ๋ง์
-//
-//
-
-
-
-
-
-//
-
-//
-//
-// #
-//
-//
-//
-// ๐
-//
-//
-//
-// );
-// };
+ Animated,
+ Dimensions,
+ Keyboard,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
+import { chatbotReply } from "../../utils/chatbotReply";
+
+// ๐ Lucide ์์ด์ฝ import
+import { Send } from "lucide-react-native";
+
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
+const { width: SCREEN_WIDTH } = Dimensions.get("window");
+const INPUT_BAR_HEIGHT = 70;
+const INPUT_FONT_SIZE = 16;
+const GAP_FROM_TAB = 0;
const ChatbotScreen = () => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
+ const insets = useSafeAreaInsets();
+ const tabBarHeight = useBottomTabBarHeight();
+
const [messages, setMessages] = useState([
- { sender: 'bot', text: '์๋
ํ์ธ์! ๋ฌด์์ ๋์๋๋ฆด๊น์?' },
+ {
+ sender: "bot",
+ text: "์๋
ํ์ธ์! ํฌ์์ ๋ํด ๊ถ๊ธํ ๊ฒ์ด ์์ผ์๋ฉด ์ธ์ ๋ ๋ฌผ์ด๋ณด์ธ์ โจ",
+ timestamp: Date.now(),
+ },
]);
- const [input, setInput] = useState('');
- const scrollRef = useRef();
+ const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(false);
+ const [inputHeight, setInputHeight] = useState(44);
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
-// const suggestions = [
-// "์ธ๊ธฐ ์ฃผ์", "AI ์ถ์ฒ ์ข
๋ชฉ", "์ฅ ์ด๋ฐ ์ด์", "๋์ฅ์ฃผ",
-// "์ด์ ์ ๊ธ์์น 10", "์ฝ์คํผ/์ฝ์ค๋ฅ ์์น๋ฅ ", "๋ฐฐ๋น์ฃผ ์ถ์ฒ", "๊ฒ์ ๊ธ์์น ์ข
๋ชฉ"
-// ];
-
-const suggestions = [
- "์ฃผ์ ์์", "PER", "๋ฐฐ๋น๊ธ", "์ฐ๋์ฃผ", "์ ๋ฌผ",
- "ํธ๊ฐ์ฐฝ", "๋ถ์ฐํฌ์๊ฐ ์ ํ์ํด?", "์์ฅํ์ง",
- "์ฝ์คํผ๋ ์ฝ์ค๋ฅ ์ฐจ์ด์ ", "๊ณต๋งค๋", "์ฃผ์ ๊ฑฐ๋ ์๊ฐ",
- "ํ๋จ๊ฐ", "๋งค์&๋งค๋", "์๊ฐ", "์์๋ฃ", "ETF"
-];
-
- // const suggestions = [
- // "ETF ํฌ์๊ฐ ๋ญ์ผ?",
- // "์ผ์ฑ์ ์ ์ฃผ์ ์ด๋?",
- // "์นด์นด์ค ์ค์ ์ ์ด๋?",
- // "์์ฆ ๋จ๋ ์ฐ์
์๋ ค์ค",
- // "AI ๊ด๋ จ์ฃผ ์๋ ค์ค",
- // ];
-
-const sendMessage = async () => {
- if (!input.trim()) return;
-
- const userMsg = { sender: 'user', text: input };
- const loadingMsg = { sender: 'bot', text: '...' };
-
- setMessages((prev) => [...prev, userMsg, loadingMsg]);
- setInput('');
- setLoading(true);
-
- setTimeout(() => {
- scrollRef.current?.scrollToEnd({ animated: true });
- }, 100);
-
- const reply = await chatbotReply(input);
-
- console.log("๐ค chatbotReply:", reply);
- console.log("๐ ๊ธธ์ด:", reply.length);
-
- setMessages((prev) => {
- const newMessages = [...prev];
- newMessages.pop(); // loading ๋ฉ์์ง ์ ๊ฑฐ
- return [...newMessages, { sender: 'bot', text: reply }];
- });
-
- setLoading(false);
-
- // ์คํฌ๋กค
- setTimeout(() => {
- scrollRef.current?.scrollToEnd({ animated: true });
- }, 100);
-};
-
+ const scrollRef = useRef(null);
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(50)).current;
+
+ const suggestions = [
+ { text: "์ฃผ์ ํฌ์ ์์ํ๊ธฐ", icon: "๐", category: "๊ธฐ์ด" },
+ { text: "PER๊ณผ PBR ์ฐจ์ด์ ", icon: "๐", category: "์งํ" },
+ { text: "๋ฐฐ๋น์ฃผ ์ถ์ฒํด์ค", icon: "๐ฐ", category: "ํฌ์" },
+ { text: "๋ถ์ฐํฌ์ ์ ๋ต", icon: "๐ฏ", category: "์ ๋ต" },
+ { text: "์ฝ์คํผ vs ์ฝ์ค๋ฅ", icon: "๐๏ธ", category: "์์ฅ" },
+ { text: "ETF๋ ๋ฌด์์ธ๊ฐ์?", icon: "๐ฆ", category: "์ํ" },
+ { text: "๊ณต๋งค๋ ์๋ฆฌ", icon: "๐", category: "๊ฑฐ๋" },
+ { text: "์ฃผ์ ๊ฑฐ๋ ์๊ฐ", icon: "โฐ", category: "๊ธฐ์ด" },
+ ];
+
+ // ํค๋ณด๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋
+ useEffect(() => {
+ const keyboardWillShow = Keyboard.addListener(
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
+ (event) => {
+ setKeyboardHeight(event.endCoordinates.height);
+ }
+ );
+
+ const keyboardWillHide = Keyboard.addListener(
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
+ () => {
+ setKeyboardHeight(0);
+ }
+ );
+
+ return () => {
+ keyboardWillShow.remove();
+ keyboardWillHide.remove();
+ };
+ }, []);
+
+ React.useEffect(() => {
+ if (showSuggestions) {
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: 0,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ } else {
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: 50,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }
+ }, [showSuggestions]);
+
+ const sendMessage = useCallback(
+ async (messageText = input) => {
+ if (!messageText.trim()) return;
+
+ const userMsg = {
+ sender: "user",
+ text: messageText,
+ timestamp: Date.now(),
+ };
+ const loadingMsg = { sender: "bot", text: "typing", timestamp: Date.now() };
+
+ setMessages((prev) => [...prev, userMsg, loadingMsg]);
+ setInput("");
+ setLoading(true);
+ setShowSuggestions(false);
+ setInputHeight(44);
+
+ setTimeout(() => {
+ scrollRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+
+ try {
+ const reply = await chatbotReply(messageText);
+ setMessages((prev) => {
+ const next = [...prev];
+ next.pop();
+ return [
+ ...next,
+ { sender: "bot", text: reply, timestamp: Date.now() },
+ ];
+ });
+ } catch {
+ setMessages((prev) => {
+ const next = [...prev];
+ next.pop();
+ return [
+ ...next,
+ {
+ sender: "bot",
+ text: "์ฃ์กํด์, ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์ ๐",
+ timestamp: Date.now(),
+ },
+ ];
+ });
+ } finally {
+ setLoading(false);
+ setTimeout(() => {
+ scrollRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ }
+ },
+ [input]
+ );
+ // ๋์ ๋์ด ๊ณ์ฐ
+ const dynamicInputBarHeight = Math.max(INPUT_BAR_HEIGHT, inputHeight + 26);
+ const bottomOffset = keyboardHeight > 0 ? 0 : tabBarHeight + GAP_FROM_TAB;
+
+ // ์ถ์ฒ ์ง๋ฌธ ์ปจํ
์ด๋์ bottom ์์น ๊ณ์ฐ (ํค๋ณด๋ ๋์ด ํฌํจ)
+ const suggestionBottomPosition = keyboardHeight > 0
+ ? keyboardHeight + dynamicInputBarHeight + 12
+ : bottomOffset + dynamicInputBarHeight + 12;
+
+ const TypingIndicator = () => {
+ const dot1Anim = useRef(new Animated.Value(0.4)).current;
+ const dot2Anim = useRef(new Animated.Value(0.4)).current;
+ const dot3Anim = useRef(new Animated.Value(0.4)).current;
+
+ React.useEffect(() => {
+ const animate = () => {
+ Animated.sequence([
+ Animated.timing(dot1Anim, { toValue: 1, duration: 400, useNativeDriver: true }),
+ Animated.timing(dot2Anim, { toValue: 1, duration: 400, useNativeDriver: true }),
+ Animated.timing(dot3Anim, { toValue: 1, duration: 400, useNativeDriver: true }),
+ Animated.timing(dot1Anim, { toValue: 0.4, duration: 400, useNativeDriver: true }),
+ Animated.timing(dot2Anim, { toValue: 0.4, duration: 400, useNativeDriver: true }),
+ Animated.timing(dot3Anim, { toValue: 0.4, duration: 400, useNativeDriver: true }),
+ ]).start(() => animate());
+ };
+ animate();
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+ };
return (
-
-
+
-{messages.map((msg, index) => (
-
- {msg.text === '...' && msg.sender === 'bot' ? (
-
- ) : (
-
- {msg.text}
-
- )}
-
-))}
-
-
-
-{showSuggestions && (
-
- {suggestions.map((item, idx) => (
- {
- setInput(item);
- setShowSuggestions(false);
- }}
- style={styles.suggestionPill}
- >
- {item}
-
- ))}
-
-)}
-
- setShowSuggestions((prev) => !prev)}
->
-
-
- #
-
-
-
-
-
-
-
- {/* */}
-
-
-
-
-
+ {/* Header */}
+
+
+
+
+ AI Assistant
+
+
+
+ ํฌ์ ์ ๋ฌธ ์๋ด
+
+
+
+
+ {messages.map((msg, index) => {
+ const isUser = msg.sender === "user";
+ const isTyping = msg.text === "typing";
+
+ return (
+
+ {!isUser && (
+
+
+ ๐ค
+
+
+ )}
+
+
+ {isTyping ? (
+
+ ) : (
+
+ {msg.text}
+
+ )}
+
+
+ {isUser && (
+
+
+ ๐ค
+
+
+ )}
+
+ );
+ })}
+
+
+ {/* Suggestions */}
+ {showSuggestions && (
+
+
+
+
+
+ ๐ก ์ถ์ฒ ์ง๋ฌธ
+
+
+
+
+ {suggestions.map((item, idx) => (
+ sendMessage(item.text)}
+ style={[styles.suggestionCard, {
+ backgroundColor: theme.background.secondary,
+ borderColor: theme.border.medium,
+ shadowColor: theme.shadow
+ }]}
+ activeOpacity={0.7}
+ >
+ {item.icon}
+
+ {item.text}
+
+
+ {item.category}
+
+
+ ))}
+
+
+ )}
+
+ {/* Input Bar */}
+
+
+ setShowSuggestions((prev) => !prev)}
+ activeOpacity={0.7}
+ >
+
+ {showSuggestions ? "โจ" : "๐ก"}
+
+
+
+
+ {
+ const { height } = event.nativeEvent.contentSize;
+ const newHeight = Math.min(Math.max(height + 8, 44), 120);
+ setInputHeight(newHeight);
+ }}
+ returnKeyType="send"
+ onSubmitEditing={() => sendMessage()}
+ blurOnSubmit={false}
+ autoCorrect={false}
+ autoCapitalize="none"
+ multiline
+ maxLength={500}
+ textAlignVertical="top"
+ />
+
+
+ sendMessage()}
+ activeOpacity={0.7}
+ style={[
+ styles.sendButton,
+ {
+ backgroundColor: theme.background.secondary,
+ borderColor: theme.border.medium
+ },
+ input.trim() && styles.sendButtonActive,
+ input.trim() && {
+ backgroundColor: `${theme.accent.primary}4D`,
+ borderColor: `${theme.accent.primary}80`
+ }
+ ]}
+ disabled={!input.trim() || loading}
+ >
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#003340',
},
- chatContainer: {
- flexGrow: 1, // โ
์ด ์ค ์ถ๊ฐ!
- paddingTop: 60,
+
+ keyboardView: {
+ flex: 1,
+ },
+
+ header: {
+ paddingBottom: 20,
paddingHorizontal: 20,
- paddingBottom: 120,
- },
- botMessage: {
- backgroundColor: '#E0E6E7',
- borderRadius: 10,
- padding: 12,
- alignSelf: 'flex-start',
- marginBottom: 10,
- maxWidth: '80%', // โ
์ ๋ ํฌ๊ธฐ ์ ํ
- marginRight: 30, // โ
์ค๋ฅธ์ชฝ ์ฌ๋ฐฑ ์ถ๊ฐ
- //flexShrink: 1, // โ
ํ
์คํธ ๋์น ๊ฒฝ์ฐ ์ค์ด๊ธฐ ํ์ฉ
- //flexWrap: 'wrap', // โ
ํ
์คํธ ์ค๋ฐ๊ฟ ํ์ฉ
+ alignItems: "center",
+ borderBottomWidth: 1,
},
- botText: {
- color: '#222',
+
+ aiIndicator: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 4,
+ },
+
+ aiDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ marginRight: 8,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.8,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+
+ aiText: {
+ fontSize: 16,
+ fontWeight: "600",
+ letterSpacing: 0.5,
+ },
+
+ headerSubtitle: {
+ fontSize: 14,
+ letterSpacing: 0.3,
+ },
+
+ chatScroll: {
+ flex: 1,
+ },
+
+ chatContainer: {
+ paddingTop: 20,
+ paddingHorizontal: 16,
+ },
+
+ messageWrapper: {
+ flexDirection: "row",
+ marginBottom: 16,
+ alignItems: "flex-end",
+ },
+
+ userMessageWrapper: {
+ justifyContent: "flex-end",
+ },
+
+ botMessageWrapper: {
+ justifyContent: "flex-start",
+ },
+
+ avatarContainer: {
+ marginHorizontal: 8,
+ },
+
+ botAvatar: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+
+ userAvatar: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+
+ avatarText: {
+ fontSize: 14,
+ },
+
+ messageBubble: {
+ maxWidth: SCREEN_WIDTH * 0.7,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 20,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+
+ botBubble: {
+ borderBottomLeftRadius: 6,
+ },
+
+ userBubble: {
+ borderBottomRightRadius: 6,
+ },
+
+ messageText: {
fontSize: 15,
- lineHeight: 22,
+ lineHeight: 20,
+ letterSpacing: 0.2,
},
- userMessage: {
- backgroundColor: '#D567A1',
- borderRadius: 10,
- padding: 12,
- alignSelf: 'flex-end',
- marginBottom: 10,
- maxWidth: '80%',
+
+ botText: {
},
+
userText: {
- color: 'white',
- fontSize: 15,
+ fontWeight: "500",
},
- inputBar: {
- position: 'absolute',
- bottom: 65,
+
+ typingContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 4,
+ },
+
+ typingDot: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ marginRight: 4,
+ },
+
+ // Suggestions
+ suggestionContainer: {
+ position: "absolute",
left: 0,
right: 0,
- flexDirection: 'row',
- padding: 18,
- backgroundColor: '#003340',
- alignItems: 'center',
- },
- hashButton: {
- width: 36,
- height: 36,
- borderRadius: 10,
- backgroundColor: '#D567A1',
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 8,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+
+ suggestionBackground: {
+ ...StyleSheet.absoluteFillObject,
+ borderRadius: 12,
+ borderWidth: 1,
},
- hashButtonActive: {
- backgroundColor: '#738C93', // โ
๋๋ ์ ๋ ์กฐ๊ธ ์งํ ํํฌ ์์
-},
+ suggestionHeader: {
+ marginBottom: 12,
+ },
+
+ suggestionTitle: {
+ fontSize: 16,
+ fontWeight: "600",
+ letterSpacing: 0.3,
+ },
- hashText: {
- color: 'white',
- fontWeight: 'bold',
+ suggestionRow: {
+ paddingVertical: 8,
+ },
+
+ suggestionCard: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 16,
+ borderWidth: 1,
+ marginRight: 12,
+ minWidth: 140,
+ alignItems: "center",
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+
+ suggestionIcon: {
fontSize: 20,
+ marginBottom: 4,
},
- suggestionContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- bottom: 140,
- paddingHorizontal: 20,
- paddingTop: 20,
- paddingBottom: 20,
- gap: 8, // React Native >= 0.71
- backgroundColor: '#738C93',
-},
-
-suggestionPill: {
- backgroundColor: '#e5e5e5',
- paddingVertical: 8,
- paddingHorizontal: 10,
- borderRadius: 20,
- marginRight: 8,
- marginBottom: 4,
-
-},
+ suggestionText: {
+ fontSize: 13,
+ fontWeight: "500",
+ textAlign: "center",
+ lineHeight: 16,
+ marginBottom: 2,
+ },
-suggestionText: {
- fontSize: 14,
- color: '#003340',
-},
+ suggestionCategory: {
+ fontSize: 10,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 8,
+ overflow: "hidden",
+ },
- textInput: {
+ // Input Bar
+ inputBar: {
+ paddingTop: 12,
+ paddingHorizontal: 16,
+ borderTopWidth: 1,
+ },
+
+ inputContainer: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ gap: 8,
+ minHeight: 44,
+ },
+
+ suggestionButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+
+ suggestionButtonIcon: {
+ fontSize: 18,
+ },
+
+ textInputContainer: {
flex: 1,
- backgroundColor: '#3D5B66',
- borderRadius: 10,
- paddingHorizontal: 15,
- paddingVertical: 10,
- color: 'white',
- fontSize: 15,
- marginRight: 8,
+ borderRadius: 22,
+ borderWidth: 1,
+ minHeight: 44,
+ maxHeight: 120,
+ justifyContent: "center",
},
- // searchButton: {
- // padding: 8,
- // },
- searchText: {
- fontSize: 20,
- color: 'white',
+
+ textInput: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ fontSize: INPUT_FONT_SIZE,
+ lineHeight: 20,
+ letterSpacing: 0.2,
+ minHeight: 44,
+ },
+
+ sendButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+
+ sendButtonActive: {
+ transform: [{ scale: 1.05 }],
},
});
diff --git a/src/screens/Guide/GuideLevel1.js b/src/screens/Guide/GuideLevel1.js
index 6aabfd2..7cd95ee 100644
--- a/src/screens/Guide/GuideLevel1.js
+++ b/src/screens/Guide/GuideLevel1.js
@@ -1,5 +1,5 @@
-// GuideLevel1.js
-import React, { useState, useCallback } from 'react';
+// GuideLevel1.js - Updated Header Row
+import React, { useState } from 'react';
import {
View,
ScrollView,
@@ -10,6 +10,7 @@ import {
Alert,
} from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
// SVG ์์ด์ฝ import
import CheckIcon from '../../assets/icons/studycheck.svg';
@@ -21,31 +22,39 @@ import { getNewAccessToken } from '../../utils/token';
const GuideLevel1 = () => {
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+
const [loading, setLoading] = useState(true);
const [contentProgress, setContentProgress] = useState({});
+ const [error, setError] = useState(null);
const fetchProgress = async () => {
setLoading(true);
- const accessToken = await getNewAccessToken(navigation);
- if (!accessToken) {
- Alert.alert('์ธ์ฆ ์ค๋ฅ', 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
- navigation.navigate('Login');
- return;
- }
+ setError(null);
try {
- const res = await fetch(
- `${API_BASE_URL}progress/level/1/content/`,
- {
- method: 'GET',
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
+ const accessToken = await getNewAccessToken(navigation);
+ if (!accessToken) {
+ Alert.alert('์ธ์ฆ ์ค๋ฅ', 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
+ navigation.navigate('Login');
+ return;
+ }
+
+ const res = await fetch(`${API_BASE_URL}progress/level/1/content/`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
if (!res.ok) throw new Error(`Level 1 content fetch failed: ${res.status}`);
+
const data = await res.json();
- setContentProgress(data.content_progress);
+ setContentProgress(data?.content_progress || {});
} catch (err) {
- console.error(err);
+ console.error('Error fetching progress:', err);
+ setError(err.message);
Alert.alert('๋ฐ์ดํฐ ์ค๋ฅ', '์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
} finally {
setLoading(false);
@@ -53,67 +62,80 @@ const GuideLevel1 = () => {
};
useFocusEffect(
- useCallback(() => {
+ React.useCallback(() => {
fetchProgress();
}, [])
);
+ const handleRetry = () => fetchProgress();
+
+ const handlePress = (id) => {
+ navigation.navigate('StudyScreen', { level: 1, contentIndex: id });
+ };
+
if (loading) {
return (
-
+
);
}
+ if (error) {
+ return (
+
+ ๋คํธ์ํฌ ์ค๋ฅ
+
+ ๋ค์ ์๋
+
+
+ );
+ }
+
const entries = Object.entries(contentProgress)
.map(([key, done]) => ({ id: Number(key), done }))
.sort((a, b) => a.id - b.id);
- const firstIncomplete = entries.find(e => !e.done)?.id;
+ const firstIncomplete = entries.find((e) => !e.done)?.id;
return (
-
- navigation.goBack()}
- style={styles.backButton}
- accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
- >
- {'<'}
-
-
- 1๋จ๊ณ
-
-
+
+ {/* ๐น Header Row */}
+
+ navigation.goBack()}
+ style={styles.backButton}
+ accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
+ >
+ {'<'}
+
+
+ 1๋จ๊ณ
+
+
+
{entries.map(({ id, done }, idx) => {
- const isChest = !done && id === firstIncomplete;
+ const isCurrent = !done && id === firstIncomplete;
+
let IconComp;
if (done) IconComp = ;
- else if (isChest) IconComp = ;
+ else if (isCurrent) IconComp = ;
else IconComp = ;
- const clickable = done || isChest;
- const handlePress = () =>
- navigation.navigate('StudyScreen', { level: 1, contentIndex: id });
+ const isClickable = done || isCurrent;
return (
- {clickable ? (
-
+ {isClickable ? (
+ handlePress(id)} activeOpacity={0.7} style={styles.iconTouchable}>
{IconComp}
) : (
- IconComp
+ {IconComp}
)}
{`์ฑํฐ 1-${id}`}
@@ -128,19 +150,25 @@ const GuideLevel1 = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#6DC0D4',
+ backgroundColor: '#6DC0D4', // ๊ธฐ์กด Level1 ํค ์ ์ง
paddingHorizontal: 30,
- paddingTop: 60,
},
center: {
justifyContent: 'center',
alignItems: 'center',
},
+ // ๐น Header Row ์คํ์ผ
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center', // ๊ฐ์ด๋ฐ ๊ธฐ์ค
+ marginTop: 20,
+ marginBottom: 20,
+ },
backButton: {
- position: 'absolute',
- top: 50,
- left: 20,
- zIndex: 10,
+ position: 'absolute', // ์ผ์ชฝ์ ๊ณ ์
+ left: 0,
+ padding: 10, // ํฐ์น ์์ญ ํ๋
},
backText: {
fontSize: 36,
@@ -150,16 +178,14 @@ const styles = StyleSheet.create({
fontSize: 24,
fontWeight: 'bold',
color: '#FFFFFF',
- alignSelf: 'center',
- marginBottom: 20,
},
scrollView: {
- marginTop: 20,
paddingBottom: 60,
+ paddingTop: 20,
},
stepContainer: {
width: '100%',
- marginVertical: 16,
+ marginVertical: 20,
flexDirection: 'row',
alignItems: 'flex-start',
},
@@ -174,15 +200,41 @@ const styles = StyleSheet.create({
stepBox: {
alignItems: 'center',
},
+ iconTouchable: {
+ padding: 10,
+ },
+ iconLocked: {
+ padding: 10,
+ opacity: 0.6,
+ },
chapterLabel: {
- marginTop: 8,
- paddingHorizontal: 10,
- paddingVertical: 4,
+ marginTop: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 6,
backgroundColor: '#FFFFFF40',
borderRadius: 12,
fontSize: 14,
fontWeight: '600',
color: '#003340A0',
+ textAlign: 'center',
+ minWidth: 80,
+ },
+ errorText: {
+ color: '#ffffff',
+ fontSize: 18,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 20,
+ },
+ retryButtonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
},
});
diff --git a/src/screens/Guide/GuideLevel2.js b/src/screens/Guide/GuideLevel2.js
index ae5e0ca..c16f838 100644
--- a/src/screens/Guide/GuideLevel2.js
+++ b/src/screens/Guide/GuideLevel2.js
@@ -1,5 +1,5 @@
-// GuideLevel2.js
-import React, { useEffect, useState } from 'react';
+// GuideLevel2.js - Updated Header Row (consistent with Level1)
+import React, { useState } from 'react';
import {
View,
ScrollView,
@@ -9,7 +9,8 @@ import {
ActivityIndicator,
Alert,
} from 'react-native';
-import { useNavigation } from '@react-navigation/native';
+import { useNavigation, useFocusEffect } from '@react-navigation/native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
// SVG ์์ด์ฝ import
import CheckIcon from '../../assets/icons/studycheck.svg';
@@ -21,12 +22,17 @@ import { getNewAccessToken } from '../../utils/token';
const GuideLevel2 = () => {
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+
const [loading, setLoading] = useState(true);
const [contentProgress, setContentProgress] = useState({});
+ const [error, setError] = useState(null);
+
+ const fetchProgress = async () => {
+ setLoading(true);
+ setError(null);
- useEffect(() => {
- const fetchProgress = async () => {
- setLoading(true);
+ try {
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
Alert.alert('์ธ์ฆ ์ค๋ฅ', 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
@@ -34,92 +40,110 @@ const GuideLevel2 = () => {
return;
}
- try {
- const res = await fetch(
- `${API_BASE_URL}progress/level/2/content/`,
- {
- method: 'GET',
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
- if (!res.ok) throw new Error(`Level 2 content fetch failed: ${res.status}`);
- const data = await res.json();
- setContentProgress(data.content_progress);
- } catch (err) {
- console.error(err);
- Alert.alert('๋ฐ์ดํฐ ์ค๋ฅ', '์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
- } finally {
- setLoading(false);
- }
- };
+ const res = await fetch(`${API_BASE_URL}progress/level/2/content/`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!res.ok) throw new Error(`Level 2 content fetch failed: ${res.status}`);
+
+ const data = await res.json();
+ setContentProgress(data?.content_progress || {});
+ } catch (err) {
+ console.error('Error fetching progress:', err);
+ setError(err.message);
+ Alert.alert('๋ฐ์ดํฐ ์ค๋ฅ', '์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useFocusEffect(
+ React.useCallback(() => {
+ fetchProgress();
+ }, [])
+ );
- fetchProgress();
- }, [navigation]);
+ const handleRetry = () => fetchProgress();
+
+ const handleChapterPress = (contentIndex) => {
+ navigation.navigate('StudyScreen', {
+ level: 2,
+ contentIndex,
+ });
+ };
if (loading) {
return (
-
+
);
}
+ if (error) {
+ return (
+
+ ๋คํธ์ํฌ ์ค๋ฅ
+
+ ๋ค์ ์๋
+
+
+ );
+ }
+
+ // progress ๊ฐ์ฒด๋ฅผ [ { id, done }, ... ] ํํ๋ก ๋ณํ & ์ ๋ ฌ
const entries = Object.entries(contentProgress)
.map(([key, done]) => ({ id: Number(key), done }))
.sort((a, b) => a.id - b.id);
- const firstIncomplete = entries.find(e => !e.done)?.id;
+ const firstIncomplete = entries.find((e) => !e.done)?.id;
return (
-
- navigation.goBack()}
- style={styles.backButton}
- accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
- >
- {'<'}
-
-
- 2๋จ๊ณ
-
-
+
+ {/* ๐น Header Row - Level1๊ณผ ๋์ผํ ๊ตฌ์กฐ */}
+
+ navigation.goBack()}
+ style={styles.backButton}
+ accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
+ >
+ {'<'}
+
+
+ 2๋จ๊ณ
+
+
+
{entries.map(({ id, done }, idx) => {
- const isChest = !done && id === firstIncomplete;
+ const isCurrent = !done && id === firstIncomplete;
+
let IconComp;
- if (done) {
- IconComp = ;
- } else if (isChest) {
- IconComp = ;
- } else {
- IconComp = ;
- }
-
- const clickable = done || isChest;
- const handlePress = () => {
- navigation.navigate('StudyScreen', {
- level: 2,
- contentIndex: id,
- });
- };
+ if (done) IconComp = ;
+ else if (isCurrent) IconComp = ;
+ else IconComp = ;
+
+ const isClickable = done || isCurrent;
return (
- {clickable ? (
-
+ {isClickable ? (
+ handleChapterPress(id)}
+ activeOpacity={0.7}
+ style={styles.iconTouchable}
+ >
{IconComp}
) : (
- IconComp
+ {IconComp}
)}
{`์ฑํฐ 2-${id}`}
@@ -134,19 +158,25 @@ const GuideLevel2 = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#2594B0',
+ backgroundColor: '#2594B0', // ๊ธฐ์กด Level2 ํค ์ ์ง
paddingHorizontal: 30,
- paddingTop: 60,
},
center: {
justifyContent: 'center',
alignItems: 'center',
},
+ // ๐น Header Row ์คํ์ผ - Level1๊ณผ ๋์ผ
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center', // ๊ฐ์ด๋ฐ ๊ธฐ์ค
+ marginTop: 20,
+ marginBottom: 20,
+ },
backButton: {
- position: 'absolute',
- top: 50,
- left: 20,
- zIndex: 10,
+ position: 'absolute', // ์ผ์ชฝ์ ๊ณ ์
+ left: 0,
+ padding: 10, // ํฐ์น ์์ญ ํ๋
},
backText: {
fontSize: 36,
@@ -156,16 +186,14 @@ const styles = StyleSheet.create({
fontSize: 24,
fontWeight: 'bold',
color: '#FFFFFF',
- alignSelf: 'center',
- marginBottom: 20,
},
scrollView: {
- marginTop: 20,
paddingBottom: 60,
+ paddingTop: 20,
},
stepContainer: {
width: '100%',
- marginVertical: 16,
+ marginVertical: 20,
flexDirection: 'row',
alignItems: 'flex-start',
},
@@ -180,16 +208,42 @@ const styles = StyleSheet.create({
stepBox: {
alignItems: 'center',
},
+ iconTouchable: {
+ padding: 10,
+ },
+ iconLocked: {
+ padding: 10,
+ opacity: 0.6,
+ },
chapterLabel: {
- marginTop: 8,
- paddingHorizontal: 10,
- paddingVertical: 4,
+ marginTop: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 6,
backgroundColor: '#FFFFFF40',
borderRadius: 12,
fontSize: 14,
fontWeight: '600',
color: '#003340A0',
+ textAlign: 'center',
+ minWidth: 80,
+ },
+ errorText: {
+ color: '#ffffff',
+ fontSize: 18,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 20,
+ },
+ retryButtonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
},
});
-export default GuideLevel2;
+export default GuideLevel2;
\ No newline at end of file
diff --git a/src/screens/Guide/GuideLevel3.js b/src/screens/Guide/GuideLevel3.js
index a6a7ee4..7cc7f56 100644
--- a/src/screens/Guide/GuideLevel3.js
+++ b/src/screens/Guide/GuideLevel3.js
@@ -1,4 +1,4 @@
-// GuideLevel3.js
+// GuideLevel3.js - Updated Header Row (consistent with Level1)
import React, { useEffect, useState } from 'react';
import {
View,
@@ -8,8 +8,10 @@ import {
TouchableOpacity,
ActivityIndicator,
Alert,
+ Dimensions,
} from 'react-native';
-import { useNavigation } from '@react-navigation/native';
+import { useNavigation, useFocusEffect } from '@react-navigation/native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
// SVG ์์ด์ฝ import
import CheckIcon from '../../assets/icons/studycheck.svg';
@@ -19,14 +21,20 @@ import StudyingIcon from '../../assets/icons/studying.svg';
import { API_BASE_URL } from '../../utils/apiConfig';
import { getNewAccessToken } from '../../utils/token';
+const { width: screenWidth } = Dimensions.get('window');
+
const GuideLevel3 = () => {
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true);
const [contentProgress, setContentProgress] = useState({});
+ const [error, setError] = useState(null);
- useEffect(() => {
- const fetchProgress = async () => {
- setLoading(true);
+ const fetchProgress = async () => {
+ setLoading(true);
+ setError(null);
+
+ try {
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
Alert.alert('์ธ์ฆ ์ค๋ฅ', 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
@@ -34,36 +42,68 @@ const GuideLevel3 = () => {
return;
}
- try {
- const res = await fetch(
- `${API_BASE_URL}progress/level/3/content/`,
- {
- method: 'GET',
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
- if (!res.ok) throw new Error(`Level 3 content fetch failed: ${res.status}`);
- const data = await res.json();
- setContentProgress(data.content_progress);
- } catch (err) {
- console.error(err);
- Alert.alert('๋ฐ์ดํฐ ์ค๋ฅ', '์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
- } finally {
- setLoading(false);
+ const res = await fetch(
+ `${API_BASE_URL}progress/level/3/content/`,
+ {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+
+ if (!res.ok) {
+ throw new Error(`Level 3 content fetch failed: ${res.status}`);
}
- };
+
+ const data = await res.json();
+ setContentProgress(data.content_progress || {});
+ } catch (err) {
+ console.error('Error fetching progress:', err);
+ setError(err.message);
+ Alert.alert('๋ฐ์ดํฐ ์ค๋ฅ', '์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useFocusEffect(
+ React.useCallback(() => {
+ fetchProgress();
+ }, [])
+ );
+ const handleRetry = () => {
fetchProgress();
- }, [navigation]);
+ };
+
+ const handleChapterPress = (contentIndex) => {
+ navigation.navigate('StudyScreen', {
+ level: 3,
+ contentIndex: contentIndex,
+ });
+ };
if (loading) {
return (
-
+
);
}
+ if (error) {
+ return (
+
+ ๋คํธ์ํฌ ์ค๋ฅ
+
+ ๋ค์ ์๋
+
+
+ );
+ }
+
// progress ๊ฐ์ฒด๋ฅผ [ { id, done }, ... ] ํํ๋ก ๋ณํ & ์ ๋ ฌ
const entries = Object.entries(contentProgress)
.map(([key, done]) => ({ id: Number(key), done }))
@@ -72,39 +112,37 @@ const GuideLevel3 = () => {
const firstIncomplete = entries.find(e => !e.done)?.id;
return (
-
- navigation.goBack()}
- style={styles.backButton}
- accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
- >
- {'<'}
-
+
+ {/* ๐น Header Row - Level1๊ณผ ๋์ผํ ๊ตฌ์กฐ */}
+
+ navigation.goBack()}
+ style={styles.backButton}
+ accessibilityLabel="๋ค๋ก๊ฐ๊ธฐ"
+ >
+ {'<'}
+
- 3๋จ๊ณ
+ 3๋จ๊ณ
+
{entries.map(({ id, done }, idx) => {
- const isChest = !done && id === firstIncomplete;
- let IconComp;
+ const isCurrentChapter = !done && id === firstIncomplete;
+ let IconComponent;
+
if (done) {
- IconComp = ;
- } else if (isChest) {
- IconComp = ;
+ IconComponent = ;
+ } else if (isCurrentChapter) {
+ IconComponent = ;
} else {
- IconComp = ;
+ IconComponent = ;
}
- const clickable = done || isChest;
- const handlePress = () => {
- navigation.navigate('StudyScreen', {
- level: 3,
- contentIndex: id,
- });
- };
+ const isClickable = done || isCurrentChapter;
return (
{
]}
>
- {clickable ? (
-
- {IconComp}
+ {isClickable ? (
+ handleChapterPress(id)}
+ activeOpacity={0.7}
+ style={styles.iconTouchable}
+ >
+ {IconComponent}
) : (
- IconComp
+
+ {IconComponent}
+
)}
{`์ฑํฐ 3-${id}`}
@@ -137,17 +181,25 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#037F9F',
paddingHorizontal: 30,
- paddingTop: 60,
},
+
center: {
justifyContent: 'center',
alignItems: 'center',
},
+
+ // ๐น Header Row ์คํ์ผ - Level1๊ณผ ๋์ผ
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center', // ๊ฐ์ด๋ฐ ๊ธฐ์ค
+ marginTop: 20,
+ marginBottom: 20,
+ },
backButton: {
- position: 'absolute',
- top: 50,
- left: 20,
- zIndex: 10,
+ position: 'absolute', // ์ผ์ชฝ์ ๊ณ ์
+ left: 0,
+ padding: 10, // ํฐ์น ์์ญ ํ๋
},
backText: {
fontSize: 36,
@@ -157,40 +209,73 @@ const styles = StyleSheet.create({
fontSize: 24,
fontWeight: 'bold',
color: '#FFFFFF',
- alignSelf: 'center',
- marginBottom: 20,
},
+
scrollView: {
- marginTop: 20,
paddingBottom: 60,
+ paddingTop: 20,
},
+
stepContainer: {
width: '100%',
- marginVertical: 16,
+ marginVertical: 20,
flexDirection: 'row',
alignItems: 'flex-start',
},
+
left: {
justifyContent: 'flex-start',
paddingLeft: '15%',
},
+
right: {
justifyContent: 'flex-end',
paddingRight: '15%',
},
+
stepBox: {
alignItems: 'center',
},
+
+ iconTouchable: {
+ padding: 10, // ํฐ์น ์์ญ ํ๋
+ },
+
+ iconLocked: {
+ padding: 10,
+ opacity: 0.6,
+ },
+
chapterLabel: {
- marginTop: 8,
- paddingHorizontal: 10,
- paddingVertical: 4,
+ marginTop: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 6,
backgroundColor: '#FFFFFF40',
borderRadius: 12,
fontSize: 14,
fontWeight: '600',
color: '#003340A0',
+ textAlign: 'center',
+ minWidth: 80,
+ },
+
+ errorText: {
+ color: '#ffffff',
+ fontSize: 18,
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ retryButton: {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 20,
+ },
+ retryButtonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: '600',
},
});
-export default GuideLevel3;
+export default GuideLevel3;
\ No newline at end of file
diff --git a/src/screens/Guide/GuideScreen.js b/src/screens/Guide/GuideScreen.js
index d2c6148..a8c87cd 100644
--- a/src/screens/Guide/GuideScreen.js
+++ b/src/screens/Guide/GuideScreen.js
@@ -11,24 +11,55 @@ import {
Image,
} from "react-native";
import { useNavigation, useFocusEffect } from "@react-navigation/native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import Icon from "react-native-vector-icons/Feather";
+
import LearningProgressBar from "../../components/LearningProgressBar";
import InspectIcon from "../../assets/icons/stock-inspect.svg";
import ResultIcon from "../../assets/icons/stock-result.svg";
import LockIcon from "../../assets/icons/lock.svg";
-// import QuestionIcon from "../../assets/icons/question.png";
+
import { API_BASE_URL } from "../../utils/apiConfig";
import { getNewAccessToken } from "../../utils/token";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const LEVELS = [1, 2, 3];
const GuideScreen = () => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+ const tabBarHeight = useBottomTabBarHeight();
+
+ // ์๋จ ์ฌ๋ฐฑ: ๊ธฐ๊ธฐ safe-area + ์ถ๊ฐ ๋ง์ง
+ const topGutter = Math.max(insets.top, 0) + 24;
+
const [progressMap, setProgressMap] = useState({});
const [loading, setLoading] = useState(true);
+ // โ
ํํ ๋ฆฌ์ผ: ํค๋ ์ฐ์ธก ์์ด์ฝ์ผ๋ก ์ด๋
useFocusEffect(
useCallback(() => {
+ navigation.setOptions({
+ headerTitle: "ํ์ต ๊ฐ์ด๋",
+ headerStyle: { backgroundColor: theme.background.primary },
+ headerTintColor: theme.text.secondary,
+ headerRight: () => (
+ navigation.navigate("TutorialScreen", { allowSkip: true })}
+ style={{ paddingHorizontal: 12, paddingVertical: 6 }}
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
+ >
+
+
+ ),
+ });
+
const loadAllProgress = async () => {
setLoading(true);
const accessToken = await getNewAccessToken(navigation);
@@ -41,263 +72,384 @@ const GuideScreen = () => {
try {
const map = {};
for (const levelId of LEVELS) {
- const res = await fetch(
- `${API_BASE_URL}progress/level/${levelId}/`,
- {
- method: "GET",
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
- if (!res.ok) {
- throw new Error(`Level ${levelId} fetch failed: ${res.status}`);
- }
+ const res = await fetch(`${API_BASE_URL}progress/level/${levelId}/`, {
+ method: "GET",
+ headers: { Authorization: `Bearer ${accessToken}` },
+ });
+ if (!res.ok) throw new Error(`Level ${levelId} fetch failed: ${res.status}`);
map[levelId] = await res.json();
}
setProgressMap(map);
} catch (err) {
console.error(err);
- Alert.alert(
- "๋ฐ์ดํฐ ์ค๋ฅ",
- "์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
- );
+ Alert.alert("๋ฐ์ดํฐ ์ค๋ฅ", "์งํ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
} finally {
setLoading(false);
}
};
loadAllProgress();
- }, [navigation])
+ }, [navigation, theme])
);
if (loading) {
return (
-
-
+
+
);
}
const ClearButton = ({ label, onPress }) => (
-
+
- {label}
-
+
+ {label}
+
+
);
const UnClearButton = ({ onPress, children }) => (
-
+
{children}
-
+
);
return (
-
- ๐ง ํฌ์ ์ ํ ๊ฒ์ฌํ๊ธฐ
-
- navigation.navigate("TypeExam")}
- >
-
- ์ ํ ๊ฒ์ฌํ๊ธฐ
-
+
+
+ {/* ์ธ๋ผ์ธ ํํ ๋ฆฌ์ผ ์นด๋ */}
navigation.navigate("TypeResult")}
+ activeOpacity={0.85}
+ onPress={() => navigation.navigate("TutorialScreen", { allowSkip: true })}
+ style={[styles.tutorialCard, {
+ backgroundColor: theme.background.card,
+ borderColor: theme.border.light
+ }]}
>
-
- ์ ํ ๊ฒฐ๊ณผ ํ์ธํ๊ธฐ
+
+
+
+
+
+ ํํ ๋ฆฌ์ผ ๋น ๋ฅด๊ฒ ๋ณด๊ธฐ
+
+
+ ํต์ฌ ๊ธฐ๋ฅ์ 1๋ถ ์ปท์ผ๋ก ํ์ด๋ณด๊ธฐ
+
+
+
-
-
- โ๏ธ ์ฃผ์ ์ด๋ณด๋ฅผ ์ํ ํ์ต๊ฐ์ด๋
-
- {LEVELS.map((levelId) => {
- const data = progressMap[levelId] || {
- completed: 0,
- total: 0,
- is_level_completed: false,
- progress_ratio: "0/0",
- };
-
- // 1๋จ๊ณ๋ ํญ์ ์ ๊ธ ํด์ .
- // ๊ทธ ์ธ์๋ ์ด์ ๋จ๊ณ ์๋ฃ ์ฌ๋ถ๋ก ์ ๊ธ ์ํ ๊ฒฐ์
- const prevComplete =
- levelId === 1 || progressMap[levelId - 1]?.is_level_completed;
- const showLockIcon = !prevComplete;
-
- const label = `${levelId}๋จ๊ณ`;
- const onPress = () => navigation.navigate(`GuideLevel${levelId}`);
-
- return (
-
- {data.is_level_completed ? (
-
- ) : (
-
-
- {label}
- {showLockIcon && (
-
- )}
+
+ ๐ง ํฌ์ ์ ํ ๊ฒ์ฌํ๊ธฐ
+
+
+
+ navigation.navigate("TypeExam")}
+ activeOpacity={0.9}
+ >
+
+
+
+
+
+
+ ์ ํ ๊ฒ์ฌํ๊ธฐ
+
+
+ ๊ฐ๋จํ ์ง๋ฌธ์ผ๋ก ํฌ์ ์ฑํฅ ํ์
+
+
+
-
- )}
-
+
+
+ navigation.navigate("TypeResult")}
+ activeOpacity={0.9}
+ >
+
+
+
+
+
+
+ ๊ฒฐ๊ณผ ํ์ธํ๊ธฐ
+
+
+ ๋์ ํฌ์ ์ ํ๊ณผ ์ถ์ฒ ์ ๋ต
+
+
+
- );
- })}
+
+
+
+
+
+
+ โ๏ธ ์ฃผ์ ์ด๋ณด๋ฅผ ์ํ ํ์ต๊ฐ์ด๋
+
+
+
+ {LEVELS.map((levelId) => {
+ const data = progressMap[levelId] || {
+ completed: 0,
+ total: 0,
+ is_level_completed: false,
+ progress_ratio: "0/0",
+ };
+
+ const prevComplete = levelId === 1 || progressMap[levelId - 1]?.is_level_completed;
+ const showLockIcon = !prevComplete;
+
+ const label = `${levelId}๋จ๊ณ`;
+ const onPress = () => navigation.navigate(`GuideLevel${levelId}`);
+
+ return (
+
+ {data.is_level_completed ? (
+
+ ) : (
+
+
+
+ {label}
+
+ {showLockIcon && }
+
+
+ )}
+
+
+
+ );
+ })}
+
-
- navigation.navigate("TutorialScreen", { allowSkip: true })}
- activeOpacity={0.7}
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
- style={styles.fabImageWrapper} // (์ต์
) ๊ทธ๋ฆผ์๋ง ์ฃผ๋ ๋ํผ
- >
-
-
- ํํ ๋ฆฌ์ผ
-
);
};
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#003340",
- paddingHorizontal: 30,
- paddingTop: 60,
+ container: {
+ flex: 1
},
- center: {
- justifyContent: "center",
+
+ scrollContent: {
+ paddingHorizontal: 20
+ },
+
+ center: {
+ justifyContent: "center",
+ alignItems: "center"
+ },
+
+ // ์ธ๋ผ์ธ ํํ ๋ฆฌ์ผ ์นด๋
+ tutorialCard: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 14,
+ padding: 12,
+ borderWidth: 1,
+ marginBottom: 16,
+ },
+ tutorialCardLeft: {
+ width: 44,
+ height: 44,
+ borderRadius: 10,
alignItems: "center",
+ justifyContent: "center",
+ },
+ tutorialTitle: {
+ fontSize: 14.5,
+ fontWeight: "600",
+ letterSpacing: 0.2,
},
+ tutorialDesc: {
+ marginTop: 2,
+ fontSize: 12.5,
+ },
+
title: {
- color: "#EEEEEE",
- fontSize: 18,
- marginBottom: 20,
- marginLeft: 15,
+ fontSize: 17,
+ marginBottom: 15,
+ fontWeight: "500",
+ textAlign: "left",
+ marginLeft: 4,
marginTop: 5,
- fontWeight: "600",
+ letterSpacing: 0.2,
},
+
buttonContainer: {
- flexDirection: "row",
- justifyContent: "space-between",
+ gap: 12,
marginBottom: 10,
},
+
examButton: {
- flex: 1,
- aspectRatio: 1,
- backgroundColor: "#6EE69EE0",
- borderRadius: 20,
- marginHorizontal: 10,
- alignItems: "center",
- justifyContent: "center",
- padding: 16,
+ borderRadius: 16,
+ padding: 20,
+ borderWidth: 1,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 6,
},
+
resultButton: {
- flex: 1,
- aspectRatio: 1,
- backgroundColor: "#F074BAE0",
- borderRadius: 20,
- marginHorizontal: 10,
+ borderRadius: 16,
+ padding: 20,
+ borderWidth: 1,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 6,
+ },
+
+ examButtonContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+
+ examIconContainer: {
+ width: 80,
+ height: 75,
+ borderRadius: 16,
+ justifyContent: "flex-end",
+ alignItems: "center",
+ },
+
+ resultIconContainer: {
+ width: 80,
+ height: 75,
+ borderRadius: 16,
+ justifyContent: "flex-end",
alignItems: "center",
- justifyContent: "center",
- padding: 16,
},
- buttonText: {
- fontFamily: "System",
- color: "#EFF1F5",
- fontSize: 15,
+
+ examTextContainer: {
+ flex: 1,
+ marginLeft: 16,
+ marginRight: 12,
+ },
+
+ examButtonTitle: {
+ fontSize: 17,
fontWeight: "600",
- marginTop: 10,
+ marginBottom: 4,
+ letterSpacing: 0.3,
+ },
+
+ examButtonSubtitle: {
+ fontSize: 13,
+ fontWeight: "400",
+ lineHeight: 18,
},
+
divider: {
height: 1,
- backgroundColor: "#4A5A60",
- marginVertical: 20,
+ marginVertical: 25,
},
- menuContainer: {
- paddingBottom: 30,
+
+ menuContainer: {
+ paddingBottom: 10
},
- menuRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
+ levelBlock: {
+ marginBottom: 8
},
+ menuRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center"
+ },
+
clearButton: {
- backgroundColor: "#D4DDEF60",
- padding: 15,
- borderRadius: 15,
- marginVertical: 5,
- marginHorizontal: 10,
+ borderRadius: 12,
+ paddingVertical: 16,
+ paddingHorizontal: 18,
+ marginVertical: 6,
+ borderWidth: 1,
},
unclearButton: {
- backgroundColor: "#D4DDEF20",
- padding: 15,
- borderRadius: 15,
- marginVertical: 5,
- marginHorizontal: 10,
- },
- labelWithIcon: {
- flexDirection: "row",
- alignItems: "center",
+ borderRadius: 12,
+ paddingVertical: 16,
+ paddingHorizontal: 18,
+ marginVertical: 6,
+ borderWidth: 1,
},
- lockIcon: {
- marginLeft: 6,
- marginTop: 1,
- },
- menuText: {
- fontSize: 16,
- color: "white",
- fontWeight: "bold",
- },
- fabContainer: {
- position: "absolute",
- right: 30,
- bottom: 100,
- alignItems: "center",
+
+ labelWithIcon: {
+ flexDirection: "row",
+ alignItems: "center"
},
- // (์ ํ) ์ด๋ฏธ์ง์ ์ด์ง ๊ทธ๋ฆผ์ ์ฃผ๊ณ ์ถ์ผ๋ฉด ์ฌ์ฉ, ์๋๋ฉด ์ญ์ ํด๋ ๋จ
- // fabImageWrapper: {
- // shadowColor: "#000",
- // shadowOffset: { width: 0, height: 2 },
- // shadowOpacity: 0.2,
- // shadowRadius: 3,
- // elevation: 3,
- // },
- // PNG ์์ฒด๊ฐ ๋ฒํผ์ด๋ฏ๋ก ๋ฐฐ๊ฒฝ/ํ
๋๋ฆฌ ์์
- fabImage: {
- width: 56, // ์๋ณธ ํฌ๊ธฐ๋ฅผ ์ฐ๊ณ ์ถ์ผ๋ฉด ์ด ๋ ์ค ์ง์๋ ๋ฉ๋๋ค
- height: 56,
+ lockIcon: {
+ marginLeft: 6,
+ marginTop: 1
},
- fabLabel: {
- marginTop: 6,
- fontSize: 12,
- color: "#EEEEEE",
+
+ menuText: {
+ fontSize: 17,
+ fontWeight: "500",
+ letterSpacing: 0.2
},
});
-export default GuideScreen;
+export default GuideScreen;
\ No newline at end of file
diff --git a/src/screens/Guide/StudyScreen.js b/src/screens/Guide/StudyScreen.js
index 80e9272..1f19af9 100644
--- a/src/screens/Guide/StudyScreen.js
+++ b/src/screens/Guide/StudyScreen.js
@@ -1,22 +1,29 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState, useCallback } from "react";
import {
View,
Text,
ScrollView,
TouchableOpacity,
ActivityIndicator,
- Animated,
StyleSheet,
Alert,
+ Dimensions,
+ Share,
+ Modal,
} from "react-native";
-import { useRoute, useNavigation } from "@react-navigation/native";
+import { useRoute, useNavigation, useFocusEffect } from "@react-navigation/native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import Icon from "react-native-vector-icons/Feather";
import Markdown from "react-native-markdown-display";
import { API_BASE_URL } from "../../utils/apiConfig";
import { getNewAccessToken } from "../../utils/token";
+const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
+
const StudyScreen = () => {
const { level, contentIndex } = useRoute().params;
const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
// map level & contentIndex โ advanced-guide id
const guideApiId =
@@ -29,217 +36,662 @@ const StudyScreen = () => {
const [content, setContent] = useState("");
const [loading, setLoading] = useState(true);
const [completing, setCompleting] = useState(false);
+ const [error, setError] = useState(null);
+ const [showMenu, setShowMenu] = useState(false); // ๋ฉ๋ด ๋ชจ๋ฌ ์ํ
+ const [fontSize, setFontSize] = useState(16); // ๊ธ๊ผด ํฌ๊ธฐ ์ํ
const [progressIndex, setProgressIndex] = useState(0);
- const [showButton, setShowButton] = useState(true);
- const buttonAnim = useRef(new Animated.Value(1)).current;
- const scrollOffset = useRef(0);
+ const scrollViewRef = useRef(null);
// fetch guide content
- useEffect(() => {
- const fetchGuide = async () => {
+ const fetchGuide = async () => {
+ setLoading(true);
+ setError(null);
+
+ try {
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
Alert.alert("์ธ์ฆ ์ค๋ฅ", "ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.");
navigation.navigate("Login");
return;
}
- try {
- const res = await fetch(
- `${API_BASE_URL}api/advanced-guides/${guideApiId}/`,
- {
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
- if (!res.ok) throw new Error(`Failed to load (${res.status})`);
- const data = await res.json();
- setContent(data.content);
- } catch (err) {
- console.error(err);
- setContent("[๋ถ๋ฌ์ค๊ธฐ ์คํจ]");
- } finally {
- setLoading(false);
+
+ const res = await fetch(
+ `${API_BASE_URL}api/advanced-guides/${guideApiId}/`,
+ {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+
+ if (!res.ok) {
+ throw new Error(`Failed to load guide (${res.status})`);
}
- };
+
+ const data = await res.json();
+ setContent(data.content || "์ฝํ
์ธ ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.");
+ } catch (err) {
+ console.error("Error fetching guide:", err);
+ setError(err.message);
+ setContent("์ฝํ
์ธ ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
fetchGuide();
- }, [guideApiId, navigation]);
+ }, [guideApiId]);
- // handle scroll for progress bar & button
- const handleScroll = (e) => {
+ // handle scroll for progress bar only
+ const handleScroll = useCallback((e) => {
const y = e.nativeEvent.contentOffset.y;
const scrollH = e.nativeEvent.contentSize.height - e.nativeEvent.layoutMeasurement.height;
- const pct = scrollH > 0 ? y / scrollH : 0;
- setProgressIndex(Math.min(5, Math.floor(pct * 5)));
-
- const dir = y > scrollOffset.current ? "down" : "up";
- if (dir === "down" && showButton) {
- setShowButton(false);
- Animated.timing(buttonAnim, { toValue: 0, duration: 200, useNativeDriver: true }).start();
- } else if (dir === "up" && !showButton) {
- setShowButton(true);
- Animated.timing(buttonAnim, { toValue: 1, duration: 200, useNativeDriver: true }).start();
- }
- scrollOffset.current = y;
- };
+ const pct = scrollH > 0 ? Math.max(0, Math.min(1, y / scrollH)) : 0;
+ setProgressIndex(Math.min(10, Math.floor(pct * 10)));
+ }, []);
// mark complete
const handleComplete = async () => {
+ if (completing) return;
+
setCompleting(true);
- const accessToken = await getNewAccessToken(navigation);
- if (!accessToken) {
- Alert.alert("์ธ์ฆ ์ค๋ฅ", "ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.");
- navigation.navigate("Login");
- return;
- }
try {
+ const accessToken = await getNewAccessToken(navigation);
+ if (!accessToken) {
+ Alert.alert("์ธ์ฆ ์ค๋ฅ", "ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.");
+ navigation.navigate("Login");
+ return;
+ }
+
const res = await fetch(
`${API_BASE_URL}progress/complete/${level}/${contentIndex}/`,
{
method: "POST",
- headers: { Authorization: `Bearer ${accessToken}` },
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
}
);
- if (!res.ok) throw new Error(`Complete failed (${res.status})`);
- // ๋์๊ฐ์ GuideLevel ํ๋ฉด์ด ๋ฆฌํ๋ ์๋๋๋ก
- navigation.goBack();
+
+ if (!res.ok) {
+ throw new Error(`Complete failed (${res.status})`);
+ }
+
+ // ์ฑ๊ณต ํผ๋๋ฐฑ
+ Alert.alert(
+ "ํ์ต ์๋ฃ!",
+ "์ฑํฐ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃํ์ต๋๋ค.",
+ [
+ {
+ text: "ํ์ธ",
+ onPress: () => navigation.goBack(),
+ }
+ ]
+ );
} catch (err) {
- console.error(err);
- Alert.alert("์ค๋ฅ", "์๋ฃ ์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+ console.error("Error completing:", err);
+ Alert.alert("์ค๋ฅ", "์๋ฃ ์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.");
} finally {
setCompleting(false);
}
};
+ const handleRetry = () => {
+ fetchGuide();
+ };
+
+ // ๋ฉ๋ด ๊ธฐ๋ฅ๋ค
+ const handleShare = async () => {
+ try {
+ await Share.share({
+ message: `์ฑํฐ ${level}-${contentIndex} ํ์ต ์ฝํ
์ธ ๋ฅผ ๊ณต์ ํฉ๋๋ค!`,
+ title: 'ํ์ต ์ฝํ
์ธ ๊ณต์ ',
+ });
+ } catch (error) {
+ console.error('๊ณต์ ์ค๋ฅ:', error);
+ }
+ setShowMenu(false);
+ };
+
+ const handleBookmark = () => {
+ Alert.alert("๋ถ๋งํฌ", "์ด ์ฑํฐ๋ฅผ ๋ถ๋งํฌ์ ์ถ๊ฐํ์ต๋๋ค!", [
+ { text: "ํ์ธ", onPress: () => setShowMenu(false) }
+ ]);
+ };
+
+ const handleScrollToTop = () => {
+ scrollViewRef.current?.scrollTo({ y: 0, animated: true });
+ setShowMenu(false);
+ };
+
+ const handleFontSizeChange = (size) => {
+ setFontSize(size);
+ setShowMenu(false);
+ };
+
+ const handleReport = () => {
+ Alert.alert(
+ "์ฝํ
์ธ ์ ๊ณ ",
+ "์ด ์ฝํ
์ธ ์ ๋ฌธ์ ๊ฐ ์๋์?",
+ [
+ { text: "์ทจ์", style: "cancel" },
+ { text: "์ ๊ณ ํ๊ธฐ", onPress: () => {
+ Alert.alert("์ ๊ณ ์๋ฃ", "์ ๊ณ ๊ฐ ์ ์๋์์ต๋๋ค. ๊ฒํ ํ ์กฐ์นํ๊ฒ ์ต๋๋ค.");
+ setShowMenu(false);
+ }}
+ ]
+ );
+ };
+
if (loading) {
return (
-
+
+ ํ์ต ์ฝํ
์ธ ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ ์ฝํ
์ธ ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค
+
+ ๋ค์ ์๋
+
);
}
return (
-
- {/* header */}
+
+ {/* Header */}
- navigation.goBack()} style={styles.backButton}>
- {"<"}
+ navigation.goBack()}
+ style={styles.backButton}
+ activeOpacity={0.7}
+ >
+
+
- {`${level}-${contentIndex}`}
+ ์ฑํฐ {level}-{contentIndex}
ํ์ต ์ฝํ
์ธ
-
+
+
+ setShowMenu(true)}
+ >
+
+
+
- {/* progress bar */}
-
+ {/* Progress Bar */}
+
+
+ {Math.round((progressIndex / 10) * 100)}%
+
- {/* body */}
+ {/* Content */}
- {content}
+
+ {content}
+
+
+ {/* Complete Button - ํ์ด์ง ํ๋จ์ ๊ณ ์ */}
+
+
+ {completing ? (
+
+
+ ์ฒ๋ฆฌ ์ค...
+
+ ) : (
+
+
+ ํ์ต์ ์๋ฃํ์ด์
+
+ )}
+
+
- {/* complete button */}
- setShowMenu(false)}
>
setShowMenu(false)}
>
- {completing ? (
-
- ) : (
- ํ์ต์ ์๋ฃํ์ด์
- )}
+
+ {/*
+
+ ๋ถ๋งํฌ ์ถ๊ฐ
+ */}
+
+
+
+ ๊ณต์ ํ๊ธฐ
+
+
+
+
+ ๋งจ ์๋ก
+
+
+
+
+
+ ๊ธ๊ผด ํฌ๊ธฐ
+
+ handleFontSizeChange(14)}
+ >
+ ์๊ฒ
+
+ handleFontSizeChange(16)}
+ >
+ ๋ณดํต
+
+ handleFontSizeChange(18)}
+ >
+ ํฌ๊ฒ
+
+
+
+
+
+
+
+
+ ๋ฌธ์ ์ ๊ณ
+
+
-
+
);
};
const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: "#F7F9FA" },
- center: { flex: 1, justifyContent: "center", alignItems: "center" },
+ container: {
+ flex: 1,
+ backgroundColor: "#F8FAFB"
+ },
+
+ center: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: 20,
+ },
+
+ loadingText: {
+ color: "#666",
+ fontSize: 16,
+ marginTop: 16,
+ textAlign: "center",
+ },
+
+ errorText: {
+ color: "#666",
+ fontSize: 18,
+ textAlign: "center",
+ marginVertical: 16,
+ },
+
+ retryButton: {
+ backgroundColor: "#00AACC",
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 24,
+ marginTop: 16,
+ },
+
+ retryButtonText: {
+ color: "#fff",
+ fontSize: 16,
+ fontWeight: "600",
+ },
+
header: {
flexDirection: "row",
alignItems: "center",
- paddingTop: 50,
paddingHorizontal: 20,
- paddingBottom: 10,
- backgroundColor: "#E0F4F9",
+ paddingVertical: 16,
+ backgroundColor: "#E8F4F8",
+ borderBottomWidth: 1,
+ borderBottomColor: "rgba(0, 51, 64, 0.1)",
+ },
+
+ backButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: "rgba(255, 255, 255, 0.8)",
+ alignItems: "center",
+ justifyContent: "center",
+ marginRight: 12,
+ },
+
+ headerTitleContainer: {
+ flex: 1,
+ alignItems: "center"
+ },
+
+ chapterNumber: {
+ fontSize: 14,
+ color: "#666",
+ fontWeight: "500",
+ },
+
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: "#003340",
+ marginTop: 2,
+ },
+
+ headerRight: {
+ width: 40,
+ alignItems: "center",
+ },
+
+ menuButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: "rgba(255, 255, 255, 0.8)",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+
+ progressContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ backgroundColor: "#fff",
},
- backButton: { marginRight: 10 },
- backText: { fontSize: 28, color: "#003340" },
- headerTitleContainer: { flex: 1, alignItems: "center" },
- chapterNumber: { fontSize: 14, color: "#003340" },
- headerTitle: { fontSize: 18, fontWeight: "bold", color: "#003340" },
progressBarContainer: {
- height: 6,
- width: "80%",
- backgroundColor: "#D0DCE0",
- borderRadius: 3,
- marginTop: 10,
- marginBottom: 6,
+ flex: 1,
+ height: 8,
+ backgroundColor: "#E5F3F6",
+ borderRadius: 4,
+ marginRight: 12,
},
+
progressBarFill: {
- height: 6,
+ height: 8,
backgroundColor: "#00AACC",
- borderRadius: 3,
+ borderRadius: 4,
+ minWidth: 8,
+ },
+
+ progressText: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: "#00AACC",
+ minWidth: 40,
+ textAlign: "right",
},
scrollArea: {
+ flex: 1,
+ },
+
+ scrollContent: {
+ paddingBottom: 20,
+ },
+
+ contentContainer: {
+ backgroundColor: "#fff",
+ marginHorizontal: 0,
+ paddingHorizontal: 20,
+ paddingVertical: 24,
+ },
+
+ completeButtonContainer: {
paddingHorizontal: 20,
- marginTop: 10,
- marginBottom: 70,
+ paddingVertical: 20,
+ backgroundColor: "#F8FAFB",
},
completeButton: {
+ backgroundColor: "#00AACC",
+ paddingVertical: 16,
+ borderRadius: 16,
+ alignItems: "center",
+ shadowColor: "#00AACC",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 6,
+ },
+
+ completingButton: {
+ backgroundColor: "#0088AA",
+ },
+
+ buttonContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+
+ buttonText: {
+ color: "#fff",
+ fontSize: 16,
+ fontWeight: "600",
+ marginLeft: 8,
+ },
+
+ completingContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+
+ completingText: {
+ color: "#fff",
+ fontSize: 16,
+ fontWeight: "600",
+ marginLeft: 8,
+ },
+
+ // ๋ฉ๋ด ๋ชจ๋ฌ ์คํ์ผ
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
+ },
+
+ menuContainer: {
position: "absolute",
- bottom: 20,
- left: 20,
right: 20,
- backgroundColor: "#003340",
- paddingVertical: 14,
- borderRadius: 10,
+ backgroundColor: "#fff",
+ borderRadius: 12,
+ paddingVertical: 8,
+ minWidth: 180,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.15,
+ shadowRadius: 12,
+ elevation: 8,
+ },
+
+ menuItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+
+ menuItemText: {
+ fontSize: 15,
+ color: "#333",
+ marginLeft: 12,
+ fontWeight: "500",
+ },
+
+ menuDivider: {
+ height: 1,
+ backgroundColor: "#E5E7EB",
+ marginHorizontal: 12,
+ marginVertical: 4,
+ },
+
+ fontSizeContainer: {
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+
+ fontSizeTitle: {
+ fontSize: 14,
+ color: "#666",
+ marginBottom: 8,
+ fontWeight: "500",
+ },
+
+ fontSizeButtons: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+
+ fontSizeButton: {
+ flex: 1,
+ paddingVertical: 6,
alignItems: "center",
+ marginHorizontal: 2,
+ borderRadius: 6,
+ backgroundColor: "#F3F4F6",
+ },
+
+ fontSizeButtonActive: {
+ backgroundColor: "#00AACC",
+ },
+
+ fontSizeButtonText: {
+ fontSize: 13,
+ color: "#666",
+ fontWeight: "500",
+ },
+
+ fontSizeButtonTextActive: {
+ color: "#fff",
},
- buttonText: { color: "#fff", fontSize: 16, fontWeight: "bold" },
});
const markdownStyles = {
- body: { fontSize: 16, lineHeight: 26, color: "#333" },
- heading1: { fontSize: 22, fontWeight: "bold", marginTop: 20, marginBottom: 8 },
- heading2: { fontSize: 20, fontWeight: "bold", marginTop: 18, marginBottom: 6 },
- list_item: { flexDirection: "row", alignItems: "flex-start" },
+ body: {
+ fontSize: 16,
+ lineHeight: 28,
+ color: "#333",
+ fontFamily: "System",
+ },
+
+ heading1: {
+ fontSize: 24,
+ fontWeight: "700",
+ marginTop: 24,
+ marginBottom: 12,
+ color: "#003340",
+ },
+
+ heading2: {
+ fontSize: 20,
+ fontWeight: "600",
+ marginTop: 20,
+ marginBottom: 10,
+ color: "#003340",
+ },
+
+ heading3: {
+ fontSize: 18,
+ fontWeight: "600",
+ marginTop: 16,
+ marginBottom: 8,
+ color: "#003340",
+ },
+
+ paragraph: {
+ marginBottom: 16,
+ lineHeight: 28,
+ },
+
+ list_item: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ marginBottom: 8,
+ },
+
+ bullet_list: {
+ marginBottom: 16,
+ },
+
+ code_inline: {
+ backgroundColor: "#F5F7FA",
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 4,
+ fontSize: 14,
+ color: "#00AACC",
+ },
+
+ code_block: {
+ backgroundColor: "#F5F7FA",
+ padding: 16,
+ borderRadius: 8,
+ marginVertical: 12,
+ },
+
+ blockquote: {
+ backgroundColor: "#F8FAFB",
+ borderLeftWidth: 4,
+ borderLeftColor: "#00AACC",
+ paddingLeft: 16,
+ paddingVertical: 12,
+ marginVertical: 12,
+ },
};
-export default StudyScreen;
+export default StudyScreen;
\ No newline at end of file
diff --git a/src/screens/Guide/TypeExamScreen.js b/src/screens/Guide/TypeExamScreen.js
index 9b1e63f..0e5c2b7 100644
--- a/src/screens/Guide/TypeExamScreen.js
+++ b/src/screens/Guide/TypeExamScreen.js
@@ -10,25 +10,25 @@ import {
import { API_BASE_URL } from "../../utils/apiConfig";
import { getNewAccessToken } from "../../utils/token";
-// ์ง๋ฌธ ๋ฐ์ดํฐ๋ API์์ ๋ถ๋ฌ์ฌ ์์
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
const TypeExamScreen = ({ navigation }) => {
- // ์ง๋ฌธ ๋ฐ์ดํฐ์ ํ์ฌ ์ํ๋ฅผ ์ ์ฅํ๋ ์ํ ๋ณ์๋ค
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const [questions, setQuestions] = useState([]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
- // ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ง๋ฌธ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
useEffect(() => {
fetchQuestions();
}, []);
- // ์ง๋ฌธ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๋ ํจ์
const fetchQuestions = async () => {
try {
- // ์ธ์ฆ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
console.error("์ก์ธ์ค ํ ํฐ์ด ์์ต๋๋ค.");
@@ -63,30 +63,22 @@ const TypeExamScreen = ({ navigation }) => {
}
};
- // ๋ต๋ณ ์ ํ ํธ๋ค๋ฌ
const handleSelectOption = (option) => {
- // ํ์ฌ ๋ต๋ณ ๋ฐฐ์ด ๋ณต์ฌ๋ณธ ์์ฑ
const newAnswers = [...answers];
- // ํ์ฌ ์ง๋ฌธ์ ๋ํ ๋ต๋ณ ์ ์ฅ
newAnswers[currentQuestionIndex] = option;
setAnswers(newAnswers);
- // ๋ง์ง๋ง ์ง๋ฌธ์ธ์ง ํ์ธ
if (currentQuestionIndex === questions.length - 1) {
- // ๋ง์ง๋ง ์ง๋ฌธ์ด๋ฉด ๊ฒฐ๊ณผ ์ ์ถ
submitAnswers(newAnswers);
} else {
- // ๋ค์ ์ง๋ฌธ์ผ๋ก ์ด๋
setCurrentQuestionIndex(currentQuestionIndex + 1);
}
};
- // ๋ต๋ณ ์ ์ถ ํจ์
const submitAnswers = async (finalAnswers) => {
setIsSubmitting(true);
try {
- // ์ธ์ฆ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
console.error("์ก์ธ์ค ํ ํฐ์ด ์์ต๋๋ค.");
@@ -96,7 +88,6 @@ const TypeExamScreen = ({ navigation }) => {
return;
}
- // API ์๋ํฌ์ธํธ์ ๋ต๋ณ ์ ์ถ
const response = await fetch(`${API_BASE_URL}mbti/result/`, {
method: "POST",
headers: {
@@ -112,7 +103,6 @@ const TypeExamScreen = ({ navigation }) => {
throw new Error(`์๋ฒ ์๋ต์ด ์ ์์ ์ด์ง ์์ต๋๋ค: ${response.status}`);
}
- // ์ฑ๊ณต์ ์ผ๋ก ์ ์ถ๋๋ฉด ๊ฒฐ๊ณผ ํ๋ฉด์ผ๋ก ์ด๋
navigation.reset({
index: 0,
routes: [
@@ -130,34 +120,36 @@ const TypeExamScreen = ({ navigation }) => {
}
};
- // ๋ก๋ฉ ์ค์ด๊ฑฐ๋ ์ง๋ฌธ์ด ์์ ๊ฒฝ์ฐ
if (isLoading || questions.length === 0) {
return (
-
-
- ์ง๋ฌธ์ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+
+ ์ง๋ฌธ์ ๋ถ๋ฌ์ค๋ ์ค...
+
);
}
- // ํ์ฌ ์ง๋ฌธ ๊ฐ์ฒด
const currentQuestion = questions[currentQuestionIndex];
-
- // ์งํ ์ํฉ ๊ณ์ฐ (์: "3 / 12")
const progressText = `${currentQuestionIndex + 1} / ${questions.length}`;
return (
-
+
{isSubmitting ? (
-
- ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ ์ค์
๋๋ค...
+
+
+ ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ ์ค์
๋๋ค...
+
) : (
<>
- {progressText}
-
+
+ {progressText}
+
+
{
width: `${
((currentQuestionIndex + 1) / questions.length) * 100
}%`,
+ backgroundColor: theme.status.success,
},
]}
/>
@@ -172,26 +165,26 @@ const TypeExamScreen = ({ navigation }) => {
-
+
{currentQuestion.question_text}
handleSelectOption("A")}
>
-
+
A. {currentQuestion.option_a}
handleSelectOption("B")}
>
-
+
B. {currentQuestion.option_b}
@@ -205,7 +198,6 @@ const TypeExamScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
padding: 20,
},
progressContainer: {
@@ -213,20 +205,17 @@ const styles = StyleSheet.create({
marginBottom: 20,
},
progressText: {
- color: "#FFFFFF",
fontSize: 16,
marginBottom: 5,
textAlign: "right",
},
progressBar: {
height: 8,
- backgroundColor: "#D4DDEF20",
borderRadius: 4,
overflow: "hidden",
},
progressFill: {
height: "100%",
- backgroundColor: "#6EE69E",
borderRadius: 4,
},
questionContainer: {
@@ -234,7 +223,6 @@ const styles = StyleSheet.create({
alignItems: "center",
},
questionText: {
- color: "#FFFFFF",
fontSize: 22,
fontWeight: "600",
textAlign: "center",
@@ -243,13 +231,11 @@ const styles = StyleSheet.create({
marginTop: 20,
},
optionButton: {
- backgroundColor: "#D4DDEF60",
padding: 20,
borderRadius: 15,
marginVertical: 10,
},
optionText: {
- color: "#FFFFFF",
fontSize: 16,
},
loadingContainer: {
@@ -258,10 +244,9 @@ const styles = StyleSheet.create({
alignItems: "center",
},
loadingText: {
- color: "#FFFFFF",
fontSize: 18,
marginTop: 20,
},
});
-export default TypeExamScreen;
+export default TypeExamScreen;
\ No newline at end of file
diff --git a/src/screens/Guide/TypeResultScreen.js b/src/screens/Guide/TypeResultScreen.js
index fcb138c..47dedd8 100644
--- a/src/screens/Guide/TypeResultScreen.js
+++ b/src/screens/Guide/TypeResultScreen.js
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import {
View,
Text,
- StyleSheet,
+ StyleSheet as RNStyleSheet,
Image,
ScrollView,
TouchableOpacity,
@@ -17,21 +17,24 @@ import ViewShot from "react-native-view-shot";
import { API_BASE_URL } from "../../utils/apiConfig";
import { getNewAccessToken } from "../../utils/token";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const TypeResultScreen = ({ navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const [result, setResult] = useState(null);
const [recommendations, setRecommendations] = useState(null);
const [loading, setLoading] = useState(true);
const viewShotRef = useRef();
useEffect(() => {
- // ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
fetchResultAndRecommendations();
}, []);
- // ๊ฒฐ๊ณผ์ ์ถ์ฒ ์ ๋ณด๋ฅผ ํจ๊ป ๊ฐ์ ธ์ค๋ ํจ์
const fetchResultAndRecommendations = async () => {
try {
- // ์ธ์ฆ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const accessToken = await getNewAccessToken(navigation);
if (!accessToken) {
console.error("์ก์ธ์ค ํ ํฐ์ด ์์ต๋๋ค.");
@@ -43,7 +46,6 @@ const TypeResultScreen = ({ navigation }) => {
console.log("MBTI ๊ฒฐ๊ณผ์ ์ถ์ฒ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ค...");
- // 1. ๋จผ์ ๊ธฐ๋ณธ ๊ฒฐ๊ณผ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const resultResponse = await fetch(
`${API_BASE_URL}/mbti/result/detail/`,
{
@@ -67,25 +69,19 @@ const TypeResultScreen = ({ navigation }) => {
const resultData = JSON.parse(resultText);
console.log("ํ์ฑ๋ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ:", resultData);
- // ์๋ฒ๊ฐ {"result":"RDGQ"} ํํ๋ก ์๋ตํ๋ ๊ฒฝ์ฐ
if (resultData.result && typeof resultData.result === "string") {
mbtiType = resultData.result;
setResult({ type: mbtiType });
- }
- // ์๋ฒ๊ฐ {type: "RDGQ"} ํํ๋ก ์๋ตํ๋ ๊ฒฝ์ฐ
- else if (resultData.type) {
+ } else if (resultData.type) {
mbtiType = resultData.type;
setResult(resultData);
- }
- // ๊ทธ ์ธ์ ์์์น ๋ชปํ ์๋ต ํํ
- else {
+ } else {
console.error("์์์น ๋ชปํ ๊ฒฐ๊ณผ ํํ:", resultData);
throw new Error("๊ฒฐ๊ณผ ๋ฐ์ดํฐ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค");
}
console.log("MBTI ์ ํ ์ฝ๋:", mbtiType);
- // 2. ์ถ์ฒ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const recResponse = await fetch(
`${API_BASE_URL}/mbti/result/recommendations/`,
{
@@ -164,54 +160,45 @@ const TypeResultScreen = ({ navigation }) => {
});
};
- // ์ด๋ฏธ์ง ๋์ ๋ก๋ ํจ์
const getMbtiImage = (mbtiType) => {
if (!mbtiType) return null;
console.log("์ด๋ฏธ์ง ๋ก๋ ์๋:", mbtiType);
- // MBTI ํ์
์ ๋ฐ๋ผ ์ด๋ฏธ์ง ์ ํ
- // React Native์์๋ require() ์ธ์๋ก ๋์ ๋ฌธ์์ด์ ์ฌ์ฉํ ์ ์์
- // ๋ฐ๋ผ์ ๋ชจ๋ ๊ฐ๋ฅํ ์ผ์ด์ค๋ฅผ ์ง์ ๋งคํ
switch (mbtiType) {
- // ์์ ํ(S) ์ ํ๋ค
case "SDGH":
- return require("../../assets/mbti/SDGH.png"); // ๊ผผ๊ผผํ ์ฐ๊ตฌ์
+ return require("../../assets/mbti/SDGH.png");
case "SDGQ":
- return require("../../assets/mbti/SDGQ.png"); // ํ์ค์ ์ธ ๊ธฐํํฌ์ฐฉ๊ฐ
+ return require("../../assets/mbti/SDGQ.png");
case "SDVH":
- return require("../../assets/mbti/SDVH.png"); // ๊ฑฐ๋ถ์ด ์ฐ๊ตฌ์
+ return require("../../assets/mbti/SDVH.png");
case "SDVQ":
- return require("../../assets/mbti/SDVQ.png"); // ์ซ์ ์์ ์ฌ
+ return require("../../assets/mbti/SDVQ.png");
case "SFGH":
- return require("../../assets/mbti/SFGH.png"); // ์ฐ์งํ ์ฑ์ฅ ๋๋ถ
+ return require("../../assets/mbti/SFGH.png");
case "SFGQ":
- return require("../../assets/mbti/SFGQ.png"); // ์๊ฐ์ ๋
ธ๋ฆฌ๋ ํํฐ
+ return require("../../assets/mbti/SFGQ.png");
case "SFVH":
- return require("../../assets/mbti/SFVH.png"); // ์์ ์ ์ธ ํญํด์
+ return require("../../assets/mbti/SFVH.png");
case "SFVQ":
- return require("../../assets/mbti/SFVQ.png"); // ๊ณผ๊ฐํ ํ๋ ์ด์ด
-
- // ๋ชจํํ(R) ์ ํ๋ค
+ return require("../../assets/mbti/SFVQ.png");
case "RDGH":
- return require("../../assets/mbti/RDGH.png"); // ๋ฏธ๋์ ์ ๋์ฝ ์ฐพ๋ ์
+ return require("../../assets/mbti/RDGH.png");
case "RDGQ":
- return require("../../assets/mbti/RDGQ.png"); // ์จ์ ๋ณด์ ๊ฐ๋ณ์ฌ
+ return require("../../assets/mbti/RDGQ.png");
case "RDVH":
- return require("../../assets/mbti/RDVH.png"); // ์ธ๋ด์ฌ ๊ฐํ ํฌ์์
+ return require("../../assets/mbti/RDVH.png");
case "RDVQ":
- return require("../../assets/mbti/RDVQ.png"); // ๋ณํ์ ์ถค๊พผ
+ return require("../../assets/mbti/RDVQ.png");
case "RFGH":
- return require("../../assets/mbti/RFGH.png"); // ๋ฏธ๋๋ฅผ ํฅํ ๊ฐ์ฒ์
+ return require("../../assets/mbti/RFGH.png");
case "RFGQ":
- return require("../../assets/mbti/RFGQ.png"); // ๋ณํ์ ์ ๋์ฃผ์
+ return require("../../assets/mbti/RFGQ.png");
case "RFVH":
- return require("../../assets/mbti/RFVH.png"); // ํ์ ์ฌ๋ฅ๊พผ
+ return require("../../assets/mbti/RFVH.png");
case "RFVQ":
- return require("../../assets/mbti/RFVQ.png"); // ๋ณ๋ ์ถ์ ์
-
+ return require("../../assets/mbti/RFVQ.png");
default:
- // ์ผ์นํ๋ ์ด๋ฏธ์ง๊ฐ ์์ ๊ฒฝ์ฐ ๊ฒฝ๊ณ ํ์
console.warn(`์ด๋ฏธ์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: ${mbtiType}`);
return null;
}
@@ -221,46 +208,51 @@ const TypeResultScreen = ({ navigation }) => {
navigation.navigate('MainTab', { screen: 'Guide' });
};
- // ๋ก๋ฉ ์ค ํ๋ฉด
if (loading) {
return (
-
-
- ๊ฒฐ๊ณผ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+
+ ๊ฒฐ๊ณผ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
);
}
- // ๊ฒฐ๊ณผ๊ฐ ์๋ ๊ฒฝ์ฐ
if (!result || !recommendations) {
return (
-
- ๊ฒฐ๊ณผ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
+
+
+ ๊ฒฐ๊ณผ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
+
navigation.navigate("TypeExam")}
>
- ๋ค์ ํ
์คํธํ๊ธฐ
+
+ ๋ค์ ํ
์คํธํ๊ธฐ
+
);
}
- // ์ด๋ฏธ์ง ๋ก๋ํ๊ธฐ
const mbtiImage = getMbtiImage(recommendations.mbti || result.type);
return (
-
-
+
+
- {'<'}
+ {'<'}
- ๋์ ํฌ์ ์ ํ
-
-
+
+ ๋์ ํฌ์ ์ ํ
+
+
+
@@ -273,103 +265,111 @@ const TypeResultScreen = ({ navigation }) => {
quality: 0.9,
result: "tmpfile",
}}
- style={styles.hiddenViewShot}
+ style={resultStyles.hiddenViewShot}
>
-
- {/* ๋ธ๋๋ฉ ์์ ์ถ๊ฐ */}
- ๋๋ ํฌ์ ์ ํ ํ
์คํธ
+
+ ๋๋ ํฌ์ ์ ํ ํ
์คํธ
-
-
+
+
{recommendations?.mbti || result?.type}
- ๋น์ ์ ํฌ์ ์ ํ์
-
+ ๋น์ ์ ํฌ์ ์ ํ์
+
{recommendations?.alias || "ํฌ์์"}
{mbtiImage ? (
-
+
) : (
-
-
+
+
์ ํ ์ด๋ฏธ์ง๋ฅผ ์ค๋น ์ค์
๋๋ค
)}
{recommendations?.psychology_guide && (
-
-
+
+
{recommendations.psychology_guide}
)}
- ๋๋ ์ฑ์์ ์์ธํ ํ์ธํ์ธ์!
+ ๋๋ ์ฑ์์ ์์ธํ ํ์ธํ์ธ์!
-
-
-
-
+
+
+
+
{recommendations.mbti || result.type}
- ๋น์ ์ ํฌ์ ์ ํ์
-
+
+ ๋น์ ์ ํฌ์ ์ ํ์
+
+
{recommendations.alias || "ํฌ์์"}
{mbtiImage ? (
-
+
) : (
-
-
+
+
์ ํ ์ด๋ฏธ์ง๋ฅผ ์ค๋น ์ค์
๋๋ค.
)}
-
+
{recommendations.psychology_guide && (
-
- ๋น์ ์ ์ํ ์กฐ์ธ
-
+
+
+ ๋น์ ์ ์ํ ์กฐ์ธ
+
+
{recommendations.psychology_guide}
)}
{recommendations.books && recommendations.books.length > 0 && (
-
- ์ถ์ฒ ๋์
+
+
+ ์ถ์ฒ ๋์
+
{recommendations.books.map((book, index) => (
-
+
โข {book}
))}
@@ -377,41 +377,50 @@ const TypeResultScreen = ({ navigation }) => {
)}
{recommendations.websites && recommendations.websites.length > 0 && (
-
- ์ถ์ฒ ์น์ฌ์ดํธ
+
+
+ ์ถ์ฒ ์น์ฌ์ดํธ
+
{recommendations.websites.map((website, index) => (
openLink(website)}
>
- โข {website}
+
+ โข {website}
+
))}
)}
- {recommendations.newsletters &&
- recommendations.newsletters.length > 0 && (
-
- ์ถ์ฒ ๊ธฐ์ฌ
- {recommendations.newsletters.map((newsletter, index) => (
- openLink(newsletter)}
- >
- โข {newsletter}
-
- ))}
-
- )}
+ {recommendations.newsletters && recommendations.newsletters.length > 0 && (
+
+
+ ์ถ์ฒ ๊ธฐ์ฌ
+
+ {recommendations.newsletters.map((newsletter, index) => (
+ openLink(newsletter)}
+ >
+
+ โข {newsletter}
+
+
+ ))}
+
+ )}
-
+
navigation.navigate("TypeExam")}
>
- ๋ค์ ๊ฒ์ฌํ๊ธฐ
+
+ ๋ค์ ๊ฒ์ฌํ๊ธฐ
+
@@ -419,10 +428,9 @@ const TypeResultScreen = ({ navigation }) => {
);
};
-const styles = StyleSheet.create({
+const resultStyles = RNStyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
},
header: {
flexDirection: "row",
@@ -436,7 +444,6 @@ const styles = StyleSheet.create({
padding: 8,
},
headerTitle: {
- color: "#FFFFFF",
fontSize: 18,
fontWeight: "600",
},
@@ -447,16 +454,13 @@ const styles = StyleSheet.create({
flex: 1,
padding: 20,
},
-
- // ViewShot ์จ๊น ์คํ์ผ
hiddenViewShot: {
position: 'absolute',
- left: -9999, // ํ๋ฉด ๋ฐ์ผ๋ก ์ด๋
+ left: -9999,
top: -9999,
- width: 350, // ์ ์ ํ ๊ณต์ ์ด๋ฏธ์ง ํฌ๊ธฐ
- height: 600, // ์ ์ ํ ๊ณต์ ์ด๋ฏธ์ง ๋์ด
+ width: 350,
+ height: 600,
},
-
shareableContent: {
width: 350,
height: 600,
@@ -466,7 +470,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
},
-
shareableMbtiContainer: {
backgroundColor: "#6EE69E",
paddingHorizontal: 20,
@@ -474,19 +477,16 @@ const styles = StyleSheet.create({
borderRadius: 20,
marginBottom: 10,
},
-
shareableMbtiType: {
color: "#003340",
fontSize: 18,
fontWeight: "700",
},
-
shareableLabel: {
fontSize: 16,
color: "#666",
marginBottom: 5,
},
-
shareableNickname: {
color: "#003340",
fontSize: 28,
@@ -494,7 +494,6 @@ const styles = StyleSheet.create({
marginBottom: 20,
textAlign: "center",
},
-
shareableImageContainer: {
width: 200,
height: 200,
@@ -502,12 +501,10 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
},
-
shareableMbtiImage: {
width: "100%",
height: "100%",
},
-
shareableNoImageContainer: {
width: 200,
height: 200,
@@ -517,76 +514,54 @@ const styles = StyleSheet.create({
backgroundColor: "#F0F0F0",
borderRadius: 10,
},
-
shareableNoImageText: {
color: "#666",
fontSize: 14,
fontStyle: "italic",
},
-
shareableGuideContainer: {
marginBottom: 20,
paddingHorizontal: 15,
},
-
shareableGuideText: {
fontSize: 14,
color: "#333",
textAlign: "center",
lineHeight: 20,
},
-
- shareableContainer: {
- backgroundColor: 'transparent',
- borderRadius: 20,
- marginBottom: 20,
- },
appBranding: {
fontSize: 14,
- color: '#666', // ๊ณต์ ์ฉ์ ํ์์ผ๋ก
+ color: '#666',
marginBottom: 15,
fontWeight: '500',
textAlign: 'center',
},
bottomBranding: {
fontSize: 12,
- color: '#999', // ๊ณต์ ์ฉ์ ํ์์ผ๋ก
+ color: '#999',
fontStyle: 'italic',
textAlign: 'center',
},
- additionalInfo: {
- backgroundColor: "#D4DDEF20",
- borderRadius: 20,
- padding: 20,
- alignItems: "center",
- },
-
- // ๊ธฐ์กด ์คํ์ผ
resultCard: {
- backgroundColor: "#D4DDEF20",
borderRadius: 20,
padding: 20,
alignItems: "center",
},
mbtiTypeContainer: {
- backgroundColor: "#6EE69E",
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 20,
marginBottom: 15,
},
mbtiType: {
- color: "#003340",
fontSize: 16,
fontWeight: "700",
},
nicknameTitleLabel: {
- color: "#FFFFFF",
fontSize: 16,
marginBottom: 5,
},
nickname: {
- color: "#FFFFFF",
fontSize: 32,
fontWeight: "bold",
marginBottom: 20,
@@ -609,11 +584,9 @@ const styles = StyleSheet.create({
marginVertical: 20,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#D4DDEF30",
borderRadius: 10,
},
noImageText: {
- color: "#FFFFFF",
fontSize: 16,
fontStyle: "italic",
},
@@ -622,31 +595,23 @@ const styles = StyleSheet.create({
marginTop: 20,
paddingTop: 15,
borderTopWidth: 1,
- borderTopColor: "#D4DDEF40",
},
sectionTitle: {
- color: "#6EE69E",
fontSize: 18,
fontWeight: "bold",
marginBottom: 10,
},
sectionText: {
- color: "#FFFFFF",
fontSize: 16,
lineHeight: 22,
marginBottom: 10,
},
listItem: {
- color: "#FFFFFF",
fontSize: 15,
lineHeight: 22,
marginBottom: 8,
paddingRight: 15,
},
- linkItem: {
- color: "#6EE69E",
- textDecorationLine: "underline",
- },
buttonsContainer: {
marginTop: 30,
marginBottom: 50,
@@ -658,13 +623,9 @@ const styles = StyleSheet.create({
marginVertical: 8,
},
tryAgainButton: {
- backgroundColor: "#F074BA",
- },
- guideButton: {
- backgroundColor: "#6EE69E",
+ // backgroundColor will be set by theme
},
buttonText: {
- color: "#FFFFFF",
fontSize: 16,
fontWeight: "600",
},
@@ -672,30 +633,30 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: "center",
alignItems: "center",
- },
+ },
loadingText: {
- color: "#FFFFFF",
+ //color: "#FFFFFF",
fontSize: 18,
marginTop: 20,
},
errorText: {
- color: "#FFFFFF",
+ //color: "#FFFFFF",
fontSize: 18,
marginBottom: 20,
},
retryButton: {
- backgroundColor: "#6EE69E",
+ //backgroundColor: "#6EE69E",
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 10,
},
retryButtonText: {
- color: "#003340",
+ //color: "#003340",
fontSize: 16,
fontWeight: "bold",
},
backText: {
- color: "#FFFFFF",
+ //color: "#FFFFFF",
fontSize: 24,
fontWeight: "bold",
},
diff --git a/src/screens/Main/AssetDetailScreen.js b/src/screens/Main/AssetDetailScreen.js
index 326a12c..6d5342b 100644
--- a/src/screens/Main/AssetDetailScreen.js
+++ b/src/screens/Main/AssetDetailScreen.js
@@ -13,9 +13,15 @@ import { API_BASE_URL } from "../../utils/apiConfig";
import { getNewAccessToken } from "../../utils/token";
import { Ionicons } from "@expo/vector-icons";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const screenWidth = Dimensions.get("window").width;
const AssetDetailScreen = ({ navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const [assetData, setAssetData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -117,7 +123,7 @@ const AssetDetailScreen = ({ navigation }) => {
return ((value / assetData.total_asset) * 100).toFixed(1) + "%";
};
- // ์ฐจํธ ๋ฐ์ดํฐ - ํํฐ๋ง๋ ๋ฐ์ดํฐ ์ฌ์ฉ
+ // ๐จ ์ฐจํธ ๋ฐ์ดํฐ - ํ
๋ง ์์ ์ ์ฉ
const prepareChartData = () => {
if (
!assetData ||
@@ -127,30 +133,11 @@ const AssetDetailScreen = ({ navigation }) => {
return [];
}
- const chartColors = [
- "#F074BA", // ์์๊ธ : ๋๋ ํํฌ
- "#3B82F6", // ํ๋
- "#34D399", // ์๋ฉ๋๋
- "#10B981", // ๋
น์
- "#F59E0B", // ํฉ์
- "#EF4444", // ๋นจ๊ฐ
- "#6366F1", // ๋ณด๋ผ
- "#8B5CF6", // ์ฐ๋ณด๋ผ
- "#EC4899", // ํํฌ
- "#F87171", // ์ฐ๋นจ๊ฐ
- "#FBBF24", // ์ฃผํฉ
- "#4ADE80", // ์ฐ๋
น์
- "#22D3EE", // ํ๋์
- "#60A5FA", // ์ฐํ๋
- "#A78BFA", // ๋ผ๋ฒค๋
- "#F472B6", // ์ฝ๋ ํํฌ
- ];
-
return assetData.breakdown.map((item, index) => ({
name: item.label,
value: item.value,
- color: chartColors[index % chartColors.length],
- legendFontColor: "#EFF1F5",
+ color: theme.chart.colors[index % theme.chart.colors.length],
+ legendFontColor: theme.text.primary,
legendFontSize: 12,
}));
};
@@ -167,9 +154,11 @@ const AssetDetailScreen = ({ navigation }) => {
// ๋ก๋ฉ
if (loading) {
return (
-
-
- ๋ณด์ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+
+ ๋ณด์ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
);
}
@@ -177,16 +166,23 @@ const AssetDetailScreen = ({ navigation }) => {
// ์๋ฌ
if (error) {
return (
-
- {error}
-
- ๋ค์ ์๋
+
+ {error}
+
+
+ ๋ค์ ์๋
+
navigation.goBack()}
>
- ๋์๊ฐ๊ธฐ
+
+ ๋์๊ฐ๊ธฐ
+
);
@@ -195,13 +191,17 @@ const AssetDetailScreen = ({ navigation }) => {
// ๋ฐ์ดํฐ ์์ ํ๋ฉด
if (!assetData) {
return (
-
- ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค
+
+
+ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค
+
navigation.goBack()}
>
- ๋์๊ฐ๊ธฐ
+
+ ๋์๊ฐ๊ธฐ
+
);
@@ -212,12 +212,14 @@ const AssetDetailScreen = ({ navigation }) => {
// ์ ์ ํ๋ฉด
return (
-
-
+
+
navigation.goBack()}>
- {"<"}
+ {"<"}
- ์ด ์์ฐ ์์ธ
+
+ ์ด ์์ฐ ์์ธ
+
@@ -231,7 +233,7 @@ const AssetDetailScreen = ({ navigation }) => {
height={screenWidth - 60}
chartConfig={{
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
- labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
+ labelColor: (opacity = 1) => theme.text.primary,
}}
accessor="value"
backgroundColor="transparent"
@@ -243,8 +245,10 @@ const AssetDetailScreen = ({ navigation }) => {
center={[screenWidth * 0.23, 0]}
/>
) : (
-
- ์ฐจํธ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค
+
+
+ ์ฐจํธ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค
+
)}
@@ -256,7 +260,7 @@ const AssetDetailScreen = ({ navigation }) => {
-
+
{item.name} {calculatePercentage(item.value)}
@@ -265,29 +269,36 @@ const AssetDetailScreen = ({ navigation }) => {
{/* ๋ณด์ ์ฃผ์ ๋ชฉ๋ก */}
-
+
๋ณด์ ์ฃผ์ ๋ชฉ๋ก{" "}
{ownedStocks.length > 0 && `(${ownedStocks.length}๊ฐ)`}
{ownedStocks.length > 0 ? (
ownedStocks.map((stock, index) => (
-
+
- {stock.label}
-
+
+ {stock.label}
+
+
{calculatePercentage(stock.value)}
-
+
{formatCurrency(stock.value)}์
))
) : (
-
- ๋ณด์ ์ฃผ์์ด ์์ต๋๋ค
-
+
+
+ ๋ณด์ ์ฃผ์์ด ์์ต๋๋ค
+
+
๋ฉ์ธํ๋ฉด์์ ์ฃผ์ ๊ฑฐ๋๋ฅผ ์์ํด๋ณด์ธ์!
@@ -295,22 +306,28 @@ const AssetDetailScreen = ({ navigation }) => {
{/* ์์ฐ ์์ฝ */}
-
-
- ํ๊ฐ ๊ธ์ก
-
+
+
+
+ ํ๊ฐ ๊ธ์ก
+
+
{formatCurrency(assetData.evaluation)}์
-
- ์์๊ธ
-
+
+
+ ์์๊ธ
+
+
{formatCurrency(assetData.cash)}์
- ์ด ์์ฐ
-
+
+ ์ด ์์ฐ
+
+
{formatCurrency(assetData.total_asset)}์
@@ -319,10 +336,10 @@ const AssetDetailScreen = ({ navigation }) => {
);
};
+
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
},
header: {
flexDirection: "row",
@@ -331,19 +348,16 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingTop: 48,
paddingBottom: 16,
- backgroundColor: "#003340",
},
headerTitle: {
fontSize: 18,
fontWeight: "bold",
- color: "#EFF1F5",
marginTop: 10,
},
scrollView: {
flex: 1,
paddingHorizontal: 10,
},
-
chartSection: {
position: "relative",
width: screenWidth - 60,
@@ -365,11 +379,9 @@ const styles = StyleSheet.create({
width: screenWidth - 40,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#004455",
borderRadius: 16,
},
emptyChartText: {
- color: "#EFF1F5",
fontSize: 16,
},
stockListContainer: {
@@ -378,11 +390,9 @@ const styles = StyleSheet.create({
sectionTitle: {
fontSize: 18,
fontWeight: "bold",
- color: "#F074BA",
marginBottom: 16,
},
stockItem: {
- backgroundColor: "#004455",
borderRadius: 12,
padding: 16,
marginBottom: 12,
@@ -396,31 +406,25 @@ const styles = StyleSheet.create({
stockName: {
fontSize: 16,
fontWeight: "600",
- color: "#EFF1F5",
},
stockPercentage: {
fontSize: 14,
- color: "#4ECDC4",
fontWeight: "500",
},
stockValue: {
fontSize: 14,
- color: "rgba(239, 241, 245, 0.7)",
},
emptyListContainer: {
padding: 20,
- backgroundColor: "#004455",
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
},
emptyListText: {
- color: "rgba(239, 241, 245, 0.7)",
textAlign: "center",
},
summaryContainer: {
padding: 16,
- backgroundColor: "#004455",
margin: 16,
borderRadius: 12,
},
@@ -429,14 +433,11 @@ const styles = StyleSheet.create({
justifyContent: "space-between",
paddingVertical: 12,
borderBottomWidth: 1,
- borderBottomColor: "rgba(239, 241, 245, 0.1)",
},
summaryLabel: {
- color: "rgba(239, 241, 245, 0.7)",
fontSize: 14,
},
summaryValue: {
- color: "#EFF1F5",
fontSize: 14,
fontWeight: "500",
},
@@ -445,12 +446,10 @@ const styles = StyleSheet.create({
marginTop: 8,
},
totalLabel: {
- color: "#F074BA",
fontSize: 16,
fontWeight: "bold",
},
totalValue: {
- color: "#F074BA",
fontSize: 16,
fontWeight: "bold",
},
@@ -458,35 +457,29 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#003340",
},
loadingText: {
marginTop: 12,
fontSize: 16,
- color: "#EFF1F5",
},
errorContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
- backgroundColor: "#003340",
},
errorText: {
fontSize: 16,
- color: "#FF6B6B",
marginBottom: 16,
textAlign: "center",
},
retryButton: {
- backgroundColor: "#F074BA",
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
marginBottom: 8,
},
retryButtonText: {
- color: "#EFF1F5",
fontSize: 16,
fontWeight: "600",
},
@@ -495,10 +488,8 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
borderRadius: 8,
borderWidth: 1,
- borderColor: "#F074BA",
},
backButtonText: {
- color: "#F074BA",
fontSize: 16,
fontWeight: "600",
},
@@ -518,20 +509,16 @@ const styles = StyleSheet.create({
marginRight: 8,
},
legendLabel: {
- color: "rgba(239, 241, 245, 0.7)",
fontSize: 14,
},
backText: {
fontSize: 36,
- color: "#F074BA",
},
-
emptyListSubText: {
- color: "rgba(239, 241, 245, 0.5)",
textAlign: "center",
fontSize: 14,
marginTop: 8,
},
});
-export default AssetDetailScreen;
+export default AssetDetailScreen;
\ No newline at end of file
diff --git a/src/screens/Main/MainScreen.js b/src/screens/Main/MainScreen.js
index 17520df..05ff06d 100644
--- a/src/screens/Main/MainScreen.js
+++ b/src/screens/Main/MainScreen.js
@@ -5,7 +5,6 @@ import {
TouchableOpacity,
StyleSheet,
ScrollView,
- Image,
Dimensions,
ActivityIndicator,
RefreshControl,
@@ -24,38 +23,40 @@ import {
scheduleTokenRefresh,
} from "../../utils/hantuToken";
-import BellIcon from "../../assets/icons/bell.svg";
-import SearchIcon from "../../assets/icons/search.svg";
+// ๐ Lucide ์์ด์ฝ import
+import { Bell, Search, Star } from "lucide-react-native";
+
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
const screenWidth = Dimensions.get("window").width;
const MainScreen = ({ navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const [userInfo, setUserInfo] = useState(null);
const [searchText, setSearchText] = useState("");
- const [watchlist, setWatchlist] = useState([]); // ๋น ๋ฐฐ์ด๋ก ์ด๊ธฐํ
+ const [watchlist, setWatchlist] = useState([]);
const [balance, setBalance] = useState("0์");
- const [refreshing, setRefreshing] = useState(false); // ์๋ก๊ณ ์นจ ์ํ
+ const [refreshing, setRefreshing] = useState(false);
- // ์์ฐ ๋ฐ์ดํฐ ์ํ
const [assetData, setAssetData] = useState(null);
const [assetLoading, setAssetLoading] = useState(true);
const [assetError, setAssetError] = useState(null);
- // ๊ด์ฌ์ฃผ์ ๋ก๋ฉ ์ํ
const [watchlistLoading, setWatchlistLoading] = useState(true);
useEffect(() => {
let refreshInterval;
const load = async () => {
- // ํ๊ตญํฌ์ ํ ํฐ ์ด๊ธฐํ ๋ฐ ์ฃผ๊ธฐ์ ๊ฐฑ์
await initializeHantuToken();
refreshInterval = scheduleTokenRefresh();
- // ๊ธฐ์กด ๋ฐ์ดํฐ ๋ก๋ฉ ๋ก์ง
await fetchUserInfo(navigation, setUserInfo);
await fetchUserBalance(navigation, setBalance);
await fetchAssetData();
- await loadWatchlistData(); // ๊ด์ฌ์ฃผ์ ๋ฐ์ดํฐ ๋ก๋ฉ ์ถ๊ฐ
+ await loadWatchlistData();
};
load();
return () => {
@@ -65,16 +66,15 @@ const MainScreen = ({ navigation }) => {
useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
- console.log("๐ฅ MainScreen ๋ค์ focus๋จ โ ๋ฐ์ดํฐ ์ฌ์์ฒญ");
+ console.log("๐ฅ MainScreen ๋ค์ focus๋จ โ ๋ฐ์ดํฐ ์ฌ์์ฒญ");
fetchUserBalance(navigation, setBalance);
fetchAssetData();
- loadWatchlistData(); // ๊ด์ฌ์ฃผ์ ๋ฐ์ดํฐ๋ ์๋ก๊ณ ์นจ
+ loadWatchlistData();
});
return unsubscribe;
}, [navigation]);
- // ๊ด์ฌ์ฃผ์ ๋ฐ์ดํฐ ๋ก๋ฉ ํจ์
const loadWatchlistData = async () => {
try {
setWatchlistLoading(true);
@@ -83,16 +83,13 @@ const MainScreen = ({ navigation }) => {
const result = await fetchWatchlist(navigation);
if (result.success && result.watchlist) {
- // ๊ด์ฌ์ฃผ์ ๋ชฉ๋ก์ ๊ฐ๊ฒฉ ์ ๋ณด ์ถ๊ฐ
const enrichedWatchlist = await Promise.all(
result.watchlist.map(async (stock, index) => {
try {
- // API ํธ์ถ ๊ฐ๊ฒฉ (๋๋ฌด ๋ง์ ์์ฒญ ๋ฐฉ์ง)
if (index > 0) {
await new Promise(resolve => setTimeout(resolve, 300));
}
- // ํ์ฌ๊ฐ ์กฐํ
const priceResult = await fetchWithHantuToken(
`${API_BASE_URL}trading/stock_price/?stock_code=${stock.symbol}`
);
@@ -102,7 +99,6 @@ const MainScreen = ({ navigation }) => {
currentPrice = priceResult.data.current_price;
}
- // ๊ฐ๊ฒฉ ๋ณ๋ ์ ๋ณด ์กฐํ
const changeResponse = await fetch(
`${API_BASE_URL}stocks/price_change/?stock_code=${stock.symbol}`
);
@@ -124,7 +120,7 @@ const MainScreen = ({ navigation }) => {
? `+${changeData.price_change_percentage.toFixed(2)}`
: `${changeData.price_change_percentage.toFixed(2)}`,
changeStatus: changeData.change_status,
- isFavorite: true, // ๊ด์ฌ์ฃผ์์ด๋ฏ๋ก ํญ์ true
+ isFavorite: true,
};
} catch (error) {
console.error(`โ ${stock.symbol} ๊ฐ๊ฒฉ ์ ๋ณด ์กฐํ ์คํจ:`, error);
@@ -155,7 +151,6 @@ const MainScreen = ({ navigation }) => {
}
};
- // ์์ฐ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๋ ํจ์
const fetchAssetData = async () => {
try {
setAssetLoading(true);
@@ -214,7 +209,6 @@ const MainScreen = ({ navigation }) => {
}
};
- // ์๋ก๊ณ ์นจ
const onRefresh = async () => {
setRefreshing(true);
await Promise.all([
@@ -225,43 +219,37 @@ const MainScreen = ({ navigation }) => {
setRefreshing(false);
};
- // ๊ด์ฌ์ฃผ์ ํ ๊ธ ํจ์
const toggleFavorite = async (stockSymbol) => {
try {
console.log("โญ ๊ด์ฌ์ฃผ์ ํ ๊ธ:", stockSymbol);
- // ํ์ฌ ์ํ ํ์ธ
const currentStock = watchlist.find(stock => stock.symbol === stockSymbol);
if (currentStock?.isFavorite) {
- // 1. ๋จผ์ UI์์ ๋ณ์ ๋น ๋ณ๋ก ๋ณ๊ฒฝ (์ฆ์ ๋ฐ์)
setWatchlist(prev =>
prev.map(stock =>
stock.symbol === stockSymbol
- ? { ...stock, isFavorite: false } // ๋น ๋ณ๋ก ๋ณ๊ฒฝ
+ ? { ...stock, isFavorite: false }
: stock
)
);
- // 2. ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ฒ์ ์ ๊ฑฐ ์์ฒญ (UI์์๋ ์ ๊ฑฐํ์ง ์์)
console.log("๐๏ธ ๊ด์ฌ์ฃผ์ ํด์ ์์ฒญ:", stockSymbol);
const result = await removeFromWatchlist(navigation, stockSymbol);
if (result.success) {
console.log("โ
๊ด์ฌ์ฃผ์ ํด์ ์๋ฃ (์๋ก๊ณ ์นจ ์ ์ฌ๋ผ์ง)");
} else {
- // ์๋ฒ ์์ฒญ ์คํจ ์ ๋ค์ ์ฑ์ด ๋ณ๋ก ๋๋๋ฆฌ๊ธฐ
console.error("โ ๊ด์ฌ์ฃผ์ ํด์ ์คํจ:", result.message);
setWatchlist(prev =>
prev.map(stock =>
stock.symbol === stockSymbol
- ? { ...stock, isFavorite: true } // ๋ค์ ์ฑ์ด ๋ณ๋ก ๋ณต๊ตฌ
+ ? { ...stock, isFavorite: true }
: stock
)
);
}
} else {
- // ๋น ๋ณ์ ์ฑ์ด ๋ณ๋ก ๋ณ๊ฒฝํ๊ณ ๊ด์ฌ์ฃผ์์ ์ถ๊ฐ
setWatchlist(prev =>
prev.map(stock =>
stock.symbol === stockSymbol
@@ -274,7 +262,6 @@ const MainScreen = ({ navigation }) => {
const result = await addToWatchlist(navigation, stockSymbol);
if (!result.success) {
- // ์๋ฒ ์์ฒญ ์คํจ ์ ๋ค์ ๋น ๋ณ๋ก ๋๋๋ฆฌ๊ธฐ
console.error("โ ๊ด์ฌ์ฃผ์ ์ถ๊ฐ ์คํจ:", result.message);
setWatchlist(prev =>
prev.map(stock =>
@@ -290,17 +277,14 @@ const MainScreen = ({ navigation }) => {
}
};
- // ๊ฒ์์ฐฝ ํด๋ฆญ ์ SearchScreen์ผ๋ก ์ด๋
const handleSearchPress = () => {
navigation.navigate("SearchScreen");
};
- // ์์ธ ์์ฐ ํ์ด์ง๋ก ์ด๋
const navigateToAssetDetail = () => {
navigation.navigate("AssetDetail");
};
- // ์ฃผ์ ์์ธ ํ์ด์ง๋ก ์ด๋
const handleStockPress = (stock) => {
navigation.navigate("StockDetail", {
symbol: stock.symbol,
@@ -308,13 +292,12 @@ const MainScreen = ({ navigation }) => {
});
};
- // ๊ธ์ก ํฌ๋งทํ
const formatCurrency = (amount) => {
if (!amount || isNaN(amount)) return "0";
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
- // ์ฐจํธ ๋ฐ์ดํฐ ์ค๋น
+ // ๐จ ์ฐจํธ ๋ฐ์ดํฐ์ ํ
๋ง ์์ ์ ์ฉ
const prepareChartData = () => {
if (
!assetData ||
@@ -325,30 +308,11 @@ const MainScreen = ({ navigation }) => {
return [];
}
- const chartColors = [
- "#F074BA", // ์์๊ธ : ๋๋ ํํฌ
- "#3B82F6", // ํ๋
- "#34D399", // ์๋ฉ๋๋
- "#10B981", // ๋
น์
- "#F59E0B", // ํฉ์
- "#EF4444", // ๋นจ๊ฐ
- "#6366F1", // ๋ณด๋ผ
- "#8B5CF6", // ์ฐ๋ณด๋ผ
- "#EC4899", // ํํฌ
- "#F87171", // ์ฐ๋นจ๊ฐ
- "#FBBF24", // ์ฃผํฉ
- "#4ADE80", // ์ฐ๋
น์
- "#22D3EE", // ํ๋์
- "#60A5FA", // ์ฐํ๋
- "#A78BFA", // ๋ผ๋ฒค๋
- "#F472B6", // ์ฝ๋ ํํฌ
- ];
-
const chartData = assetData.breakdown.map((item, index) => ({
name: item.label,
value: item.value,
- color: chartColors[index % chartColors.length],
- legendFontColor: "#EFF1F5",
+ color: theme.chart.colors[index % theme.chart.colors.length],
+ legendFontColor: theme.text.primary,
legendFontSize: 10,
}));
@@ -361,9 +325,11 @@ const MainScreen = ({ navigation }) => {
if (chartData.length === 0) {
return (
-
- ๋ณด์ ์์ฐ์ด ์์ต๋๋ค
-
+
+
+ ๋ณด์ ์์ฐ์ด ์์ต๋๋ค
+
+
์ฃผ์ ๊ฑฐ๋๋ฅผ ์์ํด๋ณด์ธ์!
@@ -392,78 +358,97 @@ const MainScreen = ({ navigation }) => {
/>
- ์ด ์์ฐ
+
+ ์ด ์์ฐ
+
{assetData && assetData.total_asset ? (
-
+
{formatCurrency(assetData.total_asset)}์
) : (
- 0์
+
+ 0์
+
)}
- +
+ +
);
};
- // ๋ณ๋๋ฅ ์์ ๋ฐํ
const getChangeColor = (changeStatus) => {
- switch (changeStatus) {
- case 'up':
- return "#F074BA";
- case 'down':
- return "#00BFFF";
- default:
- return "#AAAAAA";
- }
+ return theme.status[changeStatus] || theme.status.same;
};
return (
+
}
>
+ {/* ๐ ๊ฒ์ & ์๋ฆผ ์์ญ */}
-
- ์ฃผ์๋ช
๊ฒ์
+
+
+ ์ฃผ์๋ช
๊ฒ์
+
-
+
+ {/* ๐ฐ ์์ฐ ์ ๋ณด */}
- ์์๊ธ
- {balance}
+ ์์๊ธ
+ {balance}
{assetLoading ? (
-
- ์์ฐ ์ ๋ณด ๋ก๋ฉ ์ค...
+
+
+ ์์ฐ ์ ๋ณด ๋ก๋ฉ ์ค...
+
) : assetError ? (
- {assetError}
+
+ {assetError}
+
- ๋ค์ ์๋
+
+ ๋ค์ ์๋
+
) : (
@@ -472,69 +457,82 @@ const MainScreen = ({ navigation }) => {
+ {/* ๐ ๊ฑฐ๋ ๋ฒํผ */}
navigation.navigate("StockTrade")}
>
- ์ฃผ์ ๊ฑฐ๋ํ๊ธฐ ๐
+
+ ์ฃผ์ ๊ฑฐ๋ํ๊ธฐ ๐
+
+ {/* โญ ๊ด์ฌ์ฃผ์ ๋ชฉ๋ก */}
- ๋์ ๊ด์ฌ ์ฃผ์
-
- {watchlistLoading ? (
-
-
- ๊ด์ฌ์ฃผ์ ๋ก๋ฉ ์ค...
-
- ) : watchlist.length > 0 ? (
-
- {watchlist.map((stock) => (
- handleStockPress(stock)}
- activeOpacity={0.7}
- >
- {
- e.stopPropagation(); // ๋ถ๋ชจ ํฐ์น ์ด๋ฒคํธ ๋ฐฉ์ง
- toggleFavorite(stock.symbol);
- }}
- style={styles.starTouchArea}
- >
-
-
- {stock.name}
-
- {stock.price}์
-
- {stock.change}%
+
+ ๋์ ๊ด์ฌ ์ฃผ์
+
+
+ {watchlistLoading ? (
+
+
+
+ ๊ด์ฌ์ฃผ์ ๋ก๋ฉ ์ค...
-
- ))}
-
- ) : (
-
- ๊ด์ฌ์ฃผ์์ด ์์ต๋๋ค
-
- ๊ฒ์์ฐฝ์์ ์ฃผ์์ ์ฐพ์ ๊ด์ฌ์ฃผ์์ผ๋ก ๋ฑ๋กํด๋ณด์ธ์!
-
-
- )}
+ ) : watchlist.length > 0 ? (
+
+ {watchlist.map((stock) => (
+ handleStockPress(stock)}
+ activeOpacity={0.7}
+ >
+ {
+ e.stopPropagation();
+ toggleFavorite(stock.symbol);
+ }}
+ style={styles.starTouchArea}
+ >
+ {/* โญ Lucide Star ์์ด์ฝ ์ฌ์ฉ! */}
+
+
+
+ {stock.name}
+
+
+
+ {stock.price}์
+
+
+ {stock.change}%
+
+
+
+ ))}
+
+ ) : (
+
+
+ ๊ด์ฌ์ฃผ์์ด ์์ต๋๋ค
+
+
+ ๊ฒ์์ฐฝ์์ ์ฃผ์์ ์ฐพ์ ๊ด์ฌ์ฃผ์์ผ๋ก ๋ฑ๋กํด๋ณด์ธ์!
+
+
+ )}
);
@@ -543,15 +541,14 @@ const MainScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
},
scrollContent: {
paddingHorizontal: 30,
- paddingTop: 40,
- paddingBottom: 120, // ํญ ๋ฐ ๊ณต๊ฐ ํ๋ณด
+ paddingTop: 10,
+ paddingBottom: 120,
},
searchContainer: {
- marginTop: 40,
+ marginTop: 60,
flexDirection: "row",
alignItems: "center",
marginBottom: 20,
@@ -559,7 +556,6 @@ const styles = StyleSheet.create({
},
searchInputContainer: {
flex: 1,
- backgroundColor: "#EFF1F5",
borderRadius: 13,
padding: 10,
marginRight: 10,
@@ -567,29 +563,18 @@ const styles = StyleSheet.create({
alignItems: "center",
},
searchIconInInput: {
- width: 18,
- height: 18,
- fill: "#6B7280",
marginRight: 8,
},
searchPlaceholder: {
- color: "#6B7280",
fontSize: 14,
},
- BellIcon: {
- width: 24,
- height: 24,
- fill: "#EFF1F5",
- },
assetContainer: {
marginBottom: 20,
},
assetLabel: {
- color: "#F074BA",
fontSize: 18,
},
assetValue: {
- color: "#F074BA",
fontSize: 40,
fontWeight: "bold",
},
@@ -626,12 +611,10 @@ const styles = StyleSheet.create({
zIndex: 10,
},
centerInfoTitle: {
- color: "#003340",
fontSize: 18,
fontWeight: "800",
},
centerInfoAmount: {
- color: "#003340",
fontSize: 26,
fontWeight: "bold",
marginTop: 4,
@@ -640,7 +623,6 @@ const styles = StyleSheet.create({
position: "absolute",
bottom: 10,
right: 10,
- backgroundColor: "#6366F1",
width: 40,
height: 40,
borderRadius: 20,
@@ -657,7 +639,6 @@ const styles = StyleSheet.create({
zIndex: 20,
},
detailButtonText: {
- color: "#EFF1F5",
fontSize: 24,
fontWeight: "bold",
},
@@ -668,7 +649,6 @@ const styles = StyleSheet.create({
},
loadingText: {
marginTop: 8,
- color: "#EFF1F5",
fontSize: 14,
},
errorContainer: {
@@ -678,38 +658,32 @@ const styles = StyleSheet.create({
padding: 16,
},
errorText: {
- color: "#FF6B6B",
marginBottom: 12,
textAlign: "center",
},
retryButton: {
- backgroundColor: "#F074BA",
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
},
retryButtonText: {
- color: "#EFF1F5",
fontWeight: "bold",
},
tradeButton: {
- backgroundColor: "#EFF1F5",
padding: 13,
borderRadius: 13,
alignItems: "center",
marginBottom: 20,
},
tradeButtonText: {
- color: "#003340",
fontSize: 18,
fontWeight: "900",
},
watchlistContainer: {
flex: 1,
- minHeight: 200, // ์ต์ ๋์ด ์ค์
+ minHeight: 200,
},
watchlistTitle: {
- color: "#F074BA",
fontSize: 18,
marginBottom: 10,
marginLeft: 5,
@@ -722,62 +696,45 @@ const styles = StyleSheet.create({
paddingVertical: 40,
},
watchlistLoadingText: {
- color: "#EFF1F5",
fontSize: 16,
marginTop: 10,
textAlign: "center",
},
-
starTouchArea: {
- padding: 8, // ํฐ์น ์์ญ ํ์ฅ
+ padding: 8,
marginRight: 2,
},
-
- starIcon: {
- width: 20,
- height: 20,
- },
stockItem: {
flexDirection: "row",
alignItems: "center",
padding: 5,
borderBottomWidth: 1,
- borderBottomColor: "#004455",
},
stockName: {
flex: 1,
- color: "#EFF1F5",
marginLeft: 10,
},
stockPriceContainer: {
alignItems: "flex-end",
},
stockPrice: {
- color: "#EFF1F5",
},
stockChange: {
fontWeight: "bold",
},
- starIcon: {
- width: 20,
- height: 20,
- },
emptyChart: {
height: screenWidth - 60,
width: screenWidth - 60,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#004455",
borderRadius: 16,
},
emptyChartText: {
- color: "#EFF1F5",
fontSize: 18,
fontWeight: "bold",
marginBottom: 8,
},
emptyChartSubText: {
- color: "rgba(239, 241, 245, 0.7)",
fontSize: 14,
},
emptyWatchlist: {
@@ -786,14 +743,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
},
emptyWatchlistText: {
- color: "#EFF1F5",
fontSize: 16,
fontWeight: "bold",
marginBottom: 8,
textAlign: "center",
},
emptyWatchlistSubText: {
- color: "rgba(239, 241, 245, 0.7)",
fontSize: 14,
textAlign: "center",
lineHeight: 20,
diff --git a/src/screens/Main/SearchScreen.js b/src/screens/Main/SearchScreen.js
index 530d5f8..185eaeb 100644
--- a/src/screens/Main/SearchScreen.js
+++ b/src/screens/Main/SearchScreen.js
@@ -9,15 +9,16 @@ import {
ActivityIndicator,
} from "react-native";
import { API_BASE_URL } from "../../utils/apiConfig";
+import { useTheme } from "../../utils/ThemeContext";
const SearchScreen = ({ navigation }) => {
+ const { theme } = useTheme();
const [searchQuery, setSearchQuery] = useState("");
const [autoCompleteResults, setAutoCompleteResults] = useState([]);
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
- // ์๋์์ฑ API ํธ์ถ ํจ์
const fetchAutoComplete = async (query) => {
if (query.length === 0) {
setAutoCompleteResults([]);
@@ -31,7 +32,6 @@ const SearchScreen = ({ navigation }) => {
);
const data = await response.json();
- // results ๋ฐฐ์ด์ด ์๋์ง ํ์ธํ๊ณ ์ฒ๋ฆฌ
if (data.results) {
setAutoCompleteResults(data.results);
} else if (Array.isArray(data)) {
@@ -49,7 +49,6 @@ const SearchScreen = ({ navigation }) => {
}
};
- // ๊ฒ์ API ํธ์ถ ํจ์
const fetchSearchResults = async (query) => {
try {
setLoading(true);
@@ -58,7 +57,6 @@ const SearchScreen = ({ navigation }) => {
);
const data = await response.json();
- // results ๋ฐฐ์ด์ด ์๋์ง ํ์ธํ๊ณ ์ฒ๋ฆฌ
if (data.results) {
setSearchResults(data.results);
} else if (Array.isArray(data)) {
@@ -69,7 +67,7 @@ const SearchScreen = ({ navigation }) => {
}
setHasSearched(true);
- setAutoCompleteResults([]); // ์๋์์ฑ ๊ฒฐ๊ณผ ์ด๊ธฐํ
+ setAutoCompleteResults([]);
} catch (error) {
console.error("๊ฒ์ API ์ค๋ฅ:", error);
} finally {
@@ -77,7 +75,6 @@ const SearchScreen = ({ navigation }) => {
}
};
- // ๊ฒ์์ด ๋ณ๊ฒฝ ์ ์๋์์ฑ ๊ฒฐ๊ณผ ์์ฒญ
useEffect(() => {
console.log("๊ฒ์์ด ๋ณ๊ฒฝ:", searchQuery);
const delayDebounceFn = setTimeout(() => {
@@ -85,20 +82,17 @@ const SearchScreen = ({ navigation }) => {
console.log("์๋์์ฑ API ํธ์ถ ์๋:", searchQuery);
fetchAutoComplete(searchQuery);
}
- }, 800); // 800ms ๋๋ฐ์ด์ค *** ์ถํ ๋๋ฆด์๋..?
+ }, 800);
return () => clearTimeout(delayDebounceFn);
}, [searchQuery]);
- // ์๋์์ฑ ํญ๋ชฉ ์ ํ ์ ํธ์ถํจ
const handleSelectAutoComplete = (item) => {
setSearchQuery(item.name);
fetchSearchResults(item.name);
};
- // ์ด๊ฑด ๊ฒ์ ๊ฒฐ๊ณผ ํญ๋ชฉ ์ ํ ์ ํธ์ถํจ
const handleSelectStock = (item) => {
- // ์ ํํ ์ฃผ์์ ์์ธ ํ๋ฉด์ผ๋ก
navigation.navigate("StockDetail", {
symbol: item.symbol,
name: item.name,
@@ -106,19 +100,21 @@ const SearchScreen = ({ navigation }) => {
};
return (
-
+
- {/* ๐ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ */}
navigation.goBack()}
style={styles.backButton}
>
- {"<"}
+ {"<"}
{
setAutoCompleteResults([]);
}}
>
- โ
+
+ โ
+
)}
{loading && (
-
+
)}
- {/* ๋ก๊ทธ์ฐ์ด๋ณธ๊ฒ */}
{!hasSearched && autoCompleteResults.length > 0 && (
-
+
๊ฒฐ๊ณผ ์: {autoCompleteResults.length}
)}
- {/* ์๋์์ฑ ๊ฒฐ๊ณผ */}
{!hasSearched && autoCompleteResults.length > 0 && (
- ์ถ์ฒ ๊ฒ์์ด
+
+ ์ถ์ฒ ๊ฒ์์ด
+
item.symbol}
renderItem={({ item }) => (
handleSelectAutoComplete(item)}
>
- {item.name}
- {item.symbol}
+
+ {item.name}
+
+
+ {item.symbol}
+
)}
/>
)}
- {/* ๊ฒ์ ๊ฒฐ๊ณผ*/}
{hasSearched && searchResults.length > 0 && (
- ๊ฒ์ ๊ฒฐ๊ณผ
+
+ ๊ฒ์ ๊ฒฐ๊ณผ
+
item.symbol}
renderItem={({ item }) => (
handleSelectStock(item)}
>
- {item.name}
- {item.symbol}
+
+ {item.name}
+
+
+ {item.symbol}
+
)}
/>
@@ -192,7 +205,9 @@ const SearchScreen = ({ navigation }) => {
{hasSearched && searchResults.length === 0 && !loading && (
- ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
+
+ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
+
)}
@@ -202,7 +217,6 @@ const SearchScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
padding: 16,
},
header: {
@@ -216,16 +230,13 @@ const styles = StyleSheet.create({
padding: 4,
},
backText: {
- color: "#EFF1F5",
fontSize: 24,
fontWeight: "bold",
},
searchInput: {
flex: 1,
- backgroundColor: "#004455",
borderRadius: 13,
padding: 12,
- color: "#EFF1F5",
fontSize: 16,
},
clearButton: {
@@ -235,7 +246,6 @@ const styles = StyleSheet.create({
justifyContent: "center",
},
clearButtonText: {
- color: "#9ca3af",
fontSize: 16,
fontWeight: "bold",
},
@@ -247,13 +257,11 @@ const styles = StyleSheet.create({
resultsContainer: {
flex: 1,
marginTop: 8,
- //backgroundColor: '#004455',
borderRadius: 10,
padding: 10,
zIndex: 10,
},
resultTitle: {
- color: "#F074BA",
fontSize: 16,
fontWeight: "600",
marginBottom: 8,
@@ -265,18 +273,14 @@ const styles = StyleSheet.create({
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
- borderBottomColor: "#004455",
- backgroundColor: "#002530",
marginBottom: 4,
borderRadius: 8,
},
itemName: {
- color: "#EFF1F5",
fontSize: 16,
fontWeight: "500",
},
itemSymbol: {
- color: "#9ca3af",
fontSize: 14,
},
noResultsContainer: {
@@ -285,9 +289,8 @@ const styles = StyleSheet.create({
alignItems: "center",
},
noResultsText: {
- color: "#9ca3af",
fontSize: 16,
},
});
-export default SearchScreen;
+export default SearchScreen;
\ No newline at end of file
diff --git a/src/screens/Main/StockDetail.js b/src/screens/Main/StockDetail.js
index c30bf38..c6c359d 100644
--- a/src/screens/Main/StockDetail.js
+++ b/src/screens/Main/StockDetail.js
@@ -22,9 +22,15 @@ import {
isInWatchlist
} from "../../utils/watchList";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const screenWidth = Dimensions.get("window").width;
const StockDetail = ({ route, navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
const { symbol, name } = route.params;
const [loading, setLoading] = useState(true);
const [stockData, setStockData] = useState(null);
@@ -77,7 +83,7 @@ const StockDetail = ({ route, navigation }) => {
} else {
setOwnedQuantity(0);
setAveragePrice(0);
- console.log(`๐ ${symbol} ๋ณด์ ํ์ง ์์`);
+ console.log(`๐ ${symbol} ๋ณด์ ํ์ง ์์`);
}
} else {
console.warn("ํฌํธํด๋ฆฌ์ค ์๋ต ํ์์ด ์์๊ณผ ๋ค๋ฆ:", result);
@@ -113,11 +119,11 @@ const StockDetail = ({ route, navigation }) => {
// 3. ๋ฐ์ดํฐ ์ค์
if (priceData.status === "success" && changeData.status === "success") {
const changeSign =
- changeData.change_status === "up"
- ? " โถ "
- : changeData.change_status === "down"
- ? " โท "
- : "";
+ changeData.change_status === "up"
+ ? " \u25B2 "
+ : changeData.change_status === "down"
+ ? " \u25BC "
+ : "";
const priceChangeSign =
changeData.change_status === "up"
@@ -373,6 +379,38 @@ const StockDetail = ({ route, navigation }) => {
}
};
+ // ์ฐจํธ ์์ ๊ฒฐ์ ํจ์
+ const getChartColor = (opacity = 1) => {
+ if (stockData?.changeStatus === "up") {
+ return `rgba(240, 116, 186, ${opacity})`;
+ } else if (stockData?.changeStatus === "down") {
+ return `rgba(96, 165, 250, ${opacity})`;
+ }
+ return `rgba(156, 163, 175, ${opacity})`;
+ };
+
+ // ์ฐจํธ ์ค์ ์ ํ
๋ง ์ ์ฉ
+ const getChartConfig = () => ({
+ backgroundColor: theme.background.secondary,
+ backgroundGradientFrom: theme.background.secondary,
+ backgroundGradientTo: theme.background.secondary,
+ decimalPlaces: 0,
+ color: getChartColor,
+ labelColor: (opacity = 1) => theme.text.primary,
+ style: {
+ borderRadius: 16,
+ },
+ propsForDots: {
+ r: "4",
+ strokeWidth: "2",
+ stroke: stockData?.changeStatus === "up"
+ ? theme.status.up
+ : stockData?.changeStatus === "down"
+ ? theme.status.down
+ : theme.status.same,
+ },
+ });
+
// ๋งค์ ๋ฒํผ ํธ๋ค๋ฌ
const handleBuyPress = () => {
const stock = {
@@ -463,53 +501,28 @@ const StockDetail = ({ route, navigation }) => {
const renderChart = () => {
if (chartLoading) {
return (
-
-
- ์ฐจํธ ๋ก๋ฉ ์ค...
+
+
+ ์ฐจํธ ๋ก๋ฉ ์ค...
);
}
if (!chartData || !chartData.datasets[0].data.length) {
return (
-
- ์ฐจํธ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค
+
+ ์ฐจํธ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค
);
}
return (
-
+
- stockData?.changeStatus === "up"
- ? `rgba(240, 116, 186, ${opacity})`
- : stockData?.changeStatus === "down"
- ? `rgba(96, 165, 250, ${opacity})`
- : `rgba(156, 163, 175, ${opacity})`,
- labelColor: (opacity = 1) => `rgba(239, 241, 245, ${opacity})`,
- style: {
- borderRadius: 16,
- },
- propsForDots: {
- r: "4",
- strokeWidth: "2",
- stroke:
- stockData?.changeStatus === "up"
- ? "#F074BA"
- : stockData?.changeStatus === "down"
- ? "#60a5fa"
- : "#9ca3af",
- },
- }}
+ chartConfig={getChartConfig()}
bezier
style={styles.chart}
withInnerLines={false}
@@ -525,32 +538,32 @@ const StockDetail = ({ route, navigation }) => {
if (loading || portfolioLoading) {
return (
-
-
- ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
);
}
return (
-
+
{/* ํค๋ */}
-
+
navigation.goBack()}
style={styles.backButton}
>
- {"<"}
+ {"<"}
- {name}
+ {name}
{isFavorite ? (
- โ
+ โ
) : (
- โ
+ โ
)}
@@ -558,16 +571,12 @@ const StockDetail = ({ route, navigation }) => {
{/* ๊ฐ๊ฒฉ ์น์
*/}
- {symbol}
- {stockData.price}์
+ {symbol}
+ {stockData.price}์
{stockData.change}% ({stockData.priceChange}์)
@@ -581,33 +590,29 @@ const StockDetail = ({ route, navigation }) => {
{/* ์ฃผ์ ์งํ ์น์
*/}
-
- ์ฃผ์ ์งํ
+
+ ์ฃผ์ ์งํ
-
-
+
+
์ ์ผ ์ข
๊ฐ ({stockData.previousDate})
- {stockData.previousPrice}์
+ {stockData.previousPrice}์
-
-
+
+
ํ์ฌ๊ฐ ({stockData.currentDate})
- {stockData.price}์
+ {stockData.price}์
-
- ์ ์ผ๋๋น ๋ณ๋
+
+ ์ ์ผ๋๋น ๋ณ๋
{stockData.change}% ({stockData.priceChange}์)
@@ -615,17 +620,17 @@ const StockDetail = ({ route, navigation }) => {
{/* ๋ณด์ ์ ๋ณด ์น์
*/}
-
- ๋ณด์ ์๋
-
+
+ ๋ณด์ ์๋
+
{ownedQuantity.toLocaleString()}์ฃผ
{ownedQuantity > 0 && (
-
- ํ๊ท ๋จ๊ฐ
-
+
+ ํ๊ท ๋จ๊ฐ
+
{averagePrice.toLocaleString()}์
@@ -634,17 +639,20 @@ const StockDetail = ({ route, navigation }) => {
{/* ๋งค์/๋งค๋ ๋ฒํผ ์ปจํ
์ด๋ */}
-
- ๋งค์
+
+ ๋งค์
{/* ๋งค๋ ๋ฒํผ ์กฐ๊ฑด๋ถ ๋ ๋๋ง */}
{ownedQuantity > 0 ? (
- ๋งค๋
+ ๋งค๋
) : (
@@ -664,18 +672,18 @@ const StockDetail = ({ route, navigation }) => {
-
+
{selectedPoint && (
<>
- {selectedPoint.date}
-
+ {selectedPoint.date}
+
{selectedPoint.price}์
- ํ์ธ
+ ํ์ธ
>
)}
@@ -691,13 +699,11 @@ const StockDetail = ({ route, navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
- backgroundColor: "#003340",
},
header: {
flexDirection: "row",
@@ -706,18 +712,15 @@ const styles = StyleSheet.create({
padding: 16,
marginTop: 40,
borderBottomWidth: 1,
- borderBottomColor: "#004455",
},
backButton: {
padding: 8,
},
backText: {
- color: "#EFF1F5",
fontSize: 24,
fontWeight: "bold",
},
headerTitle: {
- color: "#EFF1F5",
fontSize: 18,
fontWeight: "bold",
flex: 1,
@@ -729,7 +732,6 @@ const styles = StyleSheet.create({
starIcon: {
width: 24,
height: 24,
- color: "#F074BA",
fontSize: 24,
},
content: {
@@ -741,12 +743,10 @@ const styles = StyleSheet.create({
marginBottom: 24,
},
symbolText: {
- color: "#9ca3af",
fontSize: 14,
marginBottom: 8,
},
priceText: {
- color: "#EFF1F5",
fontSize: 36,
fontWeight: "bold",
marginBottom: 8,
@@ -755,15 +755,6 @@ const styles = StyleSheet.create({
fontSize: 18,
fontWeight: "bold",
},
- positiveChange: {
- color: "#F074BA",
- },
- negativeChange: {
- color: "#60a5fa",
- },
- neutralChange: {
- color: "#9ca3af",
- },
// ์ฐจํธ ๊ด๋ จ ์คํ์ผ
chartSection: {
marginBottom: 24,
@@ -797,7 +788,6 @@ const styles = StyleSheet.create({
},
chartContainer: {
alignItems: "center",
- backgroundColor: "#004455",
borderRadius: 16,
padding: 8,
},
@@ -806,33 +796,27 @@ const styles = StyleSheet.create({
},
chartLoadingContainer: {
height: 200,
- backgroundColor: "#004455",
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
},
chartLoadingText: {
- color: "#9ca3af",
marginTop: 10,
},
chartPlaceholder: {
height: 200,
- backgroundColor: "#004455",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
},
chartText: {
- color: "#9ca3af",
},
statsContainer: {
- backgroundColor: "#004455",
borderRadius: 8,
padding: 16,
marginBottom: 24,
},
sectionTitle: {
- color: "#F074BA",
fontSize: 18,
fontWeight: "600",
marginBottom: 16,
@@ -842,14 +826,11 @@ const styles = StyleSheet.create({
justifyContent: "space-between",
paddingVertical: 8,
borderBottomWidth: 1,
- borderBottomColor: "#003340",
},
statLabel: {
- color: "#9ca3af",
fontSize: 14,
},
statValue: {
- color: "#EFF1F5",
fontSize: 14,
fontWeight: "500",
},
@@ -862,7 +843,6 @@ const styles = StyleSheet.create({
},
buyButton: {
flex: 1,
- backgroundColor: "#6EE69E",
padding: 16,
borderRadius: 13,
alignItems: "center",
@@ -870,13 +850,11 @@ const styles = StyleSheet.create({
marginRight: 4,
},
buyButtonText: {
- color: "#003340",
fontSize: 20,
fontWeight: "900",
},
sellButton: {
flex: 1,
- backgroundColor: "#F074BA",
padding: 16,
borderRadius: 13,
alignItems: "center",
@@ -884,7 +862,6 @@ const styles = StyleSheet.create({
marginLeft: 4,
},
sellButtonText: {
- color: "#003340",
fontSize: 20,
fontWeight: "900",
},
@@ -910,41 +887,35 @@ const styles = StyleSheet.create({
alignItems: "center",
},
modalContent: {
- backgroundColor: "#004455",
borderRadius: 16,
padding: 24,
alignItems: "center",
minWidth: 200,
},
modalDate: {
- color: "#EFF1F5",
fontSize: 16,
fontWeight: "600",
marginBottom: 8,
},
modalPrice: {
- color: "#EFF1F5",
fontSize: 24,
fontWeight: "bold",
marginBottom: 16,
},
modalCloseButton: {
- backgroundColor: "#EFF1F5",
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 8,
},
modalCloseText: {
- color: "#003340",
fontSize: 14,
fontWeight: "bold",
},
loadingText: {
- color: "#EFF1F5",
fontSize: 16,
marginTop: 10,
textAlign: "center",
},
});
-export default StockDetail;
+export default StockDetail;
\ No newline at end of file
diff --git a/src/screens/Main/StockTradeScreen.js b/src/screens/Main/StockTradeScreen.js
index 829b306..ba6f669 100644
--- a/src/screens/Main/StockTradeScreen.js
+++ b/src/screens/Main/StockTradeScreen.js
@@ -16,7 +16,13 @@ import RecommendedStock from "../../components/RecommendedStock";
import { API_BASE_URL } from "../../utils/apiConfig";
import { fetchWithHantuToken } from "../../utils/hantuToken";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const StockTradeScreen = ({ navigation }) => {
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+
console.log("๐ StockTradeScreen ๋ ๋๋ง");
const [userInfo, setUserInfo] = useState(null);
const [portfolioData, setPortfolioData] = useState([]);
@@ -176,14 +182,7 @@ const StockTradeScreen = ({ navigation }) => {
};
const getChangeColor = (changeStatus) => {
- switch (changeStatus) {
- case "up":
- return "#F074BA"; // ์์น - ํํฌ
- case "down":
- return "#00BFFF"; // ํ๋ฝ - ํ๋
- default:
- return "#AAAAAA"; // ๋ณดํฉ - ํ์
- }
+ return theme.status[changeStatus] || theme.status.same;
};
const getChangeSymbol = (changeStatus) => {
@@ -199,24 +198,24 @@ const StockTradeScreen = ({ navigation }) => {
if (loading) {
return (
-
-
- ๋ณด์ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+ ๋ณด์ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
);
}
return (
-
+
{/* ์๋จ ํค๋ */}
navigation.goBack()}
style={styles.backButton}
>
- {"<"}
+ {"<"}
- ์ฃผ์ ๊ฑฐ๋ํ๊ธฐ
+ ์ฃผ์ ๊ฑฐ๋ํ๊ธฐ
{
contentContainerStyle={styles.scrollContent}
>
{/* ํ์ฌ ๋ณด์ ์ฃผ์ */}
- ํ์ฌ ๋ณด์ ์ฃผ์
-
+ ํ์ฌ ๋ณด์ ์ฃผ์
+
{portfolioData.length > 0 ? (
portfolioData.map((stock) => (
@@ -242,11 +241,11 @@ const StockTradeScreen = ({ navigation }) => {
activeOpacity={0.7}
>
- {stock.name}
- ({stock.symbol})
+ {stock.name}
+ ({stock.symbol})
-
+
{formatNumber(stock.price)}์
{
์ด ๋งค์ ๊ธ์ก: {formatNumber(stock.totalBuyPrice)}์
-
+
๋ณด์ ์๋: {formatNumber(stock.quantity)}์ฃผ
{
e.stopPropagation(); // ๋ถ๋ชจ TouchableOpacity ์ด๋ฒคํธ ๋ฐฉ์ง
console.log("๋งค์ ๋ฒํผ ํด๋ฆญ๋จ");
@@ -294,27 +293,27 @@ const StockTradeScreen = ({ navigation }) => {
navigation.navigate("TradingBuy", { stock });
}}
>
- ๋งค์
+ ๋งค์
{
e.stopPropagation(); // ๋ถ๋ชจ TouchableOpacity ์ด๋ฒคํธ ๋ฐฉ์ง
navigation.navigate("TradingSell", { stock });
}}
>
- ๋งค๋
+ ๋งค๋
-
+
))
) : (
- ๋ณด์ ์ค์ธ ์ฃผ์์ด ์์ต๋๋ค
-
+ ๋ณด์ ์ค์ธ ์ฃผ์์ด ์์ต๋๋ค
+
์๋ ์ถ์ฒ ์ฃผ์์์ ํฌ์๋ฅผ ์์ํด๋ณด์ธ์!
@@ -348,7 +347,6 @@ const StockTradeScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flexGrow: 1,
- backgroundColor: "#003340",
justifyContent: "center",
paddingHorizontal: 30,
},
@@ -360,7 +358,6 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 36,
- color: "#F074BA",
},
scrollView: {
flex: 1,
@@ -381,19 +378,16 @@ const styles = StyleSheet.create({
headerTitle: {
fontSize: 20,
fontWeight: "bold",
- color: "#F074BA",
textAlign: "center",
top: 10,
},
sectionTitle: {
fontSize: 18,
- color: "#FFD1EB",
fontWeight: "bold",
marginBottom: 0,
},
divider: {
height: 1,
- backgroundColor: "#4A5A60",
marginVertical: 10,
},
stockItem: {
@@ -407,13 +401,11 @@ const styles = StyleSheet.create({
},
stockName: {
fontSize: 16,
- color: "#EFF1F5",
fontWeight: "bold",
marginBottom: 4,
},
stockCode: {
fontSize: 12,
- color: "#AFA5CF",
marginBottom: 8,
},
priceContainer: {
@@ -422,7 +414,6 @@ const styles = StyleSheet.create({
},
stockPrice: {
fontSize: 18,
- color: "#EFF1F5",
fontWeight: "bold",
marginRight: 10,
},
@@ -442,7 +433,6 @@ const styles = StyleSheet.create({
},
quantity: {
fontSize: 14,
- color: "#EFF1F5",
marginTop: 4,
},
buttonContainer: {
@@ -450,24 +440,20 @@ const styles = StyleSheet.create({
gap: 10,
},
buyButton: {
- backgroundColor: "#6EE69E",
paddingVertical: 8,
paddingHorizontal: 18,
borderRadius: 8,
},
buyText: {
- color: "#003340",
fontWeight: "bold",
fontSize: 16,
},
sellButton: {
- backgroundColor: "#F074BA",
paddingVertical: 8,
paddingHorizontal: 18,
borderRadius: 8,
},
sellText: {
- color: "#003340",
fontWeight: "bold",
fontSize: 16,
},
@@ -477,17 +463,14 @@ const styles = StyleSheet.create({
},
emptyText: {
fontSize: 16,
- color: "#EFF1F5",
textAlign: "center",
marginBottom: 8,
},
emptySubText: {
fontSize: 14,
- color: "#AFA5CF",
textAlign: "center",
},
loadingText: {
- color: "#EFF1F5",
fontSize: 16,
marginTop: 10,
textAlign: "center",
@@ -497,9 +480,8 @@ const styles = StyleSheet.create({
alignItems: "center",
},
recommendedLoadingText: {
- color: "#AFA5CF",
fontSize: 14,
},
});
-export default StockTradeScreen;
+export default StockTradeScreen;
\ No newline at end of file
diff --git a/src/screens/Main/TradingBuyScreen.js b/src/screens/Main/TradingBuyScreen.js
index 35a046a..f507979 100644
--- a/src/screens/Main/TradingBuyScreen.js
+++ b/src/screens/Main/TradingBuyScreen.js
@@ -14,9 +14,11 @@ import {
import { fetchWithHantuToken } from "../../utils/hantuToken";
import { fetchUserInfo } from "../../utils/user";
import { API_BASE_URL } from "../../utils/apiConfig";
-import { fetchWithAuth } from "../../utils/token"; // fetchWithAuth ์ฌ์ฉ
+import { fetchWithAuth } from "../../utils/token";
+import { useTheme } from "../../utils/ThemeContext";
const TradingBuyScreen = ({ route, navigation }) => {
+ const { theme } = useTheme();
const stock = route.params?.stock;
const [quantity, setQuantity] = useState("1");
const [currentPrice, setCurrentPrice] = useState(0);
@@ -26,12 +28,9 @@ const TradingBuyScreen = ({ route, navigation }) => {
useEffect(() => {
const init = async () => {
- // ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
await fetchUserInfo(navigation, (info) => {
if (info?.id) setUserId(info.id);
});
-
- // ํ์ฌ๊ฐ ๊ฐ์ ธ์ค๊ธฐ
await fetchCurrentPrice(stock?.symbol);
};
init();
@@ -62,7 +61,6 @@ const TradingBuyScreen = ({ route, navigation }) => {
console.log("โ
ํ์ฌ๊ฐ ์
๋ฐ์ดํธ:", data.current_price);
} else {
console.warn("โ ๏ธ ํ์ฌ๊ฐ API ์๋ต ์คํจ:", data);
- // ๊ธฐ์กด ์ฃผ์ ๊ฐ๊ฒฉ์ ์ฌ์ฉ
setCurrentPrice(
typeof stock.price === "string"
? parseInt(stock.price.replace(/,/g, ""))
@@ -71,7 +69,6 @@ const TradingBuyScreen = ({ route, navigation }) => {
}
} catch (error) {
console.error("โ ํ์ฌ๊ฐ ์กฐํ ์คํจ:", error);
- // ๊ธฐ์กด ์ฃผ์ ๊ฐ๊ฒฉ์ ์ฌ์ฉ
setCurrentPrice(
typeof stock.price === "string"
? parseInt(stock.price.replace(/,/g, ""))
@@ -109,7 +106,6 @@ const TradingBuyScreen = ({ route, navigation }) => {
setLoading(true);
try {
- // ์ข
๋ชฉ ์๋ณ์ ๊ฒฐ์ (์ข
๋ชฉ์ฝ๋ ์ฐ์ ์ฌ์ฉ)
const stockIdentifier = stock.symbol || stock.name;
const orderData = {
@@ -122,7 +118,6 @@ const TradingBuyScreen = ({ route, navigation }) => {
console.log("๐ก ๋งค์ ์ฃผ๋ฌธ ๋ฐ์ดํฐ:", orderData);
- // โ
fetchWithAuth ์ฌ์ฉ (์ผ๋ฐ ๋ฐฑ์๋ API์ด๋ฏ๋ก)
const response = await fetchWithAuth(
`${API_BASE_URL}trading/trade/`,
{
@@ -166,9 +161,9 @@ const TradingBuyScreen = ({ route, navigation }) => {
};
const getChangeColor = (change) => {
- if (change > 0) return "#F074BA";
- if (change < 0) return "#00BFFF";
- return "#AAAAAA";
+ if (change > 0) return theme.status.up;
+ if (change < 0) return theme.status.down;
+ return theme.status.same;
};
const getChangeSymbol = (change) => {
@@ -180,33 +175,39 @@ const TradingBuyScreen = ({ route, navigation }) => {
return (
{/* ํค๋ */}
navigation.goBack()}>
- {"<"}
+
+ {"<"}
+
- ๋งค์
+
+ ๋งค์
+
{/* ์ข
๋ชฉ ์ ๋ณด */}
- {stock?.name || "์ข
๋ชฉ๋ช
์์"}
-
+
+ {stock?.name || "์ข
๋ชฉ๋ช
์์"}
+
+
({stock?.symbol || "์ข
๋ชฉ์ฝ๋ ์์"})
{priceLoading ? (
-
+
) : (
<>
-
+
{formatNumber(currentPrice)}์
{stock?.change !== undefined && (
@@ -225,50 +226,64 @@ const TradingBuyScreen = ({ route, navigation }) => {
-
+
{/* ํ์ฌ ๋ณด์ ๋ */}
- ํ์ฌ ๋ณด์ ๋
-
+
+ ํ์ฌ ๋ณด์ ๋
+
+
{formatNumber(stock?.quantity || 0)}์ฃผ
{/* ์๋ ์
๋ ฅ */}
- ๋งค์ ์๋
+
+ ๋งค์ ์๋
+
- ์ฃผ
+ ์ฃผ
{/* ์ด ๊ธ์ก */}
-
- ์ด ๋งค์ ๊ธ์ก
-
+
+
+ ์ด ๋งค์ ๊ธ์ก
+
+
{formatNumber(calculateTotal())}์
{/* ๋งค์ ๋ฒํผ */}
{loading ? (
-
+
) : (
-
+
{formatNumber(parseInt(quantity) || 0)}์ฃผ ๋งค์ํ๊ธฐ
)}
@@ -282,7 +297,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 30,
- backgroundColor: "#003340",
},
safeArea: {
flex: 1,
@@ -297,16 +311,14 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 28,
- color: "#F074BA",
marginRight: 15,
},
title: {
fontSize: 20,
fontWeight: "bold",
- color: "#F074BA",
flex: 1,
textAlign: "center",
- marginRight: 43, // ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ ๊ณต๊ฐ๋งํผ ๋ณด์
+ marginRight: 43,
},
stockRow: {
flexDirection: "row",
@@ -318,13 +330,11 @@ const styles = StyleSheet.create({
flex: 1,
},
stockName: {
- color: "white",
fontSize: 18,
fontWeight: "bold",
marginBottom: 4,
},
stockCode: {
- color: "#AFA5CF",
fontSize: 14,
},
priceBlock: {
@@ -332,7 +342,6 @@ const styles = StyleSheet.create({
},
priceText: {
fontSize: 20,
- color: "white",
fontWeight: "bold",
marginBottom: 4,
},
@@ -342,7 +351,6 @@ const styles = StyleSheet.create({
},
divider: {
height: 1,
- backgroundColor: "#4A5A60",
marginVertical: 20,
},
infoSection: {
@@ -350,12 +358,10 @@ const styles = StyleSheet.create({
},
label: {
fontSize: 16,
- color: "#FFD1EB",
marginBottom: 8,
},
value: {
fontSize: 18,
- color: "#FFFFFF",
fontWeight: "bold",
},
inputRow: {
@@ -363,19 +369,16 @@ const styles = StyleSheet.create({
alignItems: "center",
},
input: {
- backgroundColor: "#FFFFFF",
borderRadius: 10,
paddingHorizontal: 15,
paddingVertical: 12,
fontSize: 18,
- color: "#000000",
minWidth: 100,
textAlign: "center",
marginRight: 10,
},
unit: {
fontSize: 18,
- color: "#FFFFFF",
},
totalRow: {
flexDirection: "row",
@@ -385,21 +388,17 @@ const styles = StyleSheet.create({
marginBottom: 40,
paddingVertical: 15,
paddingHorizontal: 20,
- backgroundColor: "#004455",
borderRadius: 10,
},
totalLabel: {
fontSize: 16,
- color: "#FFFFFF",
},
totalAmount: {
fontSize: 20,
fontWeight: "bold",
- color: "#6EE69E",
},
buyButton: {
marginTop: "auto",
- backgroundColor: "#6EE69E",
borderRadius: 12,
paddingVertical: 16,
alignItems: "center",
@@ -411,8 +410,7 @@ const styles = StyleSheet.create({
buyButtonText: {
fontSize: 18,
fontWeight: "bold",
- color: "#003340",
},
});
-export default TradingBuyScreen;
+export default TradingBuyScreen;
\ No newline at end of file
diff --git a/src/screens/Main/TradingSellScreen.js b/src/screens/Main/TradingSellScreen.js
index 55da427..f6eec8b 100644
--- a/src/screens/Main/TradingSellScreen.js
+++ b/src/screens/Main/TradingSellScreen.js
@@ -14,10 +14,11 @@ import {
import { fetchUserInfo } from "../../utils/user";
import { API_BASE_URL } from "../../utils/apiConfig";
import { fetchWithHantuToken } from "../../utils/hantuToken";
-// โฌ๏ธ ๋ณ๊ฒฝ: getNewAccessToken ์ ๊ฑฐ, fetchWithAuth ์ถ๊ฐ
import { fetchWithAuth } from "../../utils/token";
+import { useTheme } from "../../utils/ThemeContext";
const TradingSellScreen = ({ route, navigation }) => {
+ const { theme } = useTheme();
const stock = route.params?.stock;
const [quantity, setQuantity] = useState("1");
const [currentPrice, setCurrentPrice] = useState(0);
@@ -77,7 +78,6 @@ const TradingSellScreen = ({ route, navigation }) => {
const handleSell = async () => {
console.log("๐ธ ๋งค๋ ์ฃผ๋ฌธ ์์");
- // โฌ๏ธ ์ถ๊ฐ: userId ์์ผ๋ฉด ์ฐจ๋จ
if (!userId) {
Alert.alert("์ค๋ฅ", "์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.");
return;
@@ -111,7 +111,6 @@ const TradingSellScreen = ({ route, navigation }) => {
setLoading(true);
try {
- // โฌ๏ธ ๋ณ๊ฒฝ: ์๋ฒ๊ฐ ์ฝ๋๋ง ๋ฐ์ผ๋ฏ๋ก symbol ๊ณ ์
const stockIdentifier = stock.symbol;
const orderData = {
@@ -124,7 +123,6 @@ const TradingSellScreen = ({ route, navigation }) => {
console.log("๐ก ๋งค๋ ์ฃผ๋ฌธ ๋ฐ์ดํฐ:", orderData);
- // โฌ๏ธ ๋ณ๊ฒฝ: Content-Type ๋ช
์
const response = await fetchWithAuth(
`${API_BASE_URL}trading/trade/`,
{
@@ -166,7 +164,13 @@ const TradingSellScreen = ({ route, navigation }) => {
};
const formatNumber = (number) => number.toLocaleString();
- const getChangeColor = (change) => (change > 0 ? "#F074BA" : change < 0 ? "#00BFFF" : "#AAAAAA");
+
+ const getChangeColor = (change) => {
+ if (change > 0) return theme.status.up;
+ if (change < 0) return theme.status.down;
+ return theme.status.same;
+ };
+
const getChangeSymbol = (change) => (change > 0 ? "โฒ" : change < 0 ? "โผ" : "");
const maxSellQuantity = parseInt(stock?.quantity) || 0;
@@ -174,31 +178,41 @@ const TradingSellScreen = ({ route, navigation }) => {
return (
{/* ํค๋ */}
navigation.goBack()}>
- {"<"}
+
+ {"<"}
+
- ๋งค๋
+
+ ๋งค๋
+
{/* ์ข
๋ชฉ ์ ๋ณด */}
- {stock?.name || "์ข
๋ชฉ๋ช
์์"}
- ({stock?.symbol || "์ข
๋ชฉ์ฝ๋ ์์"})
+
+ {stock?.name || "์ข
๋ชฉ๋ช
์์"}
+
+
+ ({stock?.symbol || "์ข
๋ชฉ์ฝ๋ ์์"})
+
{priceLoading ? (
-
+
) : (
<>
- {formatNumber(currentPrice)}์
+
+ {formatNumber(currentPrice)}์
+
{stock?.change !== undefined && (
{getChangeSymbol(stock.change)}
@@ -210,61 +224,85 @@ const TradingSellScreen = ({ route, navigation }) => {
-
+
{/* ํ์ฌ ๋ณด์ ๋ */}
- ํ์ฌ ๋ณด์ ๋
- {formatNumber(maxSellQuantity)}์ฃผ
+
+ ํ์ฌ ๋ณด์ ๋
+
+
+ {formatNumber(maxSellQuantity)}์ฃผ
+
{/* ํ๊ท ๋จ๊ฐ ์ ๋ณด */}
{stock?.average_price && (
- ํ๊ท ๋จ๊ฐ
- {formatNumber(stock.average_price)}์
+
+ ํ๊ท ๋จ๊ฐ
+
+
+ {formatNumber(stock.average_price)}์
+
)}
{/* ์๋ ์
๋ ฅ */}
- ๋งค๋ ์๋
+
+ ๋งค๋ ์๋
+
- ์ฃผ
+ ์ฃผ
setQuantity(maxSellQuantity.toString())}
>
- ์ ์ฒด
+
+ ์ ์ฒด
+
{maxSellQuantity > 0 && (
- ์ต๋ {formatNumber(maxSellQuantity)}์ฃผ๊น์ง ๋งค๋ ๊ฐ๋ฅ
+
+ ์ต๋ {formatNumber(maxSellQuantity)}์ฃผ๊น์ง ๋งค๋ ๊ฐ๋ฅ
+
)}
{/* ์ด ๊ธ์ก */}
-
- ์ด ๋งค๋ ๊ธ์ก
- {formatNumber(calculateTotal())}์
+
+
+ ์ด ๋งค๋ ๊ธ์ก
+
+
+ {formatNumber(calculateTotal())}์
+
{/* ์์ ์์ต */}
{stock?.average_price && (
-
- ์์ ์์ต
+
+
+ ์์ ์์ต
+
= 0 ? "#6EE69E" : "#F074BA" },
+ { color: currentPrice - stock.average_price >= 0 ? theme.status.success : theme.button.primary },
]}
>
{currentPrice - stock.average_price >= 0 ? "+" : ""}
@@ -277,17 +315,20 @@ const TradingSellScreen = ({ route, navigation }) => {
{loading ? (
-
+
) : maxSellQuantity === 0 ? (
- ๋งค๋ํ ์ฃผ์์ด ์์ต๋๋ค
+
+ ๋งค๋ํ ์ฃผ์์ด ์์ต๋๋ค
+
) : (
-
+
{formatNumber(parseInt(quantity) || 0)}์ฃผ ๋งค๋ํ๊ธฐ
)}
@@ -298,37 +339,37 @@ const TradingSellScreen = ({ route, navigation }) => {
};
const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: "#003340", paddingHorizontal: 30 },
+ container: { flex: 1, paddingHorizontal: 30 },
safeArea: { flex: 1, paddingHorizontal: 30, paddingTop: 20 },
header: { flexDirection: "row", alignItems: "center", marginBottom: 30, marginTop: 40 },
- backText: { fontSize: 28, color: "#F074BA", marginRight: 15 },
- title: { fontSize: 20, fontWeight: "bold", color: "#F074BA", flex: 1, textAlign: "center", marginRight: 43 },
+ backText: { fontSize: 28, marginRight: 15 },
+ title: { fontSize: 20, fontWeight: "bold", flex: 1, textAlign: "center", marginRight: 43 },
stockRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 20 },
stockInfo: { flex: 1 },
- stockName: { color: "white", fontSize: 18, fontWeight: "bold", marginBottom: 4 },
- stockCode: { color: "#AFA5CF", fontSize: 14 },
+ stockName: { fontSize: 18, fontWeight: "bold", marginBottom: 4 },
+ stockCode: { fontSize: 14 },
priceBlock: { alignItems: "flex-end" },
- priceText: { fontSize: 20, color: "white", fontWeight: "bold", marginBottom: 4 },
+ priceText: { fontSize: 20, fontWeight: "bold", marginBottom: 4 },
changeText: { fontSize: 14, fontWeight: "bold" },
- divider: { height: 1, backgroundColor: "#4A5A60", marginVertical: 20 },
+ divider: { height: 1, marginVertical: 20 },
infoSection: { marginBottom: 25 },
- label: { fontSize: 16, color: "#FFD1EB", marginBottom: 8 },
- value: { fontSize: 18, color: "#FFFFFF", fontWeight: "bold" },
+ label: { fontSize: 16, marginBottom: 8 },
+ value: { fontSize: 18, fontWeight: "bold" },
inputRow: { flexDirection: "row", alignItems: "center" },
- input: { backgroundColor: "#FFFFFF", borderRadius: 10, paddingHorizontal: 15, paddingVertical: 12, fontSize: 18, color: "#000000", minWidth: 100, textAlign: "center", marginRight: 10 },
- unit: { fontSize: 18, color: "#FFFFFF", marginRight: 10 },
- maxButton: { backgroundColor: "#4A5A60", paddingHorizontal: 12, paddingVertical: 8, borderRadius: 6 },
- maxButtonText: { color: "#FFFFFF", fontSize: 14, fontWeight: "bold" },
- maxInfo: { fontSize: 12, color: "#AFA5CF", marginTop: 5 },
- totalRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: 20, marginBottom: 15, paddingVertical: 15, paddingHorizontal: 20, backgroundColor: "#004455", borderRadius: 10 },
- totalLabel: { fontSize: 16, color: "#FFFFFF" },
- totalAmount: { fontSize: 20, fontWeight: "bold", color: "#F074BA" },
- profitRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 30, paddingVertical: 12, paddingHorizontal: 20, backgroundColor: "#002A35", borderRadius: 10 },
- profitLabel: { fontSize: 14, color: "#FFFFFF" },
+ input: { borderRadius: 10, paddingHorizontal: 15, paddingVertical: 12, fontSize: 18, minWidth: 100, textAlign: "center", marginRight: 10 },
+ unit: { fontSize: 18, marginRight: 10 },
+ maxButton: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 6 },
+ maxButtonText: { fontSize: 14, fontWeight: "bold" },
+ maxInfo: { fontSize: 12, marginTop: 5 },
+ totalRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: 20, marginBottom: 15, paddingVertical: 15, paddingHorizontal: 20, borderRadius: 10 },
+ totalLabel: { fontSize: 16 },
+ totalAmount: { fontSize: 20, fontWeight: "bold" },
+ profitRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 30, paddingVertical: 12, paddingHorizontal: 20, borderRadius: 10 },
+ profitLabel: { fontSize: 14 },
profitAmount: { fontSize: 16, fontWeight: "bold" },
- sellButton: { marginTop: "auto", backgroundColor: "#F074BA", borderRadius: 12, paddingVertical: 16, alignItems: "center", marginBottom: 30 },
+ sellButton: { marginTop: "auto", borderRadius: 12, paddingVertical: 16, alignItems: "center", marginBottom: 30 },
disabledButton: { backgroundColor: "#A0A0A0" },
- sellButtonText: { fontSize: 18, fontWeight: "bold", color: "#003340" },
+ sellButtonText: { fontSize: 18, fontWeight: "bold" },
});
-export default TradingSellScreen;
+export default TradingSellScreen;
\ No newline at end of file
diff --git a/src/screens/MyPage/ChangePasswordScreen.js b/src/screens/MyPage/ChangePasswordScreen.js
index 985f546..183512d 100644
--- a/src/screens/MyPage/ChangePasswordScreen.js
+++ b/src/screens/MyPage/ChangePasswordScreen.js
@@ -13,8 +13,11 @@ import { getNewAccessToken } from "../../utils/token";
import EyeOpen from "../../components/EyeOpen";
import EyeClosed from "../../components/EyeClosed";
import { fetchWithAuth } from "../../utils/token";
+import { useTheme } from "../../utils/ThemeContext";
export default function ChangePasswordScreen({ navigation }) {
+ const { theme } = useTheme();
+
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
@@ -75,21 +78,27 @@ export default function ChangePasswordScreen({ navigation }) {
};
return (
-
+
navigation.goBack()}
>
- {"<"}
+ {"<"}
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
+ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
- ํ์ฌ ๋น๋ฐ๋ฒํธ
-
+ ํ์ฌ ๋น๋ฐ๋ฒํธ
+
- ์ ๋น๋ฐ๋ฒํธ
-
+ ์ ๋น๋ฐ๋ฒํธ
+
{loading ? (
-
+
) : (
- ๋ณ๊ฒฝ
+ ๋ณ๊ฒฝ
)}
@@ -138,7 +159,6 @@ export default function ChangePasswordScreen({ navigation }) {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 30,
@@ -151,19 +171,16 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 36,
- color: "#F074BA",
},
title: {
fontSize: 24,
fontWeight: "bold",
- color: "#F074BA",
position: "absolute",
top: 150,
left: 30,
},
label: {
fontSize: 16,
- color: "#F074BA",
alignSelf: "flex-start",
marginLeft: 8,
marginBottom: 12,
@@ -174,16 +191,13 @@ const styles = StyleSheet.create({
width: "100%",
height: 50,
borderWidth: 1,
- borderColor: "#ddd",
borderRadius: 8,
- backgroundColor: "#f9f9f9",
marginBottom: 20,
paddingHorizontal: 10,
},
inputField: {
flex: 1,
fontSize: 16,
- color: "black",
},
icon: {
padding: 10,
@@ -191,19 +205,18 @@ const styles = StyleSheet.create({
button: {
width: "100%",
height: 50,
- backgroundColor: "#F074BA",
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
position: "absolute",
bottom: 80,
- },
- disabledButton: {
- backgroundColor: "#A0A0A0",
+ elevation: 4,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
},
buttonText: {
fontSize: 16,
- color: "#fff",
fontWeight: "bold",
},
-});
+});
\ No newline at end of file
diff --git a/src/screens/MyPage/EditUserInfoScreen.js b/src/screens/MyPage/EditUserInfoScreen.js
index b4839e2..b6b19bc 100644
--- a/src/screens/MyPage/EditUserInfoScreen.js
+++ b/src/screens/MyPage/EditUserInfoScreen.js
@@ -1,220 +1,220 @@
-import React, { useEffect, useState } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- Image,
- ActivityIndicator,
- Alert,
- ScrollView,
- TouchableOpacity,
- TextInput,
- Keyboard,
-} from 'react-native';
-
-import { getNewAccessToken } from '../../utils/token';
-import { fetchUserInfo } from '../../utils/user';
-import { updateUserInfo } from '../../utils/user';
-import { fetchUserMbtiType, getMbtiImage } from "../../utils/mbtiType";
-
-
-
-const EditUserInfoScreen = ({ navigation }) => {
- const [userInfo, setUserInfo] = useState(null);
- const [loading, setLoading] = useState(true);
- const [editingField, setEditingField] = useState(null);
- const [editValue, setEditValue] = useState('');
- const [mbtiType, setMbtiType] = useState(null);
-
- useEffect(() => {
- fetchUserMbtiType(navigation, setMbtiType);
- }, []);
-
- useEffect(() => {
- const loadUserData = async () => {
- try {
- const accessToken = await getNewAccessToken(navigation);
- if (!accessToken) {
- Alert.alert('์ธ์ฆ ๋ง๋ฃ', '๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
- navigation.navigate('Login');
- return;
- }
-
- await fetchUserInfo(navigation, setUserInfo);
- } catch (err) {
- Alert.alert('์ค๋ฅ', '์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.');
- } finally {
- setLoading(false);
- }
- };
-
- loadUserData();
- }, []);
-
- const handleEdit = (field, value) => {
- setEditingField(field);
- setEditValue(value);
- };
-
- const saveEdit = async () => {
- if (editingField) {
- const trimmed = editValue?.trim();
- if (trimmed === userInfo[editingField]) {
- // ๊ฐ์ด ์ ๋ฐ๋์์ผ๋ฉด ์๋ฒ ์์ฒญ ์ ๋ณด๋
- setEditingField(null);
- return;
- }
+// import React, { useEffect, useState } from 'react';
+// import {
+// View,
+// Text,
+// StyleSheet,
+// Image,
+// ActivityIndicator,
+// Alert,
+// ScrollView,
+// TouchableOpacity,
+// TextInput,
+// Keyboard,
+// } from 'react-native';
+
+// import { getNewAccessToken } from '../../utils/token';
+// import { fetchUserInfo } from '../../utils/user';
+// import { updateUserInfo } from '../../utils/user';
+// import { fetchUserMbtiType, getMbtiImage } from "../../utils/mbtiType";
+
+
+
+// const EditUserInfoScreen = ({ navigation }) => {
+// const [userInfo, setUserInfo] = useState(null);
+// const [loading, setLoading] = useState(true);
+// const [editingField, setEditingField] = useState(null);
+// const [editValue, setEditValue] = useState('');
+// const [mbtiType, setMbtiType] = useState(null);
+
+// useEffect(() => {
+// fetchUserMbtiType(navigation, setMbtiType);
+// }, []);
+
+// useEffect(() => {
+// const loadUserData = async () => {
+// try {
+// const accessToken = await getNewAccessToken(navigation);
+// if (!accessToken) {
+// Alert.alert('์ธ์ฆ ๋ง๋ฃ', '๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
+// navigation.navigate('Login');
+// return;
+// }
+
+// await fetchUserInfo(navigation, setUserInfo);
+// } catch (err) {
+// Alert.alert('์ค๋ฅ', '์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.');
+// } finally {
+// setLoading(false);
+// }
+// };
+
+// loadUserData();
+// }, []);
+
+// const handleEdit = (field, value) => {
+// setEditingField(field);
+// setEditValue(value);
+// };
+
+// const saveEdit = async () => {
+// if (editingField) {
+// const trimmed = editValue?.trim();
+// if (trimmed === userInfo[editingField]) {
+// // ๊ฐ์ด ์ ๋ฐ๋์์ผ๋ฉด ์๋ฒ ์์ฒญ ์ ๋ณด๋
+// setEditingField(null);
+// return;
+// }
- const success = await updateUserInfo(navigation, {
- [editingField]: trimmed,
- });
+// const success = await updateUserInfo(navigation, {
+// [editingField]: trimmed,
+// });
- if (success) {
- setUserInfo((prev) => ({
- ...prev,
- [editingField]: trimmed,
- }));
- } else {
- Alert.alert('์์ ์คํจ', '์ ๋ณด ์์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
- }
+// if (success) {
+// setUserInfo((prev) => ({
+// ...prev,
+// [editingField]: trimmed,
+// }));
+// } else {
+// Alert.alert('์์ ์คํจ', '์ ๋ณด ์์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
+// }
- setEditingField(null);
- setEditValue('');
- Keyboard.dismiss();
- }
- };
- if (loading) {
- return (
-
-
-
- );
- }
-
- return (
-
- navigation.goBack()} style={styles.backButton}>
- {'<'}
-
-
-
- {/* */}
-
-
- {userInfo?.nickname || '์๊ณ ๊ฐ ๋๋ํ ํ์คํฐ'}
-
-
-
- {renderEditableItem('nickname', '๋๋ค์', userInfo?.nickname)}
- {renderEditableItem('gender', '์ฑ๋ณ', userInfo?.gender === 'male' ? '๋จ์' : userInfo?.gender === 'female' ? '์ฌ์' : '๋ฏธ๋ฑ๋ก')}
- {renderEditableItem('birthdate', '์์ผ', userInfo?.birthdate)}
- {renderEditableItem('email', '์ด๋ฉ์ผ', userInfo?.email)}
- {renderEditableItem('address', '์ฃผ์', userInfo?.address)}
-
-
- );
-
- function renderEditableItem(field, label, value) {
- const isEditing = editingField === field;
-
- return (
- handleEdit(field, value)} activeOpacity={0.8}>
-
- {label}:
- {isEditing ? (
-
- ) : (
- {value || '๋ฏธ๋ฑ๋ก'}
- )}
-
-
- );
- }
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#003340',
- paddingHorizontal: 30,
- paddingTop: 60,
- },
- backButton: {
- position: 'absolute',
- top: 50,
- left: 20,
- zIndex: 10,
- },
- backText: {
- fontSize: 36,
- color: '#F074BA',
- },
- profileSection: {
- alignItems: 'center',
- marginTop: 40,
- marginBottom: 30,
- },
- profileImage: {
- width: 100,
- height: 100,
- borderRadius: 50,
- borderWidth: 3,
- borderColor: "#FFFFFFB0",
- backgroundColor: "#D4DDEF60", // โ
์ํ๋ ๋ฐฐ๊ฒฝ์
- },
- userName: {
- fontSize: 20,
- fontWeight: 'bold',
- color: '#F8C7CC',
- marginTop: 10,
- },
- infoBox: {
- backgroundColor: '#D4DDEF30',
- padding: 18,
- borderRadius: 10,
- marginBottom: 15,
- },
- infoLabel: {
- fontSize: 15,
- color: '#A9C4D3',
- marginBottom: 10,
- },
- infoValue: {
- fontSize: 18,
- fontWeight: 'bold',
- color: 'white',
- marginTop: 3,
- },
- input: {
- fontSize: 18,
- fontWeight: 'bold',
- color: 'white',
- borderBottomWidth: 1,
- borderBottomColor: '#F074BA',
- paddingVertical: 4,
- },
-});
-
-export default EditUserInfoScreen;
+// setEditingField(null);
+// setEditValue('');
+// Keyboard.dismiss();
+// }
+// };
+// if (loading) {
+// return (
+//
+//
+//
+// );
+// }
+
+// return (
+//
+// navigation.goBack()} style={styles.backButton}>
+// {'<'}
+//
+
+//
+// {/* */}
+//
+
+// {userInfo?.nickname || '์๊ณ ๊ฐ ๋๋ํ ํ์คํฐ'}
+//
+
+//
+// {renderEditableItem('nickname', '๋๋ค์', userInfo?.nickname)}
+// {renderEditableItem('gender', '์ฑ๋ณ', userInfo?.gender === 'male' ? '๋จ์' : userInfo?.gender === 'female' ? '์ฌ์' : '๋ฏธ๋ฑ๋ก')}
+// {renderEditableItem('birthdate', '์์ผ', userInfo?.birthdate)}
+// {renderEditableItem('email', '์ด๋ฉ์ผ', userInfo?.email)}
+// {renderEditableItem('address', '์ฃผ์', userInfo?.address)}
+//
+//
+// );
+
+// function renderEditableItem(field, label, value) {
+// const isEditing = editingField === field;
+
+// return (
+// handleEdit(field, value)} activeOpacity={0.8}>
+//
+// {label}:
+// {isEditing ? (
+//
+// ) : (
+// {value || '๋ฏธ๋ฑ๋ก'}
+// )}
+//
+//
+// );
+// }
+// };
+
+// const styles = StyleSheet.create({
+// container: {
+// flex: 1,
+// backgroundColor: '#003340',
+// paddingHorizontal: 30,
+// paddingTop: 60,
+// },
+// backButton: {
+// position: 'absolute',
+// top: 50,
+// left: 20,
+// zIndex: 10,
+// },
+// backText: {
+// fontSize: 36,
+// color: '#F074BA',
+// },
+// profileSection: {
+// alignItems: 'center',
+// marginTop: 40,
+// marginBottom: 30,
+// },
+// profileImage: {
+// width: 100,
+// height: 100,
+// borderRadius: 50,
+// borderWidth: 3,
+// borderColor: "#FFFFFFB0",
+// backgroundColor: "#D4DDEF60", // โ
์ํ๋ ๋ฐฐ๊ฒฝ์
+// },
+// userName: {
+// fontSize: 20,
+// fontWeight: 'bold',
+// color: '#F8C7CC',
+// marginTop: 10,
+// },
+// infoBox: {
+// backgroundColor: '#D4DDEF30',
+// padding: 18,
+// borderRadius: 10,
+// marginBottom: 15,
+// },
+// infoLabel: {
+// fontSize: 15,
+// color: '#A9C4D3',
+// marginBottom: 10,
+// },
+// infoValue: {
+// fontSize: 18,
+// fontWeight: 'bold',
+// color: 'white',
+// marginTop: 3,
+// },
+// input: {
+// fontSize: 18,
+// fontWeight: 'bold',
+// color: 'white',
+// borderBottomWidth: 1,
+// borderBottomColor: '#F074BA',
+// paddingVertical: 4,
+// },
+// });
+
+// export default EditUserInfoScreen;
diff --git a/src/screens/MyPage/FAQScreen.js b/src/screens/MyPage/FAQScreen.js
index 6822089..db703ee 100644
--- a/src/screens/MyPage/FAQScreen.js
+++ b/src/screens/MyPage/FAQScreen.js
@@ -9,6 +9,9 @@ import {
TouchableOpacity,
} from 'react-native';
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from '../../utils/ThemeContext';
+
const dummyFaqs = [
{
id: 1,
@@ -30,7 +33,7 @@ const dummyFaqs = [
question: '๋งค์์ ๋งค๋ ๋ฐฉ๋ฒ์ด ๊ถ๊ธํด์.',
answer: '์ข
๋ชฉ์ ๊ฒ์ํ ํ, ๋งค์ ๋๋ ๋งค๋ ๋ฒํผ์ ๋๋ฌ ์๋์ ์
๋ ฅํ๊ณ ์ฃผ๋ฌธ์ ์คํํ๋ฉด ๋ฉ๋๋ค.',
},
- {
+ {
id: 5,
question: '๋ชจ์ ํฌ์๋ ์ค์ ๋์ด ๋๋์?',
answer: '์๋์! ๋ชจ์ ํฌ์๋ ๊ฐ์์ ์์ฐ์ ํ์ฉํ๋ ์๋ฎฌ๋ ์ด์
์ผ๋ก, ์ค์ ๋์ด ๋ค์ง ์์ต๋๋ค.',
@@ -40,15 +43,17 @@ const dummyFaqs = [
question: '๋ชจ์ ํฌ์๋ก ์์ต์ด ๋๋ฉด ํ๊ธ์ผ๋ก ๋ฐ์ ์ ์๋์?',
answer: '๋ชจ์ ํฌ์๋ ํ์ต์ฉ ๊ธฐ๋ฅ์ด๊ธฐ ๋๋ฌธ์ ์ค์ ์์ต์ด๋ ์์ค์ ๋ฐ์ํ์ง ์์ผ๋ฉฐ, ํ๊ธ์ผ๋ก ๊ตํ๋์ง ์์ต๋๋ค.',
},
-// {
+ // {
// id: 7,
// question: '๋ญํน์ ์ด๋ป๊ฒ ๊ณ์ฐ๋๋์?',
// answer: '๋ญํน์ ์ ์ฒด ์ฌ์ฉ์ ์ค ์์ต๋ฅ ์ ๊ธฐ์ค์ผ๋ก ์ค์๊ฐ ์ง๊ณ๋๋ฉฐ, ์ผ/์ฃผ/์ ๋จ์๋ก ํ์ธํ ์ ์์ต๋๋ค.',
// },
-];
+];
const FAQScreen = ({ navigation }) => {
+ const { theme } = useTheme(); // ๐จ ํ์ฌ ํ
๋ง ์ ์ฉ
+
const [loading, setLoading] = useState(true);
const [faqs, setFaqs] = useState([]);
const [expandedId, setExpandedId] = useState(null);
@@ -57,6 +62,7 @@ const FAQScreen = ({ navigation }) => {
const loadFaqs = async () => {
setLoading(true);
try {
+ // API์์ ๋ถ๋ฌ์ค๋ ๊ฒฝ์ฐ
// const res = await fetch('https://your-api/faq');
// const data = await res.json();
setFaqs(dummyFaqs);
@@ -75,28 +81,47 @@ const FAQScreen = ({ navigation }) => {
if (loading) {
return (
-
-
+
+
);
}
return (
-
+
+ {/* ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ */}
navigation.goBack()} style={styles.backButton}>
- {'<'}
+ {'<'}
- ์์ฃผ ๋ฌป๋ ์ง๋ฌธ
+
+ ์์ฃผ ๋ฌป๋ ์ง๋ฌธ
+
{faqs.map((faq) => (
toggleExpand(faq.id)}
- style={styles.faqBox}
+ style={[
+ styles.faqBox,
+ {
+ backgroundColor: theme.background.card,
+ borderColor: theme.border.medium,
+ borderWidth: 1,
+ },
+ ]}
>
- {faq.question}
+
+ {faq.question}
+
{expandedId === faq.id && (
- {faq.answer}
+
+ {faq.answer}
+
)}
))}
@@ -110,7 +135,6 @@ export default FAQScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#003340',
paddingHorizontal: 30,
paddingTop: 60,
},
@@ -122,11 +146,9 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 36,
- color: '#F074BA',
},
title: {
fontSize: 20,
- color: '#FFFFFF',
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
@@ -135,19 +157,16 @@ const styles = StyleSheet.create({
paddingBottom: 30,
},
faqBox: {
- backgroundColor: '#D4DDEF30',
padding: 16,
borderRadius: 10,
marginBottom: 12,
},
faqQuestion: {
fontSize: 16,
- color: '#FFFFFF',
fontWeight: 'bold',
},
faqAnswer: {
marginTop: 10,
- color: '#EEEEEE',
fontSize: 15,
lineHeight: 22,
},
diff --git a/src/screens/MyPage/MyPageScreen.js b/src/screens/MyPage/MyPageScreen.js
index 3c2eacb..5f7ab14 100644
--- a/src/screens/MyPage/MyPageScreen.js
+++ b/src/screens/MyPage/MyPageScreen.js
@@ -1,4 +1,6 @@
+// /src/screens/MyPage/MyPageScreen.js
import React, { useEffect, useState } from "react";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
View,
Text,
@@ -19,15 +21,22 @@ import { fetchUserMbtiType, getMbtiImage } from "../../utils/mbtiType";
import { increaseBalance } from "../../utils/point";
import { unregisterPushToken } from "../../services/PushNotificationService";
+// ๐จ ํ
๋ง ํ
import
+import { useTheme } from "../../utils/ThemeContext";
+
const MyPageScreen = ({ navigation }) => {
- console.log("๐ MyPageScreen ๋ ๋๋ง");
+ // ๐จ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+ const { theme } = useTheme();
+ const insets = useSafeAreaInsets();
+ console.log("๐ MyPageScreen ๋ ๋๋ง");
const [userInfo, setUserInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [equippedBadges, setEquippedBadges] = useState(["๐ฅ", "๐", "๐ฏ"]);
- const [introText, setIntroText] = useState("ํฐ๋ ๋ชจ์ ํ์ฐ์ด๊ธดํด!");
const [isEditingIntro, setIsEditingIntro] = useState(false);
const [mbtiType, setMbtiType] = useState(null);
+ const [mbtiAlias, setMbtiAlias] = useState(null);
+ const [aliasLoading, setAliasLoading] = useState(false);
const DEPOSIT_AMOUNT = 100000;
@@ -35,33 +44,133 @@ const MyPageScreen = ({ navigation }) => {
fetchUserMbtiType(navigation, setMbtiType);
}, []);
- const MenuButton = ({ label, onPress }) => (
-
+ // MBTI ์ถ์ฒ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ (๋ณ๋ช
๊ฐ์ ธ์ฌ๋ผ๊ตฌ)
+ const fetchMbtiRecommendations = async () => {
+ try {
+ setAliasLoading(true);
+ console.log("๐ฏ MBTI ์ถ์ฒ ์ ๋ณด ์์ฒญ ์์");
+
+ const accessToken = await getNewAccessToken(navigation);
+ if (!accessToken) {
+ console.warn("โ ๏ธ ์ก์ธ์ค ํ ํฐ์ด ์์");
+ return;
+ }
+
+ const response = await fetch(
+ `${API_BASE_URL}mbti/result/recommendations/`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ console.log("๐ก MBTI ์ถ์ฒ API ์๋ต ์ํ:", response.status);
+
+ if (response.ok) {
+ const data = await response.json();
+ console.log("โ
MBTI ์ถ์ฒ ๋ฐ์ดํฐ:", data);
+
+ if (data.alias) {
+ setMbtiAlias(data.alias);
+ console.log("๐ญ ๋ณ๋ช
์ค์ ์๋ฃ:", data.alias);
+ } else {
+ console.warn("โ ๏ธ ์๋ต์ alias๊ฐ ์์");
+ }
+ } else {
+ const errorText = await response.text();
+ console.warn(
+ "โ MBTI ์ถ์ฒ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ์คํจ:",
+ response.status,
+ errorText
+ );
+ }
+ } catch (error) {
+ console.log("โน๏ธ MBTI ์ถ์ฒ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ์๋ฃ:", error.message || error);
+ } finally {
+ setAliasLoading(false);
+ }
+ };
+
+ // ์๋
์์ผ ํฌ๋งทํ
+ const formatBirthdate = (birthdate) => {
+ if (!birthdate) return "";
+ const date = new Date(birthdate);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}.${month}.${day}`;
+ };
+
+ // ์ด๋ฉ์ผ ๋ง์คํน
+ const maskEmail = (email) => {
+ if (!email) return "";
+ const [localPart, domain] = email.split("@");
+ if (localPart.length <= 2) return email;
+ const maskedLocal =
+ localPart.substring(0, 2) + "*".repeat(localPart.length - 2);
+ return `${maskedLocal}@${domain}`;
+ };
+
+ // ์์ผ๊น์ง D-day ๊ณ์ฐ
+ const calculateBirthdayDday = (birthdate) => {
+ if (!birthdate) return "";
+
+ const today = new Date();
+ const birth = new Date(birthdate);
+
+ const thisYearBirthday = new Date(
+ today.getFullYear(),
+ birth.getMonth(),
+ birth.getDate()
+ );
+
+ // ์ฌํด ์์ผ์ด ์ง๋ฌ์ผ๋ฉด ๋ด๋
์์ผ๋ก
+ if (thisYearBirthday < today) {
+ thisYearBirthday.setFullYear(today.getFullYear() + 1);
+ }
+ const diffTime = thisYearBirthday - today;
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return "๐ ์์ผ ์ถํ๋๋ ค์!";
+ if (diffDays === 1) return "๐ D-1";
+ return `๐ D-${diffDays}`;
+ };
+
+ // ๐จ MenuButton ์ปดํฌ๋ํธ (theme ์ ์ฉ)
+ const MenuButton = ({ label, onPress, iconColor }) => (
+
- {label}
-
+
+ {label}
+
+
);
const handleLogout = async () => {
try {
-
- // ๐ Push Token ํด์
-
+ console.log("๐ฑ Push Token ํด์ ์์");
try {
- const pushUnregisterSuccess = await unregisterPushToken();
+ const pushUnregisterSuccess = await unregisterPushToken(navigation);
if (pushUnregisterSuccess) {
console.log("โ
Push Token ํด์ ์ฑ๊ณต");
} else {
- console.warn("Push Token ํด์ ์คํจ");
+ console.warn("โ ๏ธ Push Token ํด์ ์คํจ (๊ณ์ ์งํ)");
}
} catch (pushError) {
- console.error("Push Token ํด์ ์ค ์ค๋ฅ:", pushError);
- // Push Token ํด์ ์คํจํด๋ ๋ก๊ทธ์์๋จ
+ console.log("โน๏ธ Push Token ํด์ ๊ฑด๋๋:", pushError.message || pushError);
}
- // ์๋ฒ์ ๋ก๊ทธ์์ ์์ฒญ ์๋ (์คํจํด๋ ๋ก์ปฌ ์ ๋ฆฌ๋ ์งํ)
try {
const accessToken = await getNewAccessToken(navigation);
if (accessToken) {
@@ -81,12 +190,10 @@ const MyPageScreen = ({ navigation }) => {
}
} catch (serverError) {
console.warn("โ ๏ธ ์๋ฒ ๋ก๊ทธ์์ ์์ฒญ ์ค ์ค๋ฅ:", serverError);
- // ์๋ฒ ์์ฒญ์ด ์คํจํด๋ ๋ก์ปฌ ์ ๋ฆฌ๋ ๊ณ์ ์งํ
}
- // ๋ก์ปฌ ์ ์ฅ์์ ๋ชจ๋ ๊ด๋ จ ๋ฐ์ดํฐ ์ ๋ฆฌ
await Promise.all([
- clearTokens(), // ํ ํฐ ์ ๋ฆฌ
+ clearTokens(),
AsyncStorage.removeItem("userEmail"),
AsyncStorage.removeItem("userPassword"),
AsyncStorage.removeItem("deviceId"),
@@ -99,7 +206,6 @@ const MyPageScreen = ({ navigation }) => {
{
text: "ํ์ธ",
onPress: () => {
- // navigation.reset์ ์ฌ์ฉํ์ฌ ์ด์ ํ๋ฉด ์คํ ์ ๋ฆฌ
navigation.reset({
index: 0,
routes: [{ name: "Login" }],
@@ -110,14 +216,13 @@ const MyPageScreen = ({ navigation }) => {
} catch (err) {
console.error("โ ๋ก๊ทธ์์ ์ค ์ค๋ฅ:", err);
- // ์ค๋ฅ๊ฐ ๋ฐ์ํด๋ ์ต์ํ ํ ํฐ์ ์ ๋ฆฌํ๊ณ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋
try {
await Promise.all([
clearTokens(),
AsyncStorage.removeItem("userEmail"),
AsyncStorage.removeItem("userPassword"),
- AsyncStorage.removeItem("deviceId"),
- AsyncStorage.removeItem("pushToken")
+ AsyncStorage.removeItem("deviceId"),
+ AsyncStorage.removeItem("pushToken"),
]);
} catch (cleanupError) {
console.error("โ ๋ก์ปฌ ๋ฐ์ดํฐ ์ ๋ฆฌ ์ค ์ค๋ฅ:", cleanupError);
@@ -148,11 +253,14 @@ const MyPageScreen = ({ navigation }) => {
style: "destructive",
onPress: async () => {
try {
+ console.log("๐ฑ ํ์ ํํด - Push Token ํด์ ์์");
+
+ // Push Token ํด์ ๋ฅผ ์กฐ์ฉํ๊ฒ ์ฒ๋ฆฌ
try {
- await unregisterPushToken();
- console.log("ํ์ํํด ์ Push Token ํด์ ์๋ฃ");
+ await unregisterPushToken(navigation);
+ console.log("โ
ํํด ์ Push Token ํด์ ์ฑ๊ณต");
} catch (pushError) {
- console.error("์ค๋ฅ:", pushError);
+ console.log("โน๏ธ ํํด ์ Push Token ํด์ ๊ฑด๋๋:", pushError.message || "์ ์ ์๋ ์ค๋ฅ");
}
const accessToken = await getNewAccessToken(navigation);
@@ -178,7 +286,7 @@ const MyPageScreen = ({ navigation }) => {
clearTokens(),
AsyncStorage.removeItem("userEmail"),
AsyncStorage.removeItem("userPassword"),
- AsyncStorage.removeItem("deviceId"),
+ AsyncStorage.removeItem("deviceId"),
AsyncStorage.removeItem("pushToken"),
]);
@@ -210,7 +318,11 @@ const MyPageScreen = ({ navigation }) => {
return;
}
- await fetchUserInfo(navigation, setUserInfo);
+ // ์ฌ์ฉ์ ์ ๋ณด์ MBTI ์ถ์ฒ ์ ๋ณด๋ฅผ ๋ณ๋ ฌ๋ก ๋ก๋
+ await Promise.all([
+ fetchUserInfo(navigation, setUserInfo),
+ fetchMbtiRecommendations(),
+ ]);
} catch (err) {
console.error("โ ์ฌ์ฉ์ ์ ๋ณด ๋ถ๋ฌ์ค๊ธฐ ์คํจ:", err);
Alert.alert("์ค๋ฅ", "์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.");
@@ -222,116 +334,176 @@ const MyPageScreen = ({ navigation }) => {
loadUserData();
}, []);
+ useEffect(() => {
+ const unsubscribe = navigation.addListener("focus", () => {
+ fetchMbtiRecommendations();
+ });
+
+ return unsubscribe;
+ }, [navigation]);
+
if (loading) {
return (
-
-
+
+
);
}
return (
-
-
- {/* ์ผ์ชฝ: ์ด๋ฏธ์ง + ๋๋ค์ */}
-
-
-
-
- {/* ์ค๋ฅธ์ชฝ: ๋ฑ์ง + ํ์ค์๊ฐ */}
-
-
- {equippedBadges.map((badge, index) => (
-
- {badge}
+
+
+ {/* ํ๋กํ ์น์
*/}
+
+
+ {/* ํ๋กํ ์ด๋ฏธ์ง */}
+
+
+
+
+
+ {/* ์ ์ ์ ๋ณด */}
+
+
+ {userInfo?.nickname || "์๊ณ ๊ฐ ๋๋ํ ํ์คํฐ"}
+
+
+ {/* MBTI ๋ณ๋ช
*/}
+ {aliasLoading ? (
+
+
+
+
+ ) : mbtiAlias ? (
+
+ "{mbtiAlias}"
+
+ ) : (
+
+ ๋ณ๋ช
์ ๋ถ๋ฌ์ค๋ ์ค...
+
+ )}
+
+
+ {userInfo?.email && (
+
+
+
+ {userInfo.email}
+
+
+ )}
+
+ {userInfo?.birthdate && (
+
+
+
+ {formatBirthdate(userInfo.birthdate)}
+
+
+ {calculateBirthdayDday(userInfo.birthdate)}
+
+
+ )}
- ))}
+
-
- {userInfo?.nickname || "์๊ณ ๊ฐ ๋๋ํ ํ์คํฐ"}
+
+
+
+
+ {/* ๋๋ฆผํ ์น์
*/}
+
+
+ ๐ข ์งํ ์ค์ธ ์ด๋ฒคํธ
+ navigation.navigate("Roulette")}
+ >
+
+
+ ์ผ์ผ ๋ฃฐ๋ ๋๋ฆฌ๊ธฐ
+
+
+
+
+
-
- setIsEditingIntro(true)}
+
+
+ {/* ๋ฉ๋ด ์น์
*/}
+
+
+ {/* ๐จ ํ
๋ง ๋ณ๊ฒฝ ๋ฒํผ ์ถ๊ฐ */}
+ navigation.navigate("ThemeSelector")}
+ iconColor={theme.accent.primary}
+ />
+ navigation.navigate("Notice")}
+ iconColor={theme.status.success}
+ />
+ navigation.navigate("FAQ")}
+ iconColor={theme.status.success}
+ />
+ navigation.navigate("ChangePassword")}
+ iconColor={theme.status.success}
+ />
+
+
- {isEditingIntro ? (
- setIsEditingIntro(false)}
- style={styles.introInput}
- autoFocus
- />
- ) : (
- setIsEditingIntro(true)}>
- : {introText}
-
- )}
-
-
-
- ๐น ๋๋ ค๋๋ ค ๋๋ฆผํ ๐น
-
- {/* {
- try {
- const message = await increaseBalance(navigation, DEPOSIT_AMOUNT);
- Alert.alert("์ถ์ ๋ณด์ ๋ฐ๊ธฐ", message);
- } catch (error) {
- Alert.alert("์๋ฌ", error.message || "๋ณด์ ๋ฐ๊ธฐ์ ์คํจํ์ต๋๋ค.");
- }
- }}
- >
- ์ถ์ ๋ณด์ ๋ฐ๊ธฐ
- */}
-
- navigation.navigate("Roulette")}
- >
- ์ถ์ ๋ณด์ ๋ฐ์ผ๋ฌ ๊ฐ๊ธฐ
-
-
-
-
-
-
- navigation.navigate("EditUserInfo")}
- />
- navigation.navigate("Notice")}
- />
- navigation.navigate("FAQ")}
- />
- navigation.navigate("ChangePassword")}
- />
-
-
+
);
@@ -340,135 +512,165 @@ const MyPageScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#003340",
- paddingHorizontal: 30,
- paddingTop: 60,
+ paddingHorizontal: 20,
+ paddingTop: 50,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingBottom: 24,
},
profileSection: {
- flexDirection: "row",
- alignItems: "center",
- marginTop: 30,
- marginBottom: 0,
+ marginTop: 20,
+ marginBottom: 10,
},
- profileLeft: {
+ profileCard: {
+ borderRadius: 20,
+ padding: 25,
+ flexDirection: "row",
alignItems: "center",
- marginLeft: 10,
- marginRight: 30,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
},
- profileRight: {
- flex: 1,
- justifyContent: "center",
+ profileImageContainer: {
+ position: "relative",
+ marginRight: 20
},
profileImage: {
- width: 100,
- height: 100,
+ width: 95,
+ height: 95,
borderRadius: 50,
- borderWidth: 3,
- borderColor: "#FFFFFFB0",
- backgroundColor: "#D4DDEF60",
- },
- badgeRow: {
- flexDirection: "row",
- justifyContent: "flex-start",
- marginBottom: 0,
+ backgroundColor: "rgba(212, 221, 239, 0.2)",
},
- badgeBox: {
- backgroundColor: "#FFFFFF80",
+ profileImageShadow: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: 95,
+ height: 95,
borderRadius: 50,
- paddingVertical: 6,
- paddingHorizontal: 6,
- marginRight: 8,
+ backgroundColor: "transparent",
+ borderWidth: 1,
+ borderColor: "rgba(247, 206, 229, 0.3)",
},
- badgeText: {
- fontSize: 15,
- color: "white",
- fontWeight: "bold",
+ userInfoContainer: {
+ flex: 1,
+ justifyContent: "center",
+ backgroundColor: "transparent"
},
userName: {
- fontSize: 18,
- fontWeight: "bold",
- color: "#F8C7CC",
- marginTop: 10,
- marginBottom: 5,
+ fontSize: 22,
+ fontWeight: "700",
+ marginBottom: 4,
+ marginTop: 5,
+ letterSpacing: 0.5,
},
- introRow: {
- flexDirection: "row",
- alignItems: "center",
- marginTop: 0,
- marginLeft: 0,
+ mbtiAlias: {
+ fontSize: 14,
+ fontWeight: "400",
+ marginBottom: 13,
+ letterSpacing: 0.3,
+ },
+ mbtiAliasEmpty: {
+ fontSize: 12,
+ fontWeight: "400",
+ marginBottom: 13,
+ },
+ aliasLoadingContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: 8
},
- introText: {
- fontSize: 15,
- color: "#EEEEEE",
+ aliasLoadingText: {
+ fontSize: 12,
+ marginLeft: 6,
+ fontStyle: "italic"
},
- introInput: {
+ userDetailsContainer: {
+ gap: 6
+ },
+ userDetailRow: {
+ flexDirection: "row",
+ alignItems: "center"
+ },
+ detailIcon: {
+ marginRight: 8,
+ width: 16
+ },
+ userDetailText: {
fontSize: 14,
- color: "white",
- borderBottomWidth: 1,
- borderBottomColor: "#888",
- flex: 1,
+ fontWeight: "400",
+ letterSpacing: 0.2,
+ },
+ birthdayDday: {
+ fontSize: 12,
+ fontWeight: "600",
+ marginLeft: 8,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 8,
+ overflow: "hidden",
},
divider: {
height: 1,
- backgroundColor: "#4A5A60",
- marginVertical: 20,
+ marginVertical: 25,
+ },
+ rouletteSection: {
+ marginBottom: 10
},
moneyTitle: {
- color: "#EEEEEE",
- fontSize: 18,
- marginBottom: 20,
- marginLeft: 15,
- marginTop: 5,
- fontWeight: "600",
+ fontSize: 17,
+ marginBottom: 15,
+ fontWeight: "500",
+ textAlign: "left",
+ marginLeft: 4,
},
- moneyButtonContainer: {
- flexDirection: "row",
- justifyContent: "space-between",
- marginBottom: 10,
+ rouletteButton: {
+ borderRadius: 16,
+ paddingVertical: 18,
+ paddingHorizontal: 20,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.4,
+ shadowRadius: 8,
+ elevation: 6,
},
- tiggleButton: {
- flex: 1,
- backgroundColor: "#5DB996E0",
- paddingVertical: 20,
- borderRadius: 20,
- marginHorizontal: 10,
- alignItems: "center",
+ rouletteButtonContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center"
},
- taesanButton: {
- flex: 1,
- backgroundColor: "#F074BAE0",
- paddingVertical: 20,
- borderRadius: 20,
- marginHorizontal: 10,
- alignItems: "center",
+ rouletteButtonText: {
+ fontSize: 17,
+ fontWeight: "600",
+ marginRight: 10,
+ letterSpacing: 0.3,
},
- moneyButtonText: {
- fontFamily: "Times New Roman",
- color: "#EFF1F5",
- fontSize: 18,
- fontWeight: "500",
+ menuSectionContainer: {
+ marginTop: 0,
},
menuContainer: {
- paddingVertical: 0,
- paddingHorizontal: 0,
- },
- menuRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
+ paddingBottom: 10,
},
menuButton: {
- backgroundColor: "#D4DDEF30",
- padding: 15,
- borderRadius: 10,
- marginBottom: 13,
- marginHorizontal: 5,
+ borderRadius: 12,
+ paddingVertical: 16,
+ paddingHorizontal: 18,
+ marginBottom: 10,
+ borderWidth: 1,
+ },
+ menuRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center"
},
- menuText: {
- fontSize: 16,
- color: "white",
- fontWeight: "bold",
+ menuText: {
+ fontSize: 17,
+ fontWeight: "500",
+ letterSpacing: 0.2
},
});
-export default MyPageScreen;
+export default MyPageScreen;
\ No newline at end of file
diff --git a/src/screens/MyPage/NoticeScreen.js b/src/screens/MyPage/NoticeScreen.js
index 2992829..9cda73c 100644
--- a/src/screens/MyPage/NoticeScreen.js
+++ b/src/screens/MyPage/NoticeScreen.js
@@ -12,7 +12,12 @@ import {
} from 'react-native';
import { API_BASE_URL, fetchAPI } from '../../utils/apiConfig';
+// ๐จ ํ
๋ง ์ ์ฉ
+import { useTheme } from '../../utils/ThemeContext';
+
const NoticeScreen = ({ navigation }) => {
+ const { theme } = useTheme(); // ํ์ฌ ํ
๋ง ๊ฐ์ ธ์ค๊ธฐ
+
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [notices, setNotices] = useState([]);
@@ -21,10 +26,7 @@ const NoticeScreen = ({ navigation }) => {
// ๊ณต์ง์ฌํญ ๋ชฉ๋ก ์กฐํ
const loadNotices = async () => {
try {
-
-
const result = await fetchAPI('notification/');
-
if (result.success) {
console.log('๊ณต์ง์ฌํญ ๋ก๋ฉ ์ฑ๊ณต:', result.data.length, '๊ฐ');
setNotices(result.data);
@@ -41,32 +43,10 @@ const NoticeScreen = ({ navigation }) => {
}
};
- // ๊ฐ๋ณ ๊ณต์ง์ฌํญ ์์ธ ์กฐํ
- const loadNoticeDetail = async (id) => {
- try {
- console.log(`๊ณต์ง์ฌํญ ์์ธ ์กฐํ: ID ${id}`);
-
- const result = await fetchAPI(`notification/${id}`);
-
- if (result.success) {
- console.log('๊ณต์ง์ฌํญ ์์ธ ์กฐํ ์ฑ๊ณต');
-
- return result.data;
- } else {
- console.error('๊ณต์ง์ฌํญ ์์ธ ์กฐํ ์คํจ:', result.error);
- return null;
- }
- } catch (error) {
- console.error('๊ณต์ง์ฌํญ ์์ธ ์กฐํ ์ค ์ค๋ฅ:', error);
- return null;
- }
- };
-
useEffect(() => {
loadNotices();
}, []);
- // Pull to refresh
const onRefresh = () => {
setRefreshing(true);
loadNotices();
@@ -91,52 +71,75 @@ const NoticeScreen = ({ navigation }) => {
if (loading) {
return (
-
-
- ๊ณต์ง์ฌํญ์ ๋ถ๋ฌ์ค๋ ์ค...
+
+
+
+ ๊ณต์ง์ฌํญ์ ๋ถ๋ฌ์ค๋ ์ค...
+
);
}
return (
-
+
+ {/* ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ */}
navigation.goBack()} style={styles.backButton}>
- {'<'}
+ {'<'}
-
- ๊ณต์ง์ฌํญ
-
- ๊ณต์ง์ฌํญ
+
+
}
>
{notices.length === 0 ? (
- ๋ฑ๋ก๋ ๊ณต์ง์ฌํญ์ด ์์ต๋๋ค.
+
+ ๋ฑ๋ก๋ ๊ณต์ง์ฌํญ์ด ์์ต๋๋ค.
+
) : (
notices.map((notice) => (
toggleExpand(notice.id)}
- style={styles.noticeBox}
+ style={[
+ styles.noticeBox,
+ {
+ backgroundColor: theme.background.card,
+ borderColor: theme.border.medium,
+ borderWidth: 1,
+ },
+ ]}
>
- {notice.title}
- {formatDate(notice.created_at)}
+
+ {notice.title}
+
+
+ {formatDate(notice.created_at)}
+
-
+
{expandedId === notice.id && (
-
-
+
+
{notice.content.replace(/\\r\\n/g, '\n')}
@@ -154,7 +157,6 @@ export default NoticeScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#003340',
paddingHorizontal: 30,
paddingTop: 60,
},
@@ -166,17 +168,14 @@ const styles = StyleSheet.create({
},
backText: {
fontSize: 36,
- color: '#F074BA',
},
title: {
fontSize: 20,
- color: '#FFFFFF',
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
loadingText: {
- color: '#FFFFFF',
marginTop: 10,
textAlign: 'center',
},
@@ -190,12 +189,10 @@ const styles = StyleSheet.create({
marginTop: 100,
},
emptyText: {
- color: '#EEEEEE',
fontSize: 16,
textAlign: 'center',
},
noticeBox: {
- backgroundColor: '#D4DDEF30',
padding: 16,
borderRadius: 10,
marginBottom: 12,
@@ -207,26 +204,22 @@ const styles = StyleSheet.create({
},
noticeTitle: {
fontSize: 16,
- color: '#FFFFFF',
fontWeight: 'bold',
flex: 1,
marginRight: 10,
},
noticeDate: {
fontSize: 12,
- color: '#CCCCCC',
},
noticeContentContainer: {
marginTop: 10,
},
divider: {
height: 1,
- backgroundColor: '#D4DDEF50',
marginBottom: 10,
},
noticeContent: {
- color: '#EEEEEE',
fontSize: 15,
lineHeight: 22,
},
-});
\ No newline at end of file
+});
diff --git a/src/screens/MyPage/RouletteScreen.js b/src/screens/MyPage/RouletteScreen.js
index 0598509..c5f0944 100644
--- a/src/screens/MyPage/RouletteScreen.js
+++ b/src/screens/MyPage/RouletteScreen.js
@@ -11,63 +11,30 @@ import {
StatusBar,
Platform,
Alert,
- ImageBackground,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
-import Svg, { G, Path, Text as SvgText } from 'react-native-svg';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import Svg, { G, Path, Text as SvgText, Defs, RadialGradient, Stop } from 'react-native-svg';
import { increaseBalance } from '../../utils/point';
+import { useTheme } from '../../utils/ThemeContext';
-const { width } = Dimensions.get('window');
-const WHEEL_SIZE = width * 0.75;
-const BORDER_WIDTH = 12;
+const { width, height } = Dimensions.get('window');
+const WHEEL_SIZE = width * 0.8;
+const BORDER_WIDTH = 8;
const TOTAL_SIZE = WHEEL_SIZE + BORDER_WIDTH * 2;
const RADIUS = WHEEL_SIZE / 2;
const SEGMENTS = 8;
const SEGMENT_ANGLE = 360 / SEGMENTS;
-// const prizes = [
-// 'โฉ100,000',
-// 'โฉ50,000',
-// 'โฉ30,000',
-// 'โฉ200,000',
-// 'โฉ75,000',
-// 'โฉ30,000',
-// 'โฉ150,000',
-// 'โฉ80,000',
-// ];
-
const prizes = [
- '10 ๋ง์',
- '5 ๋ง์',
- '3 ๋ง์',
- '20 ๋ง์',
- '30 ๋ง์',
- '3 ๋ง์',
- '15 ๋ง์',
- '8 ๋ง์',
-];
-
-
-// ๋ฌด์ง๊ฐ 8์ ํ๋ ํธ
-// const segmentColors = [
-// '#FF3B30', // ๋นจ๊ฐ
-// '#FF9500', // ์ฃผํฉ
-// '#FFCC00', // ๋
ธ๋
-// '#34C759', // ์ด๋ก
-// '#5AC8FA', // ์ฒญ๋ก
-// '#007AFF', // ํ๋
-// '#5856D6', // ๋ณด๋ผ
-// '#FF2D95', // ๋ถํ
-// ];
-const segmentColors = [
- '#335696D0', // ๋นจ๊ฐ
- '#003340D0', // ์ฃผํฉ
- '#335696D0', // ๋
ธ๋
- '#003340D0', // ์ด๋ก
- '#335696D0', // ์ฒญ๋ก
- '#003340D0', // ํ๋
- '#335696D0', // ๋ณด๋ผ
- '#003340D0', // ๋ถํ
+ { amount: '10๋ง์', value: 100000, display: '10๋ง' },
+ { amount: '5๋ง์', value: 50000, display: '5๋ง' },
+ { amount: '3๋ง์', value: 30000, display: '3๋ง' },
+ { amount: '20๋ง์', value: 200000, display: '20๋ง' },
+ { amount: '30๋ง์', value: 300000, display: '30๋ง' },
+ { amount: '3๋ง์', value: 30000, display: '3๋ง' },
+ { amount: '15๋ง์', value: 150000, display: '15๋ง' },
+ { amount: '8๋ง์', value: 80000, display: '8๋ง' },
];
// SVG ํฌํผ ํจ์
@@ -75,6 +42,7 @@ const polarToCartesian = (cx, cy, r, angleDeg) => {
const a = ((angleDeg - 90) * Math.PI) / 180;
return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
};
+
const describeArc = (cx, cy, r, startAngle, endAngle) => {
const start = polarToCartesian(cx, cy, r, endAngle);
const end = polarToCartesian(cx, cy, r, startAngle);
@@ -85,36 +53,82 @@ const describeArc = (cx, cy, r, startAngle, endAngle) => {
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
export default function RouletteScreen({ navigation }) {
+ const { theme } = useTheme();
const spinAnim = useRef(new Animated.Value(0)).current;
+ const pulseAnim = useRef(new Animated.Value(1)).current;
const [spinning, setSpinning] = useState(false);
const barHeight = Platform.OS === 'android' ? StatusBar.currentHeight : 0;
- const spinWheel = () => {
+ // ๋ฃฐ๋ ์ธ๊ทธ๋จผํธ ์์ (ํ๋์ฝ๋ฉ - ์ธ๋ จ๋ ๊ทธ๋ผ๋ฐ์ด์
ํ๋ ํธ)
+ const segmentColors = [
+ '#F074BA', // ๋ฉ์ธ ํํฌ
+ '#335696', // ๋ฉ์ธ ๋ธ๋ฃจ
+ '#FF6B9D', // ๋ฐ์ ํํฌ
+ '#4A90E2', // ๋ฐ์ ๋ธ๋ฃจ
+ '#E91E63', // ์งํ ํํฌ
+ '#2196F3', // ์งํ ๋ธ๋ฃจ
+ '#FF8A80', // ์ฝ๋ ํํฌ
+ '#64B5F6', // ์ค์นด์ด ๋ธ๋ฃจ
+ ];
+
+ // ํ์ค ์ ๋๋ฉ์ด์
ํจ๊ณผ
+ React.useEffect(() => {
+ const pulse = () => {
+ Animated.sequence([
+ Animated.timing(pulseAnim, {
+ toValue: 1.05,
+ duration: 1000,
+ easing: Easing.inOut(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(pulseAnim, {
+ toValue: 1,
+ duration: 1000,
+ easing: Easing.inOut(Easing.quad),
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ if (!spinning) pulse();
+ });
+ };
+ pulse();
+ }, [spinning]);
+
+ const spinWheel = async () => {
if (spinning) return;
setSpinning(true);
- const rounds = Math.floor(Math.random() * 3) + 3;
+
+ pulseAnim.setValue(1);
+
+ const rounds = Math.floor(Math.random() * 3) + 4;
const idx = Math.floor(Math.random() * SEGMENTS);
const deg = rounds * 360 + idx * SEGMENT_ANGLE + SEGMENT_ANGLE / 2;
Animated.timing(spinAnim, {
toValue: deg,
- duration: 3500,
- easing: Easing.out(Easing.cubic),
+ duration: 4000,
+ easing: Easing.bezier(0.25, 0.46, 0.45, 0.94),
useNativeDriver: true,
- }).start(() => {
+ }).start(async () => {
const final = deg % 360;
const selected = SEGMENTS - Math.floor(final / SEGMENT_ANGLE) - 1;
const prize = prizes[selected];
- const amount = parseInt(prize.replace(/[โฉ,]/g, ''), 10) * 10000;
-
- increaseBalance(navigation, amount)
- .then(msg => Alert.alert('์ถํํฉ๋๋ค! ๐', `${prize} ๋น์ฒจ!\n${msg}`))
- .catch(() => Alert.alert('์ค๋ฅ', '๋ฃฐ๋ ์ ํ๋ฃจ์ ํ ๋ฒ!\n๋ด์ผ ๋ค์ ์๋ํด๋ณด์ธ์.'))
- //.catch(() => Alert.alert('์ค๋ฅ', 'ํฌ์ธํธ ์ ๋ฆฝ์ ์คํจํ์ต๋๋ค.'))
- .finally(() => {
- setSpinning(false);
- spinAnim.setValue(final);
- });
+
+ try {
+ const msg = await increaseBalance(navigation, prize.value);
+ Alert.alert('๐ ์ถํํฉ๋๋ค!', `${prize.amount} ๋น์ฒจ!\n\n${msg}`, [
+ { text: 'ํ์ธ', style: 'default' }
+ ]);
+ } catch (error) {
+ Alert.alert(
+ 'โฐ ์ค๋์ ๊ธฐํ ์์ง',
+ '๋ฃฐ๋ ์ ํ๋ฃจ์ ํ ๋ฒ๋ง ๋์ ๊ฐ๋ฅํฉ๋๋ค.\n๋ด์ผ ๋ค์ ๋์ ํด๋ณด์ธ์! ๐',
+ [{ text: 'ํ์ธ', style: 'default' }]
+ );
+ } finally {
+ setSpinning(false);
+ spinAnim.setValue(final);
+ }
});
};
@@ -124,146 +138,299 @@ export default function RouletteScreen({ navigation }) {
});
return (
-
+
+
+ {/* ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋ฐ์ด์
*/}
+
+
{/* ํค๋ */}
navigation.goBack()}
- style={styles.backButton}
+ style={[styles.backButton, { backgroundColor: `${theme.text.primary}1A` }]}
>
- {'<'}
+
- ๋๋ ค๋๋ ค ๋๋ฆผํ
+ ๋ฐ์ผ๋ฆฌ ๋ฃฐ๋
+
- {/* ๋ฃฐ๋ */}
-
-
-
+ {/* ๋ถ์ ๋ชฉ */}
+
+
+ ๋งค์ผ ํ ๋ฒ์ ํน๋ณํ ๊ธฐํ
+
+
+ ์ด์ ์ํํด๋ณด์ธ์! ๐
+
+
+
+ {/* ๋ฃฐ๋ ์ปจํ
์ด๋ */}
+
+ {/* ์ธ๋ถ ์ฅ์ ๋ง */}
+
+
+ {/* ํฌ์ธํฐ */}
+
+
+
- {/* {[0, 90, 180, 270].map((angle, i) => (
-
+
+
+
+ {segmentColors.map((color, index) => (
+
+
+
+
+ ))}
+
+
+ {prizes.map((prize, i) => {
+ const start = i * SEGMENT_ANGLE;
+ const end = start + SEGMENT_ANGLE;
+ const path = describeArc(RADIUS, RADIUS, RADIUS - 2, start, end);
+ const fill = `url(#gradient${i})`;
+ const mid = start + SEGMENT_ANGLE / 2;
+ const angleRad = ((mid - 90) * Math.PI) / 180;
+ const tx = RADIUS + RADIUS * 0.7 * Math.cos(angleRad);
+ const ty = RADIUS + RADIUS * 0.7 * Math.sin(angleRad);
+
+ return (
+
+
+
+ {prize.display}
+
+
+ ์
+
+
+ );
+ })}
+
+
+
+ {/* ์ค์ ๋ฒํผ */}
+
- ))} */}
-
-
-
- {prizes.map((label, i) => {
- const start = i * SEGMENT_ANGLE;
- const end = start + SEGMENT_ANGLE;
- const path = describeArc(RADIUS, RADIUS, RADIUS, start, end);
- const fill = segmentColors[i];
- const mid = start + SEGMENT_ANGLE / 2;
- const angleRad = ((mid - 90) * Math.PI) / 180;
- const tx = RADIUS + RADIUS * 0.65 * Math.cos(angleRad);
- const ty = RADIUS + RADIUS * 0.65 * Math.sin(angleRad);
- return (
-
-
-
- {label}
-
-
- );
- })}
-
-
-
-
- {spinning ? '๋๋ฆฌ๋ ์ค...' : 'START'}
+ onPress={spinWheel}
+ disabled={spinning}
+ activeOpacity={0.8}
+ >
+
+ {spinning ? (
+ <>
+
+
+ ๋๋ฆฌ๋ ์ค...
+
+ >
+ ) : (
+ <>
+
+
+ START
+
+ >
+ )}
+
+
+
+
+
+ {/* ํ๋จ ์ ๋ณด */}
+
+
+
+
+ ํ๋ฃจ ํ ๋ฒ ๋ฌด๋ฃ๋ก ๋์ ๊ฐ๋ฅ
-
+
+
+
+
+ ์ต๋ 30๋ง์๊น์ง ํ๋
+
+
-
+
);
}
const styles = StyleSheet.create({
- background: {
+ container: {
flex: 1,
- width: '100%',
- height: '100%',
},
- image: {
- resizeMode: 'cover',
+ backgroundGradient: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: height * 0.6,
+ opacity: 0.95,
},
safeArea: {
flex: 1,
- backgroundColor: 'transparent',
},
header: {
flexDirection: 'row',
alignItems: 'center',
- paddingHorizontal: 16,
- height: 56,
+ justifyContent: 'space-between',
+ paddingHorizontal: 20,
+ height: 60,
+ marginTop: 10,
},
backButton: {
- position: 'absolute',
- top: 10,
- left: 20,
- zIndex: 10,
- },
- backText: {
- fontSize: 36,
- color: '#F074BA',
+ width: 44,
+ height: 44,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 22,
},
title: {
- flex: 1,
- textAlign: 'center',
- top: 5,
- color: '#FFF',
fontSize: 24,
+ fontWeight: '700',
+ textAlign: 'center',
+ },
+ headerRight: {
+ width: 44,
+ },
+ subtitleContainer: {
+ alignItems: 'center',
+ marginTop: 20,
+ marginBottom: 30,
+ },
+ subtitle: {
+ fontSize: 18,
fontWeight: '600',
+ marginBottom: 8,
},
- wheelWrapper: {
+ description: {
+ fontSize: 16,
+ fontWeight: '400',
+ },
+ wheelContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
+ position: 'relative',
+ },
+ outerRing: {
+ position: 'absolute',
+ width: TOTAL_SIZE + 20,
+ height: TOTAL_SIZE + 20,
+ borderRadius: (TOTAL_SIZE + 20) / 2,
+ borderWidth: 3,
},
- pinTop: {
+ pointerContainer: {
position: 'absolute',
- top: -(56 / 2) - BORDER_WIDTH + 160,
- zIndex: 5,
+ top: -25,
+ zIndex: 10,
},
- accentLine: {
+ pointer: {
+ width: 0,
+ height: 0,
+ backgroundColor: 'transparent',
+ borderStyle: 'solid',
+ borderLeftWidth: 15,
+ borderRightWidth: 15,
+ borderBottomWidth: 40,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ },
+ pointerShadow: {
position: 'absolute',
- width: 2,
- height: WHEEL_SIZE * 0.5,
- backgroundColor: '#FFF',
- zIndex: 3,
+ top: 2,
+ left: -13,
+ width: 0,
+ height: 0,
+ backgroundColor: 'transparent',
+ borderStyle: 'solid',
+ borderLeftWidth: 13,
+ borderRightWidth: 13,
+ borderBottomWidth: 36,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ borderBottomColor: 'rgba(0, 0, 0, 0.2)',
+ zIndex: -1,
+ },
+ wheelWrapper: {
+ position: 'relative',
+ alignItems: 'center',
+ justifyContent: 'center',
},
wheelBorder: {
position: 'absolute',
@@ -271,37 +438,60 @@ const styles = StyleSheet.create({
height: TOTAL_SIZE,
borderRadius: TOTAL_SIZE / 2,
borderWidth: BORDER_WIDTH,
- //borderColor: '#F074BA',
- borderColor: '#FFFFFFC0',
- zIndex: 1,
+ backgroundColor: 'transparent',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.3,
+ shadowRadius: 12,
+ elevation: 12,
},
wheel: {
position: 'absolute',
- width: WHEEL_SIZE,
- height: WHEEL_SIZE,
- zIndex: 2,
},
centerButton: {
position: 'absolute',
- width: WHEEL_SIZE * 0.4,
- height: WHEEL_SIZE * 0.4,
- borderRadius: (WHEEL_SIZE * 0.4) / 2,
- backgroundColor: '#FFF',
+ width: WHEEL_SIZE * 0.35,
+ height: WHEEL_SIZE * 0.35,
+ borderRadius: (WHEEL_SIZE * 0.35) / 2,
alignItems: 'center',
justifyContent: 'center',
- zIndex: 4,
shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.2,
- shadowRadius: 6,
- elevation: 8,
+ shadowOffset: { width: 0, height: 6 },
+ shadowOpacity: 0.25,
+ shadowRadius: 8,
+ elevation: 10,
+ borderWidth: 4,
},
- centerText: {
- color: '#003340',
- fontSize: 18,
+ centerButtonDisabled: {
+ opacity: 0.8,
+ transform: [{ scale: 0.95 }],
+ },
+ centerButtonInner: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ centerButtonText: {
+ fontSize: 16,
fontWeight: '700',
+ marginTop: 4,
+ },
+ bottomInfo: {
+ paddingHorizontal: 30,
+ paddingBottom: 30,
+ marginTop: 20,
+ },
+ infoCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 12,
+ marginBottom: 10,
+ borderLeftWidth: 4,
},
- disabled: {
- opacity: 0.6,
+ infoText: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginLeft: 12,
},
-});
+});
\ No newline at end of file
diff --git a/src/screens/MyPage/ThemeSelectorScreen.js b/src/screens/MyPage/ThemeSelectorScreen.js
new file mode 100644
index 0000000..2427c24
--- /dev/null
+++ b/src/screens/MyPage/ThemeSelectorScreen.js
@@ -0,0 +1,698 @@
+// src/screens/Settings/ThemeSelectorScreen.js
+import React from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ ScrollView,
+ Dimensions,
+ StatusBar,
+} from 'react-native';
+import { useTheme } from '../../utils/ThemeContext';
+import { LinearGradient } from 'expo-linear-gradient';
+import Icon from 'react-native-vector-icons/Feather';
+import * as Haptics from 'expo-haptics';
+
+const { width } = Dimensions.get('window');
+const CARD_WIDTH = (width - 60) / 2;
+const CARD_HEIGHT = 80;
+
+const ThemeSelectorScreen = ({ navigation }) => {
+ const { theme, currentTheme, changeTheme, themes } = useTheme();
+
+ const themeOptions = [
+ {
+ id: 'default',
+ name: 'Default',
+ gradient: ['#003340', '#00556F', '#F074BA'],
+ },
+ {
+ id: 'stack',
+ name: 'Main',
+ gradient: ['#F8FAF5', '#2CAD66', '#7FD99A'],
+ },
+ {
+ id: 'premium',
+ name: 'Premium',
+ gradient: ['#0A1929', '#132F4C', '#FFD700'],
+ },
+ {
+ id: 'sakura',
+ name: 'Sakura',
+ gradient: ['#FFF5F7', '#FFB7C5', '#98D8C8'],
+ },
+ {
+ id: 'ocean',
+ name: 'Ocean',
+ gradient: ['#001C30', '#00324A', '#00D4FF'],
+ },
+ {
+ id: 'autumn',
+ name: 'Autumn',
+ gradient: ['#2C1810', '#3E2418', '#DA8848'],
+ },
+ {
+ id: 'midnight',
+ name: 'Midnight',
+ gradient: ['#1A0F2E', '#2D1B4E', '#9370DB'],
+ },
+ ];
+
+ const handleThemeChange = async (themeId) => {
+ // ํ
ํฑ ํผ๋๋ฐฑ
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+
+ const success = await changeTheme(themeId);
+ if (success) {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+ }
+ };
+
+ return (
+
+
+
+ {/* iOS ์คํ์ผ ํค๋ */}
+
+ {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ navigation.goBack();
+ }}
+ style={styles.backButton}
+ >
+
+
+
+
+
+ ํ
๋ง
+
+
+ Theme Selection
+
+
+
+
+
+
+
+ {/* ํ์ฌ ํ
๋ง ๋ฐฐ๋ */}
+
+
+ CURRENT THEME
+
+ t.id === currentTheme)?.gradient || themeOptions[0].gradient}
+ start={{ x: 0, y: 0 }}
+ end={{ x: 1, y: 1 }}
+ style={styles.currentThemeBanner}
+ >
+
+
+ {themeOptions.find(t => t.id === currentTheme)?.name || 'Default'}
+
+
+
+
+
+
+
+
+ {/* ํ
๋ง ๊ทธ๋ฆฌ๋ */}
+
+
+ ALL THEMES
+
+
+ {themeOptions.map((option, index) => {
+ const isSelected = currentTheme === option.id;
+
+ return (
+
+ handleThemeChange(option.id)}
+ activeOpacity={0.8}
+ >
+
+ {/* ์ ํ ์ฒดํฌ ์ค๋ฒ๋ ์ด */}
+ {isSelected && (
+
+
+
+
+
+ )}
+
+
+ {/* iOS ์คํ์ผ ์นด๋ ๊ธ๋ก์ฐ ์ดํํธ */}
+ {isSelected && (
+
+ )}
+
+
+ {/* ์นด๋ ์๋ ํ
์คํธ */}
+
+ {option.name}
+
+
+ );
+ })}
+
+
+
+ {/* iOS ์คํ์ผ ์ธํฌ ์นด๋ */}
+
+
+
+
+
+
+ ์๋ ์ ์ฅ
+
+
+ ํ
๋ง๋ ์ฑ ์ ์ฒด์ ์ฆ์ ์ ์ฉ๋๋ฉฐ{'\n'}๋ค์ ์คํ ์์๋ ์ ์ง๋ฉ๋๋ค
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 20,
+ paddingTop: 60,
+ paddingBottom: 16,
+ },
+ backButton: {
+ width: 44,
+ height: 44,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginLeft: -12,
+ },
+ headerCenter: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ letterSpacing: 0.3,
+ },
+ headerSubtitle: {
+ fontSize: 12,
+ fontWeight: '500',
+ marginTop: 2,
+ opacity: 0.6,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingHorizontal: 20,
+ paddingBottom: 40,
+ },
+
+ // ํ์ฌ ํ
๋ง ์น์
+ currentThemeSection: {
+ marginBottom: 32,
+ },
+ sectionLabel: {
+ fontSize: 13,
+ fontWeight: '600',
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ marginBottom: 12,
+ opacity: 0.6,
+ },
+ currentThemeBanner: {
+ height: 70,
+ borderRadius: 20,
+ padding: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ overflow: 'hidden',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.2,
+ shadowRadius: 12,
+ elevation: 8,
+ },
+ bannerContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ bannerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ },
+ activeIndicator: {
+ width: 38,
+ height: 38,
+ borderRadius: 19,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+
+ // ํ
๋ง ๊ทธ๋ฆฌ๋
+ themesSection: {
+ marginBottom: 24,
+ },
+ themeGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 16,
+ },
+ themeCardWrapper: {
+ width: CARD_WIDTH,
+ alignItems: 'center',
+ gap: 8,
+ },
+ themeCard: {
+ width: CARD_WIDTH,
+ height: CARD_HEIGHT,
+ borderRadius: 16,
+ overflow: 'hidden',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.12,
+ shadowRadius: 6,
+ elevation: 4,
+ },
+ themeCardSelected: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.25,
+ shadowRadius: 12,
+ elevation: 8,
+ transform: [{ scale: 1.02 }],
+ },
+ themeCardGradient: {
+ flex: 1,
+ padding: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedOverlay: {
+ position: 'absolute',
+ top: 8,
+ right: 8,
+ zIndex: 10,
+ },
+ checkBadge: {
+ width: 22,
+ height: 22,
+ borderRadius: 11,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1.5,
+ borderColor: '#FFFFFF',
+ },
+ cardLabel: {
+ fontSize: 12,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ cardGlow: {
+ position: 'absolute',
+ top: -1.5,
+ left: -1.5,
+ right: -1.5,
+ bottom: -1.5,
+ borderRadius: 17.5,
+ borderWidth: 1.5,
+ borderColor: 'rgba(255, 255, 255, 0.3)',
+ zIndex: -1,
+ },
+
+ // ์ธํฌ ์นด๋
+ infoCard: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ padding: 16,
+ borderRadius: 16,
+ gap: 12,
+ },
+ infoIconCircle: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ infoTextContainer: {
+ flex: 1,
+ gap: 4,
+ },
+ infoTitle: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ infoText: {
+ fontSize: 12,
+ lineHeight: 16,
+ opacity: 0.8,
+ },
+});
+
+export default ThemeSelectorScreen;
+
+
+// ========================================
+// src/utils/theme.js
+// ========================================
+
+export const themes = {
+ default: {
+ background: {
+ primary: '#003340',
+ secondary: '#004455',
+ card: 'rgba(255, 255, 255, 0.09)',
+ },
+ text: {
+ primary: '#EFF1F5',
+ secondary: '#B8C5D1',
+ tertiary: '#6B7280',
+ disabled: '#AAAAAA',
+ },
+ accent: {
+ primary: '#F074BA',
+ light: '#FFD1EB',
+ pale: '#fb9dd2ff',
+ },
+ status: {
+ up: '#F074BA',
+ down: '#00BFFF',
+ same: '#AAAAAA',
+ success: '#6EE69E',
+ error: '#FF6B6B',
+ },
+ chart: {
+ colors: [
+ '#F074BA', '#3B82F6', '#34D399', '#10B981',
+ '#F59E0B', '#EF4444', '#6366F1', '#8B5CF6',
+ '#EC4899', '#F87171', '#FBBF24', '#4ADE80',
+ '#22D3EE', '#60A5FA', '#A78BFA', '#F472B6',
+ ],
+ },
+ button: {
+ primary: '#F074BA',
+ secondary: '#EFF1F5',
+ info: '#6366F1',
+ },
+ border: {
+ light: 'rgba(255, 255, 255, 0.08)',
+ medium: 'rgba(255, 255, 255, 0.1)',
+ },
+ },
+
+ stack: {
+ background: {
+ primary: '#F8FAF5',
+ secondary: '#E8EFE5',
+ card: 'rgba(44, 173, 102, 0.08)',
+ },
+ text: {
+ primary: '#1A2E1A',
+ secondary: '#2D5A3D',
+ tertiary: '#6B8E70',
+ disabled: '#A8B5A8',
+ },
+ accent: {
+ primary: '#2CAD66',
+ light: '#7FD99A',
+ pale: '#A8E6C1',
+ },
+ status: {
+ up: '#2CAD66',
+ down: '#FF8C42',
+ same: '#8E9E8E',
+ success: '#2CAD66',
+ error: '#E85D4A',
+ },
+ chart: {
+ colors: [
+ '#2CAD66', '#4ECDC4', '#667EEA', '#95E1D3',
+ '#7B68EE', '#3498DB', '#FFB84D', '#FF8C94',
+ '#9B59B6', '#1ABC9C', '#5DADE2', '#AF7AC5',
+ '#52C1B8', '#85C1E2', '#B19CD9', '#6C9A8B',
+ ],
+ },
+ button: {
+ primary: '#2CAD66',
+ secondary: '#4ECDC4',
+ info: '#667EEA',
+ },
+ border: {
+ light: 'rgba(44, 173, 102, 0.12)',
+ medium: 'rgba(44, 173, 102, 0.2)',
+ },
+ },
+
+ premium: {
+ background: {
+ primary: '#0A1929',
+ secondary: '#132F4C',
+ card: 'rgba(255, 215, 0, 0.05)',
+ },
+ text: {
+ primary: '#F0F3F7',
+ secondary: '#C2CDD9',
+ tertiary: '#8B99A8',
+ disabled: '#647586',
+ },
+ accent: {
+ primary: '#FFD700',
+ light: '#FFE97F',
+ pale: '#FFF4CC',
+ },
+ status: {
+ up: '#FFD700',
+ down: '#6495ED',
+ same: '#90A4AE',
+ success: '#4FC3F7',
+ error: '#FF7961',
+ },
+ chart: {
+ colors: [
+ '#FFD700', '#6495ED', '#4FC3F7', '#BA68C8',
+ '#FF8A65', '#4DD0E1', '#AED581', '#FFB74D',
+ '#9575CD', '#4DB6AC', '#F06292', '#7986CB',
+ '#64B5F6', '#81C784', '#FFD54F', '#A1887F',
+ ],
+ },
+ button: {
+ primary: '#FFD700',
+ secondary: '#6495ED',
+ info: '#4FC3F7',
+ },
+ border: {
+ light: 'rgba(255, 255, 255, 0.08)',
+ medium: 'rgba(255, 255, 255, 0.12)',
+ },
+ },
+
+ sakura: {
+ background: {
+ primary: '#FFF5F7',
+ secondary: '#FFE4E9',
+ card: 'rgba(255, 192, 203, 0.08)',
+ },
+ text: {
+ primary: '#2D1F2E',
+ secondary: '#5A4A5E',
+ tertiary: '#8B7A8F',
+ disabled: '#B8ADB9',
+ },
+ accent: {
+ primary: '#FFB7C5',
+ light: '#FFD4DC',
+ pale: '#FFEAF0',
+ },
+ status: {
+ up: '#98D8C8',
+ down: '#F7A4BC',
+ same: '#C5B8C9',
+ success: '#7EC4B6',
+ error: '#E88D99',
+ },
+ chart: {
+ colors: [
+ '#FFB7C5', '#98D8C8', '#C5A3D9', '#A8D8EA',
+ '#FFCAD4', '#B4E7CE', '#E8BBE0', '#87CEEB',
+ '#F7A4BC', '#7EC4B6', '#D4A5C7', '#9BD3D0',
+ '#FFC9D4', '#A5D8CF', '#E0B8D3', '#7FD5D5',
+ ],
+ },
+ button: {
+ primary: '#FFB7C5',
+ secondary: '#98D8C8',
+ info: '#C5A3D9',
+ },
+ border: {
+ light: 'rgba(255, 183, 197, 0.15)',
+ medium: 'rgba(255, 183, 197, 0.25)',
+ },
+ },
+
+ ocean: {
+ background: {
+ primary: '#001C30',
+ secondary: '#00324A',
+ card: 'rgba(0, 212, 255, 0.08)',
+ },
+ text: {
+ primary: '#E3F4FF',
+ secondary: '#B3DAF0',
+ tertiary: '#7FAEC8',
+ disabled: '#5A8CAF',
+ },
+ accent: {
+ primary: '#00D4FF',
+ light: '#66E4FF',
+ pale: '#B3F0FF',
+ },
+ status: {
+ up: '#4DFFA6',
+ down: '#FF6B9D',
+ same: '#7BA3B8',
+ success: '#26E7A6',
+ error: '#FF5C7C',
+ },
+ chart: {
+ colors: [
+ '#00D4FF', '#4DFFA6', '#FF6B9D', '#B794F4',
+ '#38B6FF', '#5EE3C1', '#FF8FB1', '#9D7EF0',
+ '#0099CC', '#4ECDC4', '#FF85A2', '#8B7FC7',
+ '#26C6DA', '#7FD99A', '#FFA0BA', '#A78BFA',
+ ],
+ },
+ button: {
+ primary: '#00D4FF',
+ secondary: '#4DFFA6',
+ info: '#B794F4',
+ },
+ border: {
+ light: 'rgba(0, 212, 255, 0.12)',
+ medium: 'rgba(0, 212, 255, 0.2)',
+ },
+ },
+
+ autumn: {
+ background: {
+ primary: '#2C1810',
+ secondary: '#3E2418',
+ card: 'rgba(218, 136, 72, 0.08)',
+ },
+ text: {
+ primary: '#FFF5E6',
+ secondary: '#E8D2B8',
+ tertiary: '#C4A885',
+ disabled: '#9E8870',
+ },
+ accent: {
+ primary: '#DA8848',
+ light: '#F4B17A',
+ pale: '#FFD6A5',
+ },
+ status: {
+ up: '#E8A24E',
+ down: '#9B6B4D',
+ same: '#A89080',
+ success: '#C69F73',
+ error: '#D64545',
+ },
+ chart: {
+ colors: [
+ '#DA8848', '#B4846C', '#E8A24E', '#8B6F47',
+ '#F4B17A', '#A67B5B', '#FFB366', '#9E7C5A',
+ '#E09F3E', '#8B7355', '#FFD085', '#A28A6B',
+ '#D4A574', '#7A6148', '#FFC872', '#B8956A',
+ ],
+ },
+ button: {
+ primary: '#DA8848',
+ secondary: '#E8A24E',
+ info: '#C69F73',
+ },
+ border: {
+ light: 'rgba(218, 136, 72, 0.15)',
+ medium: 'rgba(218, 136, 72, 0.25)',
+ },
+ },
+
+ midnight: {
+ background: {
+ primary: '#1A0F2E',
+ secondary: '#2D1B4E',
+ card: 'rgba(147, 112, 219, 0.08)',
+ },
+ text: {
+ primary: '#F0E6FF',
+ secondary: '#C9B8E4',
+ tertiary: '#9B86BD',
+ disabled: '#7A6B94',
+ },
+ accent: {
+ primary: '#9370DB',
+ light: '#B8A4E0',
+ pale: '#E0D5F7',
+ },
+ status: {
+ up: '#A78BFA',
+ down: '#60A5FA',
+ same: '#8B7FA8',
+ success: '#818CF8',
+ error: '#F472B6',
+ },
+ chart: {
+ colors: [
+ '#9370DB', '#60A5FA', '#F472B6', '#34D399',
+ '#8B5CF6', '#3B82F6', '#EC4899', '#10B981',
+ '#A78BFA', '#60A5FA', '#F87171', '#14B8A6',
+ '#C084FC', '#93C5FD', '#FDA4AF', '#6EE7B7',
+ ],
+ },
+ button: {
+ primary: '#9370DB',
+ secondary: '#60A5FA',
+ info: '#818CF8',
+ },
+ border: {
+ light: 'rgba(147, 112, 219, 0.12)',
+ medium: 'rgba(147, 112, 219, 0.2)',
+ },
+ },
+};
+
+export const defaultTheme = themes.default;
\ No newline at end of file
diff --git a/src/services/PushNotificationService.ios.js b/src/services/PushNotificationService.ios.js
index cc2b4ba..9ddc46f 100644
--- a/src/services/PushNotificationService.ios.js
+++ b/src/services/PushNotificationService.ios.js
@@ -1,170 +1,188 @@
// src/services/PushNotificationService.ios.js
-import { Platform, Alert } from 'react-native';
-import * as Notifications from 'expo-notifications';
-import * as Device from 'expo-device';
-import Constants from 'expo-constants';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import { API_BASE_URL } from '../utils/apiConfig';
-import { getNewAccessToken } from '../utils/token';
-
-// iOS: ํฌ๊ทธ๋ผ์ด๋์์๋ ๋ฐฐ๋/์ฌ์ด๋/๋ฐฐ์ง ํ์
-Notifications.setNotificationHandler({
- handleNotification: async () => ({
- shouldShowAlert: true,
- shouldPlaySound: true,
- shouldSetBadge: true,
- }),
-});
-
-const DEVICE_ID_KEY = 'deviceId';
-const PUSH_TOKEN_KEY = 'pushToken';
-const generateDeviceId = () =>
- `ios-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
-
-export async function registerPushToken(navigation) {
+import { Platform } from "react-native";
+import * as Device from "expo-device";
+import * as Notifications from "expo-notifications";
+import Constants from "expo-constants";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { API_BASE_URL } from "../utils/apiConfig";
+
+// ============================
+// ๋ด๋ถ ์ ํธ
+// ============================
+async function getOrCreateDeviceId() {
+ const KEY = "deviceId";
+ let id = await AsyncStorage.getItem(KEY);
+ if (id) return id;
+
+ const rand = Math.random().toString(36).slice(2, 10);
+ const stamp = Date.now().toString(36);
+ const model = (Device.modelId || "ios").toString().replace(/[^a-zA-Z0-9_-]/g, "");
+ id = `ios-${model}-${stamp}-${rand}`;
+ await AsyncStorage.setItem(KEY, id);
+ return id;
+}
+
+async function saveLocalToken(token) {
try {
- if (!Device.isDevice) {
- console.log('โ ๏ธ ์๋ฎฌ๋ ์ดํฐ์์๋ ํธ์ ์๋ฆผ์ด ์ ํ๋ ์ ์์ด์.');
- }
+ await AsyncStorage.setItem("pushToken", token);
+ } catch {}
+}
+async function loadLocalToken() {
+ try {
+ return (await AsyncStorage.getItem("pushToken")) || null;
+ } catch {
+ return null;
+ }
+}
- // ๊ถํ ํ์ธ/์์ฒญ
- const { status: existingStatus } = await Notifications.getPermissionsAsync();
- let finalStatus = existingStatus;
- if (existingStatus !== 'granted') {
- const { status } = await Notifications.requestPermissionsAsync();
- finalStatus = status;
- }
- if (finalStatus !== 'granted') {
- Alert.alert('์๋ฆผ ๊ถํ์ด ํ์ํฉ๋๋ค', '์ค์ > ์๋ฆผ์์ ํ์ฉํด ์ฃผ์ธ์.');
- return false;
- }
+// ============================
+// ์๋ฒ API ์ฐ๋ (/api/push-tokens)
+// ============================
+async function uploadTokenToServer(token) {
+ try {
+ const deviceId = await getOrCreateDeviceId();
+ const accessToken = await AsyncStorage.getItem("accessToken"); // ๐ ๋ก๊ทธ์ธ ํ ํฐ
+
+ // ๐ ์๋ฒ์ ์ ์ฅํ body (๋ฌธ์์ด ๊ทธ๋๋ก ExponentPushToken[...] ํฌํจ)
+ const body = {
+ token, // ExponentPushToken[...] ํํ ๊ทธ๋๋ก
+ deviceId,
+ platform: "ios",
+ };
+
+ const res = await fetch(`${API_BASE_URL}api/push-tokens`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
+ },
+ body: JSON.stringify(body),
+ });
- // EAS projectId (Dev Client/๋น๋์์ ์๋ ๋
ธ์ถ)
- const projectId =
- Constants?.easConfig?.projectId ??
- Constants?.expoConfig?.extra?.eas?.projectId;
- if (!projectId) {
- console.warn('โ projectId๊ฐ ์์ต๋๋ค. app.json์ extra.eas.projectId๋ฅผ ํ์ธํ์ธ์.');
+ if (!res.ok) {
+ console.warn("[Push] ๋ฑ๋ก ์คํจ:", res.status, await res.text());
return false;
}
- // Expo Push Token ๋ฐ๊ธ (ExponentPushToken[...])
- const { data: expoPushToken } = await Notifications.getExpoPushTokenAsync({ projectId });
- console.log('๐ Expo Push Token(iOS):', expoPushToken);
-
- // deviceId ์ค๋น
- let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
- if (!deviceId) {
- deviceId = generateDeviceId();
- await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
- }
-
- // ์๋ฒ ๋ฑ๋ก
- const ok = await sendTokenToServer(expoPushToken, deviceId, navigation);
- await AsyncStorage.setItem(PUSH_TOKEN_KEY, expoPushToken);
- if (!ok) console.warn('โ ๏ธ ์๋ฒ ๋ฑ๋ก ์คํจ(๋ก์ปฌ ์ ์ฅ์ ์๋ฃ)');
-
+ console.log("[Push] ๋ฑ๋ก ์ฑ๊ณต:", body);
+ await saveLocalToken(token);
return true;
} catch (e) {
- console.error('ํธ์ ํ ํฐ ๋ฑ๋ก ์คํจ:', e);
+ console.warn("[Push] ๋ฑ๋ก ์ค๋ฅ:", e?.message || e);
return false;
}
}
-async function sendTokenToServer(token, deviceId, navigation) {
+async function deleteTokenFromServer(token) {
try {
- const accessToken = await getNewAccessToken(navigation);
- if (!accessToken) {
- console.error('โ ์ก์ธ์ค ํ ํฐ ์์');
- return false;
- }
-
- const res = await fetch(`${API_BASE_URL}api/push-tokens/`, {
- method: 'POST',
+ const accessToken = await AsyncStorage.getItem("accessToken");
+ const res = await fetch(`${API_BASE_URL}api/push-tokens`, {
+ method: "DELETE",
headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
- body: JSON.stringify({
- token, // ExponentPushToken[...]
- deviceId,
- platform: 'ios', // iOS ๊ณ ์
- }),
+ body: JSON.stringify({ token }),
});
- const ct = res.headers.get('content-type') || '';
- const json = ct.includes('application/json') ? await res.json().catch(() => null) : null;
-
- if (res.ok && json?.ok) {
- console.log('โ
ํ ํฐ ์๋ฒ ๋ฑ๋ก ์ฑ๊ณต:', json.created ? '์ ๊ท' : '๊ธฐ์กด');
- return true;
- } else {
- const text = !json ? (await res.text().catch(() => '')).slice(0, 200) : '';
- console.warn('โ ์๋ฒ ๋ฑ๋ก ์คํจ:', res.status, json || text);
+ if (!res.ok) {
+ console.warn("[Push] ํด์ ์คํจ:", res.status, await res.text());
return false;
}
+
+ console.log("[Push] ํด์ ์ฑ๊ณต:", token);
+ return true;
} catch (e) {
- console.error('โ ์๋ฒ ์ ์ก ์ค๋ฅ:', e);
+ console.warn("[Push] ํด์ ์ค๋ฅ:", e?.message || e);
return false;
}
}
-export async function unregisterPushToken(navigation) {
+// ============================
+// ํผ๋ธ๋ฆญ API
+// ============================
+function getProjectId() {
+ return (
+ (Constants.easConfig && Constants.easConfig.projectId) ||
+ (Constants.expoConfig?.extra?.eas?.projectId) ||
+ null
+ );
+}
+
+async function ensurePermissions() {
+ const { status: existing } = await Notifications.getPermissionsAsync();
+ if (existing === "granted") return "granted";
+
+ const { status } = await Notifications.requestPermissionsAsync({
+ ios: { allowAlert: true, allowBadge: true, allowSound: true },
+ });
+ return status;
+}
+
+// ์ฑ ์์ ์ ํ ํฐ ๋ฑ๋ก
+export async function registerExpoPushToken() {
try {
- const stored = await AsyncStorage.getItem(PUSH_TOKEN_KEY);
- if (!stored) {
- console.log('๐ฑ ๋ฑ๋ก๋ ํ ํฐ ์์');
- return true;
- }
- const accessToken = await getNewAccessToken(navigation);
+ if (Platform.OS !== "ios") return { success: true, skipped: "not_ios" };
+ if (!Device.isDevice) return { success: true, skipped: "simulator" };
- const res = await fetch(`${API_BASE_URL}api/push-tokens/`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
- },
- body: JSON.stringify({ token: stored }),
- });
+ const perm = await ensurePermissions();
+ if (perm !== "granted") return { success: true, skipped: "perm_denied" };
- await AsyncStorage.removeItem(PUSH_TOKEN_KEY);
- console.log(res.ok ? 'โ
ํ ํฐ ํด์ ์๋ฃ' : 'โ ๏ธ ์๋ฒ ํด์ ์คํจ(๋ก์ปฌ ์ ๊ฑฐ ์๋ฃ)');
- return true;
+ const projectId = getProjectId();
+ if (!projectId) return { success: true, skipped: "no_project_id" };
+
+ const { data: expoPushToken } = await Notifications.getExpoPushTokenAsync({ projectId });
+
+ console.log("๐ข [Push] ExpoPushToken:", expoPushToken);
+
+ const uploaded = await uploadTokenToServer(expoPushToken);
+ return { success: true, expoPushToken, uploaded };
} catch (e) {
- await AsyncStorage.removeItem(PUSH_TOKEN_KEY);
- console.warn('ํ ํฐ ํด์ ์ค ์ค๋ฅ(๋ก์ปฌ ์ ๊ฑฐ ์๋ฃ):', e);
- return true;
+ console.warn("[Push][ERR] registerExpoPushToken:", e?.message || String(e));
+ return { success: false, error: e?.message || String(e) };
}
}
-export function setupNotificationListeners(navigation) {
- // ํฌ๊ทธ๋ผ์ด๋ ์์
- const subRecv = Notifications.addNotificationReceivedListener((n) => {
- console.log('๐ฅ(iOS) ํฌ๊ทธ๋ผ์ด๋ ์์ :', n?.request?.content);
- // ํ์ ์: ๊ณต์ง ์๋ ์ด๋ ๋ฑ
- });
+// ๋ก๊ทธ์์/ํํด ์ ํ ํฐ ํด์
+export async function unregisterPushToken() {
+ try {
+ let token = await loadLocalToken();
+
+ if (!token && Device.isDevice && Platform.OS === "ios") {
+ const projectId = getProjectId();
+ if (projectId) {
+ const { data } = await Notifications.getExpoPushTokenAsync({ projectId });
+ token = data;
+ }
+ }
- // ํญ(๋ฐฑ๊ทธ๋ผ/์ข
๋ฃ โ ๋ณต๊ท)
- const subResp = Notifications.addNotificationResponseReceivedListener((r) => {
- const data = r?.notification?.request?.content?.data || {};
- console.log('๐(iOS) ์๋ฆผ ํญ:', data);
- if (data?.screen && navigation?.navigate) {
- navigation.navigate(data.screen, data.params || {});
- } else if (data?.type === 'notice') {
- navigation?.navigate?.('NoticeScreen');
+ if (!token) {
+ console.warn("[Push] ํด์ ์คํต: ๋ก์ปฌ ํ ํฐ ์์");
+ return false;
}
- });
- // ์ฝ๋ ์คํํธ ๋ฅ๋งํฌ ์ฒ๋ฆฌ
- Notifications.getLastNotificationResponseAsync().then((initial) => {
- const data = initial?.notification?.request?.content?.data;
- if (data?.screen && navigation?.navigate) {
- navigation.navigate(data.screen, data.params || {});
+ const ok = await deleteTokenFromServer(token);
+ if (ok) {
+ await AsyncStorage.removeItem("pushToken");
}
+ return ok;
+ } catch (e) {
+ console.warn("[Push] unregister ์ค๋ฅ:", e?.message || e);
+ return false;
+ }
+}
+
+// ์๋ฆผ ๋ฆฌ์ค๋
+export function setupNotificationListeners() {
+ const recvSub = Notifications.addNotificationReceivedListener((n) => {
+ console.log("[Push][recv]:", n);
+ });
+ const respSub = Notifications.addNotificationResponseReceivedListener((r) => {
+ console.log("[Push][tap]:", r);
});
return () => {
- subRecv && Notifications.removeNotificationSubscription(subRecv);
- subResp && Notifications.removeNotificationSubscription(subResp);
+ Notifications.removeNotificationSubscription(recvSub);
+ Notifications.removeNotificationSubscription(respSub);
};
}
diff --git a/src/utils/ThemeContext.js b/src/utils/ThemeContext.js
new file mode 100644
index 0000000..b3bbf1b
--- /dev/null
+++ b/src/utils/ThemeContext.js
@@ -0,0 +1,59 @@
+// src/utils/ThemeContext.js
+import React, { createContext, useState, useContext, useEffect } from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { themes, defaultTheme } from './theme';
+
+const ThemeContext = createContext();
+
+export const ThemeProvider = ({ children }) => {
+ const [currentTheme, setCurrentTheme] = useState('default');
+ const [theme, setTheme] = useState(defaultTheme);
+
+ // ์ฑ ์์ ์ ์ ์ฅ๋ ํ
๋ง ๋ถ๋ฌ์ค๊ธฐ
+ useEffect(() => {
+ loadTheme();
+ }, []);
+
+ const loadTheme = async () => {
+ try {
+ const savedTheme = await AsyncStorage.getItem('selectedTheme');
+ if (savedTheme && themes[savedTheme]) {
+ setCurrentTheme(savedTheme);
+ setTheme(themes[savedTheme]);
+ console.log('โ
[Theme] ํ
๋ง ๋ก๋ ์๋ฃ:', savedTheme);
+ }
+ } catch (error) {
+ console.error('โ [Theme] ํ
๋ง ๋ก๋ ์คํจ:', error);
+ }
+ };
+
+ const changeTheme = async (themeName) => {
+ try {
+ if (themes[themeName]) {
+ setCurrentTheme(themeName);
+ setTheme(themes[themeName]);
+ await AsyncStorage.setItem('selectedTheme', themeName);
+ console.log('โ
[Theme] ํ
๋ง ๋ณ๊ฒฝ ์๋ฃ:', themeName);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('โ [Theme] ํ
๋ง ๋ณ๊ฒฝ ์คํจ:', error);
+ return false;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within ThemeProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/utils/point.js b/src/utils/point.js
index 56391ad..af26e6d 100644
--- a/src/utils/point.js
+++ b/src/utils/point.js
@@ -17,8 +17,16 @@ export const increaseBalance = async (navigation, amount) => {
const text = await response.text();
console.log("์์๊ธ ์ถ๊ฐ ์๋ต ๋ณธ๋ฌธ:", text);
+ // 400 ์๋ฌ(์ด๋ฏธ ์ฌ์ฉํจ)์ ๊ฒฝ์ฐ ์กฐ์ฉํ ์ฒ๋ฆฌ
+ if (response.status === 400) {
+ console.log("โฐ ๋ฃฐ๋ ์ด๋ฏธ ์ฌ์ฉํจ - ํ๋ฃจ ํ ๋ฒ ์ ํ");
+ // throw ํ์ง ๋ง๊ณ Promise.reject๋ก ์กฐ์ฉํ ์ฒ๋ฆฌ
+ return Promise.reject("already_used_today");
+ }
+
if (!response.ok) {
- throw new Error(`API ํธ์ถ ์คํจ: ${response.status}`);
+ console.log(`โ API ํธ์ถ ์คํจ: ${response.status}`);
+ return Promise.reject(`API ํธ์ถ ์คํจ: ${response.status}`);
}
const data = JSON.parse(text);
@@ -26,10 +34,19 @@ export const increaseBalance = async (navigation, amount) => {
if (data.status === "success") {
return data.message;
} else {
- throw new Error(data.message || "์ ์ ์๋ ์ค๋ฅ ๋ฐ์");
+ console.log("โ ์๋ฒ ์๋ฌ:", data.message);
+ return Promise.reject(data.message || "์ ์ ์๋ ์ค๋ฅ ๋ฐ์");
}
} catch (err) {
- console.error("์์๊ธ ์ถ๊ฐ ์คํจ:", err);
- throw err;
+ // ๋คํธ์ํฌ ์๋ฌ๋ ๊ธฐํ ์๋ฌ๋ ์กฐ์ฉํ ์ฒ๋ฆฌ
+ console.log("์์๊ธ ์ถ๊ฐ ์คํจ (์กฐ์ฉํ ์ฒ๋ฆฌ):", err.message || err);
+
+ // JSON ํ์ฑ ์๋ฌ๋ ๋คํธ์ํฌ ์๋ฌ์ ๊ฒฝ์ฐ
+ if (err.message && err.message.includes('JSON')) {
+ return Promise.reject("parsing_error");
+ }
+
+ // ๊ธฐํ ์๋ฌ์ ๊ฒฝ์ฐ
+ return Promise.reject("network_error");
}
-};
+};
\ No newline at end of file
diff --git a/src/utils/theme.js b/src/utils/theme.js
new file mode 100644
index 0000000..20dba9e
--- /dev/null
+++ b/src/utils/theme.js
@@ -0,0 +1,327 @@
+// src/utils/theme.js
+
+export const themes = {
+ // โ
๊ธฐ๋ณธ ํ
๋ง (์ ์ง)
+ default: {
+ background: {
+ primary: '#003340',
+ secondary: '#004455',
+ card: 'rgba(255, 255, 255, 0.09)',
+ },
+ text: {
+ primary: '#EFF1F5',
+ secondary: '#B8C5D1',
+ tertiary: '#6B7280',
+ disabled: '#AAAAAA',
+ },
+ accent: {
+ primary: '#F074BA',
+ light: '#FFD1EB',
+ pale: '#fb9dd2ff',
+ },
+ status: {
+ up: '#F074BA',
+ down: '#00BFFF',
+ same: '#AAAAAA',
+ success: '#6EE69E',
+ error: '#FF6B6B',
+ },
+ chart: {
+ colors: [
+ '#F074BA', '#3B82F6', '#34D399', '#10B981',
+ '#F59E0B', '#EF4444', '#6366F1', '#8B5CF6',
+ '#EC4899', '#F87171', '#FBBF24', '#4ADE80',
+ '#22D3EE', '#60A5FA', '#A78BFA', '#F472B6',
+ ],
+ },
+ button: {
+ primary: '#F074BA',
+ secondary: '#EFF1F5',
+ info: '#6366F1',
+ },
+ border: {
+ light: 'rgba(255, 255, 255, 0.08)',
+ medium: 'rgba(255, 255, 255, 0.1)',
+ },
+ },
+
+ // ๐ฟ stack - ๋ฏผํธ ํ์ดํธ
+ stack: {
+ background: {
+ primary: '#F9FCF9',
+ secondary: '#EAF4ED',
+ card: 'rgba(44, 173, 102, 0.06)',
+ },
+ text: {
+ primary: '#1F3A28',
+ secondary: '#3F5A46',
+ tertiary: '#78937B',
+ disabled: '#A8B8A8',
+ },
+ accent: {
+ primary: '#2CAD66',
+ light: '#A8E6C1',
+ pale: '#DFF9E9',
+ },
+ status: {
+ up: '#A8E6C1',
+ down: '#FFCA61',
+ same: '#94A59B',
+ success: '#4ADE80',
+ error: '#E85D4A',
+ },
+ chart: {
+colors: [
+ '#2CAD66',
+ '#FFD166',
+ '#6EE69E',
+ '#FF8FB1',
+ '#26C6DA',
+ '#B39DDB',
+ '#7FD99A',
+ '#E85D4A',
+ '#81C784',
+ '#F4E285',
+ '#A78BD4',
+ '#4FC3F7',
+ '#FFB74D',
+ '#C8E6C9',
+ '#F8BBD0',
+ '#CFD8DC',
+]
+
+ },
+ button: {
+ primary: '#2CAD66',
+ secondary: '#C7EFD4',
+ info: '#4ECDC4',
+ },
+ border: {
+ light: 'rgba(44, 173, 102, 0.12)',
+ medium: 'rgba(44, 173, 102, 0.22)',
+ },
+ },
+
+ // โจ premium - ํ์ดํธ & ๊ณจ๋
+ premium: {
+ background: {
+ primary: '#FDFBF7',
+ secondary: '#F5F1E6',
+ card: 'rgba(255, 215, 0, 0.07)',
+ },
+ text: {
+ primary: '#3A3A3A',
+ secondary: '#6B6B6B',
+ tertiary: '#9B9B9B',
+ disabled: '#C0C0C0',
+ },
+ accent: {
+ primary: '#C6A200',
+ light: '#FFE97F',
+ pale: '#FFF5CC',
+ },
+ status: {
+ up: '#C6A200',
+ down: '#6BB1FF',
+ same: '#A8A8A8',
+ success: '#7FD4C2',
+ error: '#FF7C7C',
+ },
+ chart: {
+ colors: [
+ '#C6A200', '#FFE97F', '#FFB84D', '#BFA181',
+ '#4FC3F7', '#FFD54F', '#8BC34A', '#FDD835',
+ '#BA68C8', '#FF8A65', '#FFD740', '#AED581',
+ '#4DD0E1', '#FFF9C4', '#FFB300', '#FFE082',
+ ],
+ },
+ button: {
+ primary: '#C6A200',
+ secondary: '#F7E8B3',
+ info: '#BFA181',
+ },
+ border: {
+ light: 'rgba(198, 162, 0, 0.15)',
+ medium: 'rgba(198, 162, 0, 0.25)',
+ },
+ },
+
+ // ๐ธ sakura (์ ์ง)
+ sakura: {
+ background: {
+ primary: '#FFF5F7',
+ secondary: '#FFE4E9',
+ card: 'rgba(255, 192, 203, 0.08)',
+ },
+ text: {
+ primary: '#2D1F2E',
+ secondary: '#5A4A5E',
+ tertiary: '#8B7A8F',
+ disabled: '#B8ADB9',
+ },
+ accent: {
+ primary: '#FFB7C5',
+ light: '#FFD4DC',
+ pale: '#FFEAF0',
+ },
+ status: {
+ up: '#98D8C8',
+ down: '#F7A4BC',
+ same: '#C5B8C9',
+ success: '#7EC4B6',
+ error: '#E88D99',
+ },
+ chart: {
+ colors: [
+ '#FFB7C5', '#98D8C8', '#A8D8EA', '#F7A4BC',
+ '#C5A3D9', '#E0B8D3', '#FFC9D4', '#B4E7CE',
+ '#E8BBE0', '#87CEEB', '#9BD3D0', '#FFD1FF',
+ '#A5D8CF', '#FAD0C4', '#E0B8D3', '#7FD5D5',
+ ],
+ },
+ button: {
+ primary: '#FFB7C5',
+ secondary: '#98D8C8',
+ info: '#C5A3D9',
+ },
+ border: {
+ light: 'rgba(255, 183, 197, 0.15)',
+ medium: 'rgba(255, 183, 197, 0.25)',
+ },
+ },
+
+ // ๐ฉต ocean - ์ค์นด์ด๋ธ๋ฃจ & ํ์ดํธ
+ ocean: {
+ background: {
+ primary: '#F4FBFF',
+ secondary: '#E5F6FD',
+ card: 'rgba(0, 168, 232, 0.07)',
+ },
+ text: {
+ primary: '#0F2A3A',
+ secondary: '#355870',
+ tertiary: '#7195A8',
+ disabled: '#A5B7C2',
+ },
+ accent: {
+ primary: '#00A8E8',
+ light: '#7FD4F9',
+ pale: '#D9F5FF',
+ },
+ status: {
+ up: '#00C6A8',
+ down: '#FF8FB1',
+ same: '#9DBCC6',
+ success: '#34D399',
+ error: '#FF6B81',
+ },
+ chart: {
+ colors: [
+ '#00A8E8', '#34D399', '#7FD4F9', '#FFB84D',
+ '#5EE3C1', '#F59E0B', '#6EE7B7', '#9D7EF0',
+ '#22D3EE', '#FF85A2', '#1ABC9C', '#60A5FA',
+ '#81C784', '#A78BFA', '#4ECDC4', '#64B5F6',
+ ],
+ },
+ button: {
+ primary: '#00A8E8',
+ secondary: '#9EE3FA',
+ info: '#6BB3E5',
+ },
+ border: {
+ light: 'rgba(0, 168, 232, 0.15)',
+ medium: 'rgba(0, 168, 232, 0.25)',
+ },
+ },
+
+ // ๐ autumn - ์ฝ๋ ๋ฒ ์ด์ง
+ autumn: {
+ background: {
+ primary: '#FFF9F4',
+ secondary: '#FDEFE4',
+ card: 'rgba(218, 136, 72, 0.07)',
+ },
+ text: {
+ primary: '#3A251A',
+ secondary: '#6B4E3A',
+ tertiary: '#A0765B',
+ disabled: '#BFA592',
+ },
+ accent: {
+ primary: '#E7985A',
+ light: '#FBCB9E',
+ pale: '#FFE8D0',
+ },
+ status: {
+ up: '#F8B878',
+ down: '#C6794B',
+ same: '#A08D7B',
+ success: '#E8A24E',
+ error: '#D64545',
+ },
+ chart: {
+ colors: [
+ '#E7985A', '#FBCB9E', '#C6794B', '#FFD085',
+ '#D4A574', '#F8B878', '#FFB366', '#FFC872',
+ '#E09F3E', '#F7D4B2', '#FFCD94', '#EFC97B',
+ '#FFB84D', '#F4B17A', '#DDA56B', '#FDD5A5',
+ ],
+ },
+ button: {
+ primary: '#E7985A',
+ secondary: '#F7D4B2',
+ info: '#DDA56B',
+ },
+ border: {
+ light: 'rgba(231, 152, 90, 0.15)',
+ medium: 'rgba(231, 152, 90, 0.25)',
+ },
+ },
+
+ // ๐ midnight - ๋ผ๋ฒค๋ ๊ทธ๋ ์ด
+ midnight: {
+ background: {
+ primary: '#F8F6FB',
+ secondary: '#EDE9F7',
+ card: 'rgba(147, 112, 219, 0.07)',
+ },
+ text: {
+ primary: '#2E254A',
+ secondary: '#5A4E7A',
+ tertiary: '#8A7FA5',
+ disabled: '#B8B0C9',
+ },
+ accent: {
+ primary: '#9370DB',
+ light: '#C3B1F2',
+ pale: '#E9E1FF',
+ },
+ status: {
+ up: '#B09EFF',
+ down: '#F6A6E0',
+ same: '#A8A0C0',
+ success: '#A78BFA',
+ error: '#F472B6',
+ },
+ chart: {
+ colors: [
+ '#9370DB', '#A78BFA', '#F472B6', '#C3B1F2',
+ '#60A5FA', '#8B5CF6', '#E9E1FF', '#9B86BD',
+ '#C084FC', '#FDA4AF', '#818CF8', '#D4BFFF',
+ '#CDB5FF', '#E2C9FA', '#F5B9E7', '#A6A2FF',
+ ],
+ },
+ button: {
+ primary: '#9370DB',
+ secondary: '#C3B1F2',
+ info: '#B09EFF',
+ },
+ border: {
+ light: 'rgba(147, 112, 219, 0.15)',
+ medium: 'rgba(147, 112, 219, 0.25)',
+ },
+ },
+};
+
+// ๊ธฐ๋ณธ ํ
๋ง
+export const defaultTheme = themes.default;