// App shell: tabs + side-by-side desktop + mobile preview const Atoms = window.MajorAtoms; const S1 = window.MajorScreens; const S2 = window.MajorScreens2; const S3 = window.MajorScreens3; const Li = window.lucideReact || {}; const { useState: useApp, useEffect: useAppE } = React; const TABS = [ { k: 'landing', label: '랜딩', url: '', desktop: 'web', mobile: 'web' }, { k: 'explore', label: '탐색', url: 'explore', desktop: 'web', mobile: 'web' }, { k: 'detail', label: '상세', url: 'projects/lost-and-found', desktop: 'web', mobile: 'web' }, { k: 'compare', label: '매칭 비교', url: 'projects/.../applicants', desktop: 'web', mobile: 'web' }, { k: 'dashboard', label: '대시보드', url: 'workspace/lost-and-found', desktop: 'web', mobile: 'web' }, { k: 'portfolio', label: '포트폴리오', url: 'me/portfolio/892', desktop: 'web', mobile: 'web' }, { k: 'notif', label: '모바일 알림', url: 'notifications', desktop: 'fallback', mobile: 'web' }, { k: 'mypage', label: '마이페이지', url: 'me', desktop: 'web', mobile: 'web' }, { k: 'pricing', label: '요금제', url: 'pricing', desktop: 'web', mobile: 'web' }, ]; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "wRole": 30, "wSkill": 25, "wPeriod": 20, "wInterest": 15, "wFolio": 10, "showConfetti": true }/*EDITMODE-END*/; function App() { const [tab, setTab] = useApp('landing'); const [animKey, setAnimKey] = useApp(0); const [projectId, setProjectId] = useApp(1); const [activeApplicant, setActiveApplicant] = useApp(null); const [activeTask, setActiveTask] = useApp(null); const [showCreate, setShowCreate] = useApp(false); const [peerTeammate, setPeerTeammate] = useApp(null); const [showAwardSubmit, setShowAwardSubmit] = useApp(false); const [approval, setApproval] = useApp(null); const [, setAwardVersion] = useApp(0); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // Expose nav globally so cards/buttons can navigate useAppE(() => { window.__nav = { go(target, args = {}) { if (args.projectId) setProjectId(args.projectId); setTab(target); }, }; window.__openApplicant = (a) => setActiveApplicant(a); window.__openTask = (t) => setActiveTask(t); window.__openCreate = () => setShowCreate(true); window.__openPeerEval = (m) => setPeerTeammate(m); window.__openAwardSubmit = () => setShowAwardSubmit(true); window.__openAwardApproval = (submission, applicantName) => setApproval({ submission, applicantName }); }, []); // Push weights into BREAKDOWN_LABELS so ScoreBars + demo reflect them useAppE(() => { const max = { role: t.wRole, skill: t.wSkill, period: t.wPeriod, interest: t.wInterest, folio: t.wFolio }; Atoms.BREAKDOWN_LABELS.forEach(r => { r.max = max[r.key]; }); setAnimKey(k => k + 1); }, [t.wRole, t.wSkill, t.wPeriod, t.wInterest, t.wFolio]); useAppE(() => { window.__ML_SHOW_CONFETTI = t.showConfetti; setAnimKey(k => k + 1); }, [t.showConfetti]); useAppE(() => { setAnimKey(k => k + 1); }, [tab]); const total = t.wRole + t.wSkill + t.wPeriod + t.wInterest + t.wFolio; const renderDesktop = () => { if (tab === 'notif') return ; const map = { landing: , explore: , detail: , compare: , dashboard: , portfolio: , mypage: , pricing: , }; return map[tab]; }; const renderMobile = () => { if (tab === 'notif') return <>; const map = { landing: , explore: , detail: , compare: , dashboard: , portfolio: , mypage: , pricing: , }; const tabActive = { explore: 'explore', detail: 'explore', compare: 'mine', dashboard: 'mine', portfolio: 'me', mypage: 'me', landing: 'home' }[tab]; return <>{map[tab]}; }; const cur = TABS.find(t => t.k === tab); const desktopURL = cur.url; return (
{/* Top showcase header */}
SHOWCASE 디자인 미리보기 · 7개 화면
{TABS.map((t, i) => { const on = t.k === tab; return ( ); })}
매칭 점수 애니메이션 자동 재생
{/* Side-by-side preview */}
{/* Desktop */}
{renderDesktop()}
{/* Mobile */}
{renderMobile()}
{/* Footer caption */}
DESIGN PRINCIPLES
  • ① 매칭 점수가 모든 화면의 주인공
  • ② 다양성이 곧 가치 — 전공 컬러 코딩
  • ③ 차분한 베이스 + 한 곳의 강렬한 포인트
  • ④ 학생 톤 — 반말 살짝, 신뢰감 있게
  • ⑤ 결과물(포트폴리오)에 자부심을
SCORE WEIGHTS
{[ { l: '역할 매칭', v: 30, c: '#4F46E5' }, { l: '기술 스택', v: 25, c: '#22C55E' }, { l: '참여 기간', v: 20, c: '#F59E0B' }, { l: '관심 분야', v: 15, c: '#EC4899' }, { l: '포트폴리오', v: 10, c: '#14B8A6' }, ].map(r => (
{r.l}
{r.v}
))}
MAJORS · 5
{Object.keys(window.MajorLinkData.MAJOR).map(k => { const m = window.MajorLinkData.MAJOR[k]; return ( {k} ); })}
MajorLink · 2026.07 정식 출시 예정 · 한국 대학생 20–25세
합계 {total} / 100 {total === 100 ? '✓' : '⚠'}
setTweak('wRole', v)} /> setTweak('wSkill', v)} /> setTweak('wPeriod', v)} /> setTweak('wInterest', v)} /> setTweak('wFolio', v)} /> setTweak('showConfetti', v)} />
{activeApplicant && setActiveApplicant(null)} />} {activeTask && setActiveTask(null)} />} {showCreate && setShowCreate(false)} />} {peerTeammate && setPeerTeammate(null)} />} {showAwardSubmit && setShowAwardSubmit(false)} />} {approval && setApproval(null)} onDecision={(sub, status) => { sub.status = status; setAwardVersion(v => v + 1); }} />}
); } function DeviceBadge({ label, url, mobile }) { return (
{mobile ? : } {label}
majorlink.kr/{url}
); } ReactDOM.createRoot(document.getElementById('root')).render();