diff --git a/.env.example b/.env.example index 8b64a7a3..72380b1d 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # ============================================== # API Configuration # ============================================== -NEXT_PUBLIC_API_URL=https://api.5130.co.kr +API_URL=https://api.5130.co.kr # Frontend URL (for CORS) NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 diff --git a/.env.production b/.env.production index ce22447b..2ac8054f 100644 --- a/.env.production +++ b/.env.production @@ -1,7 +1,7 @@ # ============================================== # API Configuration # ============================================== -NEXT_PUBLIC_API_URL=https://api.codebridge-x.com +API_URL=https://api.codebridge-x.com # Frontend URL (for CORS) NEXT_PUBLIC_FRONTEND_URL=https://dev.codebridge-x.com diff --git a/src/app/[locale]/(protected)/layout.tsx b/src/app/[locale]/(protected)/layout.tsx index e36bca6b..b53eb0f4 100644 --- a/src/app/[locale]/(protected)/layout.tsx +++ b/src/app/[locale]/(protected)/layout.tsx @@ -4,6 +4,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard'; import AuthenticatedLayout from '@/layouts/AuthenticatedLayout'; import { RootProvider } from '@/contexts/RootProvider'; import { ApiErrorProvider } from '@/contexts/ApiErrorContext'; +import { FCMProvider } from '@/contexts/FCMProvider'; /** * Protected Layout @@ -13,6 +14,7 @@ import { ApiErrorProvider } from '@/contexts/ApiErrorContext'; * - Apply common layout (sidebar, header) to all protected pages * - Provide global context (RootProvider) * - Provide API error handling context (ApiErrorProvider) + * - Initialize FCM push notifications (Capacitor native apps) * - Prevent browser back button cache issues * - Centralized protection for all routes under (protected) * @@ -35,9 +37,9 @@ export default function ProtectedLayout({ // ๐Ÿšจ ApiErrorProvider: Server Action 401 ์—๋Ÿฌ ์‹œ ์ž๋™ ๋กœ๊ทธ์ธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ return ( - - {children} - + + {children} + ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/not-found.tsx b/src/app/[locale]/(protected)/not-found.tsx index 6e209c17..292541ee 100644 --- a/src/app/[locale]/(protected)/not-found.tsx +++ b/src/app/[locale]/(protected)/not-found.tsx @@ -1,4 +1,7 @@ +'use client'; + import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { SearchX, Home, ArrowLeft, Map } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -12,6 +15,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; * - ๋ณดํ˜ธ๋œ ๊ฒฝ๋กœ ๋‚ด์—์„œ 404 ๋ฐœ์ƒ ์‹œ ํ‘œ์‹œ */ export default function ProtectedNotFoundPage() { + const router = useRouter(); return (
@@ -56,13 +60,11 @@ export default function ProtectedNotFoundPage() {

1250 X 250px, 10MB ์ดํ•˜์˜ PNG, JPEG, GIF @@ -371,6 +422,7 @@ export function AccountInfoClient() { id="email-consent" checked={marketingConsent.email.agreed} onCheckedChange={(checked) => handleMarketingChange('email', checked as boolean)} + disabled={isSavingMarketing} />

+

+ ์ •๋ง ํƒˆํ‡ดํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? +
+ + ๋ชจ๋“  ํ…Œ๋„ŒํŠธ์—์„œ ํƒˆํ‡ด ์ฒ˜๋ฆฌ๋˜๋ฉฐ, SAM ์„œ๋น„์Šค์—์„œ ์™„์ „ํžˆ ํƒˆํ‡ด๋ฉ๋‹ˆ๋‹ค. + +

+
+ + setWithdrawPassword(e.target.value)} + disabled={isWithdrawing} + /> +
+
@@ -425,7 +496,7 @@ export function AccountInfoClient() { {isWithdrawing ? '์ฒ˜๋ฆฌ ์ค‘...' : 'ํ™•์ธ'} diff --git a/src/layouts/AuthenticatedLayout.tsx b/src/layouts/AuthenticatedLayout.tsx index 6705c992..ec9041e6 100644 --- a/src/layouts/AuthenticatedLayout.tsx +++ b/src/layouts/AuthenticatedLayout.tsx @@ -139,22 +139,39 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); // ๋ฉ”๋‰ด ํƒ์ƒ‰ ํ•จ์ˆ˜: ๋ฉ”์ธ ๋ฉ”๋‰ด์™€ ์„œ๋ธŒ๋ฉ”๋‰ด ๋ชจ๋‘ ํƒ์ƒ‰ + // ๊ฒฝ๋กœ ๋งค์นญ: ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๊ฑฐ๋‚˜ ํ•˜์œ„ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ๋งŒ ๋งค์นญ (์˜ˆ: /hr/attendance๋Š” /hr/attendance-management์™€ ๋งค์นญ๋˜๋ฉด ์•ˆ๋จ) + const isPathMatch = (menuPath: string, currentPath: string): boolean => { + if (currentPath === menuPath) return true; + // ํ•˜์œ„ ๊ฒฝ๋กœ ํ™•์ธ: /menu/path/subpath ํ˜•ํƒœ๋งŒ ๋งค์นญ (์Šฌ๋ž˜์‹œ๋กœ ๊ตฌ๋ถ„) + return currentPath.startsWith(menuPath + '/'); + }; + const findActiveMenu = (items: MenuItem[]): { menuId: string; parentId?: string } | null => { + // ๋ชจ๋“  ๋งค์นญ ๊ฐ€๋Šฅํ•œ ๋ฉ”๋‰ด ์ˆ˜์ง‘ (๊ฐ€์žฅ ๊ธด ๊ฒฝ๋กœ๊ฐ€ ๊ฐ€์žฅ ๊ตฌ์ฒด์ ) + const matches: { menuId: string; parentId?: string; pathLength: number }[] = []; + for (const item of items) { - // ์„œ๋ธŒ๋ฉ”๋‰ด๊ฐ€ ์žˆ์œผ๋ฉด ๋จผ์ € ํ™•์ธ (๋” ๊ตฌ์ฒด์ ์ธ ๊ฒฝ๋กœ ์šฐ์„ ) + // ์„œ๋ธŒ๋ฉ”๋‰ด ํ™•์ธ if (item.children && item.children.length > 0) { for (const child of item.children) { - if (child.path && normalizedPath.startsWith(child.path)) { - return { menuId: child.id, parentId: item.id }; + if (child.path && isPathMatch(child.path, normalizedPath)) { + matches.push({ menuId: child.id, parentId: item.id, pathLength: child.path.length }); } } } - // ์„œ๋ธŒ๋ฉ”๋‰ด์—์„œ ๋งค์นญ๋˜์ง€ ์•Š์œผ๋ฉด ํ˜„์žฌ ๋ฉ”๋‰ด ํ™•์ธ - if (item.path && normalizedPath.startsWith(item.path)) { - return { menuId: item.id }; + // ๋ฉ”์ธ ๋ฉ”๋‰ด ํ™•์ธ + if (item.path && item.path !== '#' && isPathMatch(item.path, normalizedPath)) { + matches.push({ menuId: item.id, pathLength: item.path.length }); } } + + // ๊ฐ€์žฅ ๊ธด ๊ฒฝ๋กœ(๊ฐ€์žฅ ๊ตฌ์ฒด์ ์ธ ๋งค์นญ) ๋ฐ˜ํ™˜ + if (matches.length > 0) { + matches.sort((a, b) => b.pathLength - a.pathLength); + return { menuId: matches[0].menuId, parentId: matches[0].parentId }; + } + return null; }; diff --git a/src/lib/auth/logout.ts b/src/lib/auth/logout.ts index 4a7bece4..940de6f1 100644 --- a/src/lib/auth/logout.ts +++ b/src/lib/auth/logout.ts @@ -5,7 +5,8 @@ * 1. Zustand ์Šคํ† ์–ด ์ดˆ๊ธฐํ™” (๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ) * 2. sessionStorage ์บ์‹œ ์‚ญ์ œ (page_config_*, mes-*) * 3. localStorage ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์‚ญ์ œ (mes-currentUser, mes-users) - * 4. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ (HttpOnly ์ฟ ํ‚ค ์‚ญ์ œ) + * 4. FCM ํ† ํฐ ํ•ด์ œ (Capacitor ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ) + * 5. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ (HttpOnly ์ฟ ํ‚ค ์‚ญ์ œ) * * @see claudedocs/security/[PLAN-2025-12-12] tenant-data-isolation-implementation.md */ @@ -13,6 +14,8 @@ import { useMasterDataStore } from '@/stores/masterDataStore'; import { useItemStore } from '@/stores/itemStore'; +// FCM์€ Capacitor ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ (๋™์  import๋กœ ์›น ๋นŒ๋“œ ์—๋Ÿฌ ๋ฐฉ์ง€) + // ===== ์บ์‹œ ์‚ญ์ œ ๋Œ€์ƒ Prefix ===== const SESSION_STORAGE_PREFIXES = [ @@ -144,7 +147,8 @@ export async function callLogoutAPI(): Promise { * 1. Zustand ์Šคํ† ์–ด ์ดˆ๊ธฐํ™” (์ฆ‰์‹œ UI ๋ฐ˜์˜) * 2. sessionStorage ์บ์‹œ ์‚ญ์ œ * 3. localStorage ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์‚ญ์ œ - * 4. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ + * 4. FCM ํ† ํฐ ํ•ด์ œ (Capacitor ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ) + * 5. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ * * @param options.skipServerLogout - ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ ์ƒ๋žต ์—ฌ๋ถ€ (๊ธฐ๋ณธ: false) * @param options.redirectTo - ๋กœ๊ทธ์•„์›ƒ ํ›„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๊ฒฝ๋กœ (๊ธฐ๋ณธ: null) @@ -168,14 +172,27 @@ export async function performFullLogout(options?: { // 3. localStorage ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์‚ญ์ œ clearLocalStorageCache(); - // 4. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ (HttpOnly ์ฟ ํ‚ค ์‚ญ์ œ) + // 4. FCM ํ† ํฐ ํ•ด์ œ (Capacitor ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์—์„œ๋งŒ ์‹คํ–‰) + // ๋™์  import๋กœ @capacitor/core ๋นŒ๋“œ ์—๋Ÿฌ ๋ฐฉ์ง€ + try { + const fcm = await import('@/lib/capacitor/fcm'); + if (fcm.isCapacitorNative()) { + await fcm.unregisterFCMToken(); + console.log('[Logout] FCM token unregistered'); + } + } catch { + // Capacitor ๋ชจ๋“ˆ์ด ์—†๋Š” ํ™˜๊ฒฝ (์›น) - ๋ฌด์‹œ + console.log('[Logout] Skipping FCM (not in native app)'); + } + + // 5. ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ (HttpOnly ์ฟ ํ‚ค ์‚ญ์ œ) if (!skipServerLogout) { await callLogoutAPI(); } console.log('[Logout] Full logout completed successfully'); - // 5. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (์„ ํƒ์ ) + // 6. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (์„ ํƒ์ ) if (redirectTo && typeof window !== 'undefined') { window.location.href = redirectTo; }