import { useCallback, useEffect, useState } from 'react' import { ApiError, getOrgs, logout, type Org } from './api/client' import ContactsPage from './components/ContactsPage' import Dashboard from './components/Dashboard' import LoginPage from './components/LoginPage' import OrgDetail from './components/OrgDetail' import OrgList from './components/OrgList' import OrgOverview from './components/OrgOverview' import ProjectsPage from './components/ProjectsPage' import RightsDashboard from './components/RightsDashboard' import SeedUpload from './components/SeedUpload' import SharedFiles from './components/SharedFiles' import Sidebar from './components/Sidebar' import SystemPage from './components/SystemPage' import { CodeProvider } from './codes/CodeProvider' import { useCollapsiblePanel } from './hooks/useCollapsiblePanel' // 화면 4개: Dashboard(전체 기관 현황 - 좌측 기관 목록 없이 전폭으로 쓴다), // 기관관리(시안 레이아웃의 기관 상세), 채널관리(명부 시드 + 담당자 배정 + 채널 생성), // 담당자관리(담당자 3종 CRUD - 담당자 정보가 입력되는 유일한 화면). type Page = 'dashboard' | 'orgs' | 'channels' | 'contacts' | 'projects' | 'rights-check' | 'rights-process' | 'files' | 'system' const PAGE_TITLE: Record = { dashboard: 'Dashboard', orgs: '기관관리', channels: '채널관리', contacts: '담당자관리', projects: '사업관리', 'rights-check': '권리확인', 'rights-process': '권리처리', files: '자료실', system: '시스템관리', } // 'checking'이 필요하다. true로 시작하면 첫 렌더에서 getOrgs()가 아직 안 끝났는데도 // 로그인한 것처럼 빈 껍데기를 먼저 보여주고, 401이 온 뒤에야 로그인 화면으로 튄다. // // 'error'는 401이 아닌 실패(서버 500, 네트워크 단절 등)를 위한 상태다. 이런 오류까지 // '로그아웃'으로 취급해 로그인 화면으로 보내면, 로그인해도 같은 오류가 반복되고 // 원인을 알 수 없게 된다 - 세션은 멀쩡한데 서버/네트워크만 문제인 경우이므로 // 오류 메시지와 재시도 버튼을 보여주는 편이 맞다. type AuthState = 'checking' | 'authenticated' | 'anonymous' | 'error' export default function App() { const [orgs, setOrgs] = useState([]) const [selectedId, setSelectedId] = useState(null) const [auth, setAuth] = useState('checking') const [authErrorMessage, setAuthErrorMessage] = useState(null) const [page, setPage] = useState('dashboard') const [orgInitialTab, setOrgInitialTab] = useState<'권리확인' | '권리처리' | undefined>() // 창이 좁아지면 자동으로 접힌다. 사용자의 접기/펼치기 선택은 저장된다. const [menuCollapsed, toggleMenu] = useCollapsiblePanel('itnhub.menu', '(max-width: 1023px)') const [listCollapsed, toggleList] = useCollapsiblePanel('itnhub.orglist', '(max-width: 1279px)') const reload = useCallback(async () => { try { setOrgs(await getOrgs()) setAuth('authenticated') } catch (e) { if (e instanceof ApiError && e.status === 401) { setAuth('anonymous') } else { setAuthErrorMessage((e as Error).message) setAuth('error') } } }, []) useEffect(() => { void reload() }, [reload]) if (auth === 'checking') { return
불러오는 중…
} if (auth === 'anonymous') { return void reload()} /> } if (auth === 'error') { return (

{authErrorMessage}

) } const selected = orgs.find((org) => org.id === selectedId) ?? null async function handleLogout() { try { await logout() } finally { // 서버 응답과 무관하게 화면은 로그인으로 돌린다. 세션이 이미 만료된 경우에도 // 사용자를 붙잡아 둘 이유가 없다. setOrgs([]) setSelectedId(null) setAuth('anonymous') } } // 화면 선택지(진행단계·처리결과·공공누리유형 등)는 서버에서 한 번 받아 앱 전체가 나눠 쓴다. // 로그인 화면에는 필요 없으므로 인증을 통과한 다음부터 감싼다. return (
{ if ( key === 'dashboard' || key === 'orgs' || key === 'channels' || key === 'contacts' || key === 'projects' || key === 'rights-check' || key === 'rights-process' || key === 'files' || key === 'system' ) { if (key === 'orgs') setOrgInitialTab(undefined) setPage(key) } }} />
{PAGE_TITLE[page]}
관리자
{page !== 'contacts' && page !== 'dashboard' && page !== 'system' && page !== 'projects' && page !== 'rights-check' && page !== 'rights-process' && page !== 'files' && (
{page === 'channels' && !listCollapsed && ( void reload()} /> )}
)}
{page === 'dashboard' ? ( { setSelectedId(orgId) setOrgInitialTab(undefined) setPage('orgs') }} // 요청자 요구: 대시보드 하단에서 전체를 볼 때는 사업관리로 넘어간다 onOpenOrgList={() => setPage('projects')} /> ) : page === 'projects' ? ( { setSelectedId(orgId) setOrgInitialTab(undefined) setPage('orgs') }} /> ) : page === 'rights-check' || page === 'rights-process' ? ( { setSelectedId(orgId) setOrgInitialTab(page === 'rights-check' ? '권리확인' : '권리처리') setPage('orgs') }} /> ) : page === 'files' ? ( ) : page === 'system' ? ( void reload()} /> ) : page === 'contacts' ? ( ) : selected ? ( page === 'channels' ? ( void reload()} /> ) : ( void reload()} initialTab={orgInitialTab} /> ) ) : (

왼쪽에서 기관을 선택하세요.

)}
) }