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;