/* ============================================================ Login screen + App shell (sidebar / topbar / project tabs) ============================================================ */ const { useState: useS1, useEffect: useE1 } = React; /* ---------------- LOGIN ---------------- */ const SSO_DOMAINS = ['lge.com', 'concentrix.com', '직접입력']; /* role display helpers (ADMIN > CNX > LGE) */ const roleColor = (r) => r === 'ADMIN' ? 'var(--ink)' : r === 'CNX' ? 'var(--cnx-blue)' : 'var(--lg-red)'; const orgOf = (r) => r === 'ADMIN' ? '운영 관리자' : r === 'CNX' ? 'Concentrix' : 'LG Electronics'; const roleDesc = (r) => r === 'ADMIN' ? '시스템 관리' : r === 'CNX' ? '콘텐츠 관리' : '조회·선택·제출'; /* Shared SSO email field: local-part + domain select, or direct full-email entry. Parent owns the state and derives the final email from these three pieces. */ function SsoEmailField({ localPart, setLocalPart, domain, setDomain, customEmail, setCustomEmail, autoComplete }) { const isDirect = domain === '직접입력'; return (
{isDirect ? setCustomEmail(e.target.value)} placeholder="name@company.com" autoComplete={autoComplete} /> :
setLocalPart(e.target.value)} placeholder="이름" autoComplete={autoComplete} /> @
} {!isDirect ? : }
); } function ssoEmail(localPart, domain, customEmail) { return domain === '직접입력' ? customEmail : (localPart + '@' + domain); } function SignUp({ onSignUp, onBack }) { const [localPart, setLocalPart] = useS1(''); const [domain, setDomain] = useS1('lge.com'); const [customEmail, setCustomEmail] = useS1(''); const [name, setName] = useS1(''); const [pw, setPw] = useS1(''); const [pwConfirm, setPwConfirm] = useS1(''); const [inviteCode, setInviteCode] = useS1(''); const [err, setErr] = useS1(''); const [busy, setBusy] = useS1(false); const email = ssoEmail(localPart, domain, customEmail); const submit = async (e) => { e.preventDefault(); if (pw !== pwConfirm) { setErr('비밀번호가 일치하지 않습니다.'); return; } setBusy(true); setErr(''); try { const r = await window.api.register({ email, name, password: pw, inviteCode: inviteCode.trim() }); onSignUp(r); } catch (ex) { setErr(ex.message); setBusy(false); } }; return (
LG AI 365

회원가입

계정 정보를 입력하세요.

setName(e.target.value)} placeholder="홍길동" autoComplete="name" />
setPw(e.target.value)} placeholder="••••••" autoComplete="new-password" />
setPwConfirm(e.target.value)} placeholder="••••••" autoComplete="new-password" />
setInviteCode(e.target.value)} placeholder="LG-XXXXXX" autoCapitalize="characters" />
{err &&
{err}
} : null}> {busy ? '가입 중…' : '가입하기'} 로그인으로 돌아가기
); } function Login({ onLogin }) { const [localPart, setLocalPart] = useS1(''); const [domain, setDomain] = useS1('lge.com'); const [customEmail, setCustomEmail] = useS1(''); const [pw, setPw] = useS1(''); const [err, setErr] = useS1(''); const [busy, setBusy] = useS1(false); const [showSignUp, setShowSignUp] = useS1(false); const email = ssoEmail(localPart, domain, customEmail); const submit = async (e) => { e.preventDefault(); setBusy(true);setErr(''); try {const r = await window.api.login({ email, password: pw });onLogin(r);} catch (ex) {setErr(ex.message);setBusy(false);} }; if (showSignUp) return setShowSignUp(false)} />; return (
LG AI 365

로그인

계정 정보를 입력하세요.

setPw(e.target.value)} placeholder="••••••" autoComplete="current-password" />
{err &&
{err}
} : null}> {busy ? '로그인 중…' : '로그인'} setShowSignUp(true)}> 회원가입
); } /* ---------------- SHELL ---------------- */ function AppShell({ user, route, navigate, onLogout, project, children }) { const [mobileOpen, setMobileOpen] = useS1(false); const isAdmin = user.role === 'ADMIN'; const isStaff = user.role === 'ADMIN' || user.role === 'CNX'; // content + audit access useE1(() => {setMobileOpen(false);}, [route]); // 사이드바 항목과 상단 제목의 단일 출처 — 둘이 따로 놀며 어긋나지 않게 한 테이블에서 파생. // 운영/에피소드는 같은 라우트(home·project)를 tab으로 가르므로 탭 항목으로 별도 취급. const onProject = route.name === 'home' || route.name === 'project'; const isEpisodes = onProject && route.tab === 'episodes'; const tabPages = [ { ttl: '운영 일정', icon: , active: onProject && !isEpisodes, nav: { name: 'home', tab: 'ops' } }, { ttl: '에피소드 선택', icon: , active: isEpisodes, nav: { name: 'project', tab: 'episodes' } }, ]; const navPages = [ { name: 'eplog', ttl: '에피소드 로그', icon: , show: isStaff }, { name: 'epadmin', ttl: '에피소드 관리', icon: , show: isStaff }, { name: 'audit', ttl: '감사 로그', icon: , show: isStaff }, { name: 'users', ttl: '사용자 관리', icon: , show: isAdmin }, ]; const page = navPages.find((p) => p.name === route.name) || (isEpisodes ? tabPages[1] : tabPages[0]); return (
{mobileOpen &&
setMobileOpen(false)} />}
{page.icon} {page.ttl}
{roleDesc(user.role)} } onClick={onLogout}>로그아웃
{children}
); } Object.assign(window, { Login, AppShell });