Quellcode durchsuchen

fix(config): SSR config null 시 전체 페이지 크래시 방지

fetchConfig() 가 null 반환(API/SSR 일시 장애·DB Config 0행) 시 ConfigProvider 가
null 을 그대로 하위에 내려, useConfigContext 가 throw 하거나 Header 가 config.navMenu 를
null 에서 읽어 React #310 으로 전체 페이지가 client-side exception 으로 다운되던 결함 수정
(2026-07-11 antooza.com 실장애).

- configProvider: DEFAULT_CONFIG(모든 하위 폼을 널/기본 리프로 채움) 추가.
  ConfigProvider 는 initialConfig ?? DEFAULT_CONFIG, useConfigContext 는
  useContext ?? DEFAULT_CONFIG 로 null 대체 → 11개 소비 컴포넌트 자동 null-safe
- Header: config.navMenu → config?.navMenu 옵셔널 체이닝, baseNavItems 폴백 보장

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KIM-JINO5 vor 2 Wochen
Ursprung
Commit
143b3bd7a0
2 geänderte Dateien mit 131 neuen und 11 gelöschten Zeilen
  1. 1 1
      app/component/Header.tsx
  2. 130 10
      contexts/configProvider.tsx

+ 1 - 1
app/component/Header.tsx

@@ -56,7 +56,7 @@ export default function Header({ sidebarOpen, onToggle, showAside = true }: Prop
 	const config = useConfigContext();
 	const config = useConfigContext();
 
 
 	// 상단 메뉴: 관리자 관리형(config.navMenu)이 있으면 사용, 없으면 하드코딩 폴백
 	// 상단 메뉴: 관리자 관리형(config.navMenu)이 있으면 사용, 없으면 하드코딩 폴백
-	const baseItems = config.navMenu && config.navMenu.length > 0 ? config.navMenu : baseNavItems;
+	const baseItems = config?.navMenu && config.navMenu.length > 0 ? config.navMenu : baseNavItems;
 	const navItems = FEATURE_CHANNEL ? [...channelNavItems, ...baseItems] : baseItems;
 	const navItems = FEATURE_CHANNEL ? [...channelNavItems, ...baseItems] : baseItems;
 
 
 	const scrollTabs = useCallback((dir: 'left' | 'right') => {
 	const scrollTabs = useCallback((dir: 'left' | 'right') => {

+ 130 - 10
contexts/configProvider.tsx

@@ -3,27 +3,147 @@
 import { createContext, useContext } from 'react';
 import { createContext, useContext } from 'react';
 import Config from '@/types/config';
 import Config from '@/types/config';
 
 
+// SSR fetchConfig 실패(API/SSR 일시 장애, DB Config 0행) 시 사용하는 안전 기본값.
+// 모든 하위 폼 객체를 널/기본 리프로 채워, 소비 컴포넌트의 config.account.X · config.company.X ·
+// config.attendance.X 같은 직접 접근이 크래시 없이 널을 반환하도록 보장한다.
+const DEFAULT_CONFIG: Config = {
+	id: 0,
+	basic: {
+		siteName: null,
+		siteURL: null,
+		rootID: null,
+		fromEmail: null,
+		fromName: null,
+		smtpServer: null,
+		smtpPort: null,
+		smtpEnableSSL: false,
+		smtpUsername: null,
+		smtpPassword: null,
+		adminWhiteIPList: null,
+		frontWhiteIPList: null,
+		blockAlertTitle: null,
+		blockAlertContent: null,
+		isMaintenance: false,
+		maintenanceContent: null
+	},
+	images: {
+		faviconPath: null,
+		logoSquarePath: null,
+		logoHorizontalPath: null,
+		ogDefaultPath: null,
+		twitterImagePath: null,
+		appleTouchIconPath: null,
+		appIcon192Path: null,
+		appIcon512Path: null
+	},
+	meta: {
+		keywords: null,
+		description: null,
+		author: null,
+		viewport: null,
+		applicationName: null,
+		generator: null,
+		robots: null,
+		adds: null
+	},
+	company: {
+		name: null,
+		regNo: null,
+		address: null,
+		zipCode: null,
+		owner: null,
+		tel: null,
+		fax: null,
+		retailSaleNo: null,
+		addedSaleNo: null,
+		hosting: null,
+		adminName: null,
+		adminEmail: null,
+		siteUrl: null,
+		bankCode: null,
+		bankOwner: null,
+		bankNumber: null
+	},
+	account: {
+		isRegisterBlock: false,
+		isRegisterEmailAuth: false,
+		passwordMinLength: null,
+		passwordUppercaseLength: null,
+		passwordNumbersLength: null,
+		passwordSpecialcharsLength: null,
+		deniedEmailList: null,
+		deniedNameList: null,
+		changeEmailDay: null,
+		changeNameDay: null,
+		changeSummaryDay: null,
+		changeIntroDay: null,
+		changeThumbDay: null,
+		changePasswordDay: null,
+		isLoginEmailVerifiedOnly: false,
+		maxLoginTryCount: null,
+		maxLoginTryLimitSecond: null
+	},
+	emailTemplate: {
+		registerEmailFormTitle: null,
+		registerEmailFormContent: null,
+		registrationEmailFormTitle: null,
+		registrationEmailFormContent: null,
+		resetPasswordEmailFormTitle: null,
+		resetPasswordEmailFormContent: null,
+		changedPasswordEmailFormTitle: null,
+		changedPasswordEmailFormContent: null,
+		withdrawEmailFormTitle: null,
+		withdrawEmailFormContent: null,
+		emailVerifyFormTitle: null,
+		emailVerifyFormContent: null,
+		changedEmailFormTitle: null,
+		changedEmailFormContent: null
+	},
+	external: {
+		youTubeApiKeyEnc: null,
+		youTubeApiName: null,
+		googleClientId: null,
+		googleClientSecretEnc: null,
+		googleAppId: null
+	},
+	toss: {
+		tossPayMode: null,
+		hasTestClientKey: false,
+		hasTestSecretKey: false,
+		hasLiveClientKey: false,
+		hasLiveSecretKey: false
+	},
+	attendance: {
+		isEnabled: false,
+		baseExp: 0,
+		basePoint: 0,
+		useStreakBonus: false,
+		streakBonusPerDay: 0,
+		streakBonusPointPerDay: 0,
+		streakBonusMaxDays: 0,
+		useRankBonus: false,
+		rankBonusConfig: null
+	},
+	navMenu: []
+};
+
 const ConfigContext = createContext<Config|null>(null);
 const ConfigContext = createContext<Config|null>(null);
 
 
-// Context Provider
+// Context Provider — initialConfig 가 null(SSR 장애)이어도 DEFAULT_CONFIG 로 대체해
+// 하위 트리가 절대 null config 를 만나지 않도록 한다.
 export function ConfigProvider({ children, initialConfig }: {
 export function ConfigProvider({ children, initialConfig }: {
 	children: React.ReactNode,
 	children: React.ReactNode,
 	initialConfig: Config|null
 	initialConfig: Config|null
 }) {
 }) {
 	return (
 	return (
-		<ConfigContext.Provider value={initialConfig}>
+		<ConfigContext.Provider value={initialConfig ?? DEFAULT_CONFIG}>
 			{children}
 			{children}
 		</ConfigContext.Provider>
 		</ConfigContext.Provider>
 	);
 	);
 }
 }
 
 
-// Context 사용을 위한 커스텀 훅
+// Context 사용을 위한 커스텀 훅 — 값이 없어도(프로바이더 밖 사용 포함) 기본값을 반환해
+// 페이지가 예외로 다운되지 않도록 한다.
 export function useConfigContext(): Config {
 export function useConfigContext(): Config {
-	const context = useContext(ConfigContext);
-
-	if (context === null) {
-	  	throw new Error('useConfigContext는 ConfigProvider 내부에서만 사용 가능합니다.');
-	}
-
-	return context;
+	return useContext(ConfigContext) ?? DEFAULT_CONFIG;
 }
 }