// 이 파일이 하는 일: "PWA — 웹앱을 앱처럼" 코스 — 설치되고 오프라인에서도 도는 웹이
// 무엇인지에서 출발해, 매니페스트·서비스워커·푸시 알림·iOS 제약까지 7개 섹션으로 안내하는
// 정적 학습 페이지. 프레이밍은 "웹 개발자가 보는 모바일" — 학생이 배운 React/JS 지식을
// 다리 삼아, 우리 학습 플랫폼(Vite+React 웹앱)에 가장 가까운 '앱화의 길'인 PWA를 이해시킨다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_SPECTRUM = `웹 페이지 ───────────── PWA ───────────── 네이티브 앱
(브라우저 탭)          (설치되는 웹)        (스토어에서 받는 앱)

주소창으로 접속          홈 화면 아이콘         홈 화면 아이콘
탭 닫으면 사라짐         전체화면으로 실행       전체화면으로 실행
오프라인이면 먹통        오프라인도 일부 동작     카메라·센서 등 깊이 사용
설치 0 · 즉시 배포       설치 O · 즉시 배포      설치 O · 스토어 심사

핵심: PWA는 "웹의 몸으로 앱의 껍데기를 입는" 중간 지대예요.
우리가 이미 만든 React 웹앱에 파일 두어 개(매니페스트·서비스워커)를
얹으면, 그 자리에서 '설치되는 앱'으로 승격됩니다.
새 언어(Kotlin·Swift)도, 스토어 심사도 없이.`;

const CODE_MANIFEST = `// manifest.webmanifest — 브라우저에게 "나 이런 앱이야"라고 소개하는 명함
{
  "name": "AWESOMEDEV 학습 플랫폼",
  "short_name": "미림학습",          // 홈 화면 아이콘 아래 짧은 이름
  "start_url": "/",                  // 아이콘을 누르면 열릴 주소
  "display": "standalone",           // 주소창 없이 '앱처럼' 전체화면
  "background_color": "#0f172a",     // 켤 때 잠깐 뜨는 배경
  "theme_color": "#4f46e5",          // 상태표시줄 색
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png",
      "purpose": "any maskable" }    // 안드로이드 원형/각진 틀에 맞춰 잘림
  ]
}`;

const CODE_MANIFEST_LINK = `<!-- index.html의 <head>에 명함을 걸어 두면 브라우저가 읽어요 -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#4f46e5" />

<!-- display 값 4종 (덜 앱스러움 → 더 앱스러움):
     browser < minimal-ui < standalone < fullscreen
     대부분의 PWA는 standalone을 씁니다. -->`;

const CODE_SW_REGISTER = `// 서비스워커 등록 — main.jsx 어딘가에서 딱 한 번
if ('serviceWorker' in navigator) {           // 지원하는 브라우저에서만
  window.addEventListener('load', () => {      // 첫 화면을 방해하지 않도록 로드 후
    navigator.serviceWorker.register('/sw.js')
      .then((reg) => console.log('SW 등록됨:', reg.scope))
      .catch((err) => console.error('SW 등록 실패:', err));
  });
}

// 포인트: 서비스워커는 HTTPS에서만 동작해요 (localhost는 예외로 허용).
// 중간에서 요청을 가로채는 힘이 세니, 도청 위험이 없는 곳에서만 켜집니다.`;

const CODE_SW_FETCH = `// sw.js — 브라우저와 네트워크 사이에 앉은 '중간 관리인'
const CACHE = 'mirim-v1';
const ASSETS = ['/', '/index.html', '/logo.svg', '/styles/global.css'];

// ① install: 설치될 때 핵심 파일들을 미리 캐시에 담아 둔다
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE).then((c) => c.addAll(ASSETS))
  );
});

// ② fetch: 앱이 보내는 모든 요청을 가로채서 결정한다
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request)              // 캐시에 있으면?
      .then((hit) => hit || fetch(e.request))  // → 캐시, 없으면 → 네트워크
  );
});

// 이게 오프라인의 비밀: 인터넷이 끊겨도 캐시(hit)로 화면을 그려 냅니다.
// 위는 'cache-first' 전략 — 시세처럼 늘 최신이 중요하면 'network-first'로 뒤집어요.`;

const CODE_PUSH = `// 푸시 알림 — 앱이 꺼져 있어도 서버가 사용자에게 말을 거는 통로
// ① 권한을 '물어본다'. 거절하면 끝 — 강제로 보낼 방법은 없어요.
const perm = await Notification.requestPermission();   // 'granted' | 'denied'
if (perm !== 'granted') return;

// ② 이 브라우저의 '푸시 주소'를 발급받는다 (VAPID 공개키로 서명)
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,               // 반드시 눈에 보이는 알림만 (몰래 X)
  applicationServerKey: VAPID_PUBLIC_KEY,
});

// ③ 구독 정보를 우리 Spring 서버에 저장 → 나중에 서버가 발송
await fetch('/api/push/subscribe', { method: 'POST', body: JSON.stringify(sub) });

// ④ 알림이 도착하면 sw.js가 대신 받아서 띄운다 (앱이 닫혀 있어도!)
self.addEventListener('push', (e) => {
  const data = e.data.json();
  self.registration.showNotification(data.title, { body: data.body });
});`;

const CODE_VITE_PWA = `# frontend/ 에서 — 손으로 sw.js를 짜는 대신 플러그인에 맡기기
npm install -D vite-plugin-pwa

// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',      // 새 버전 배포 시 서비스워커 자동 교체
      manifest: {
        name: 'AWESOMEDEV 학습 플랫폼',
        short_name: '미림학습',
        display: 'standalone',
        theme_color: '#4f46e5',
        icons: [ /* 192 · 512 아이콘 경로 */ ],
      },
    }),
  ],
});

// npm run build → dist/에 manifest와 서비스워커가 '자동 생성'됩니다.
// 우리 프로젝트는 이미 Vite 기반이라 여기까지가 사실상 앱화의 전부예요.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다(React 입문 섹션 2). props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: 'PWA란' },
  { n: 2, label: '우리도 PWA로' },
  { n: 3, label: '매니페스트' },
  { n: 4, label: '서비스워커' },
  { n: 5, label: '푸시 알림' },
  { n: 6, label: '네이티브 대비 한계' },
  { n: 7, label: '실습 경로' },
];

export default function PwaPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 앱 개발</div>
        <h1>PWA<br />— 웹앱을 앱처럼</h1>
        <p>
          "앱을 만들려면 안드로이드·iOS를 새로 배워야 하나요?" — 꼭 그렇진 않아요.
          <strong> 우리가 이미 만든 React 웹앱</strong>에 파일 두어 개를 얹으면, 홈 화면에
          설치되고 오프라인에서도 도는 <strong>PWA(Progressive Web App)</strong>가 됩니다.
          웹 개발자가 가장 적은 힘으로 '앱'에 닿는 길, 그 지형을 읽어 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 70분</span>
          <span className="chip">실습: 매니페스트·SW 얹기</span>
          <span className="chip">선수 지식: React·HTTP 기초</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="PWA란 무엇인가" sub="웹과 네이티브 사이, '설치되는 웹'">
        <p>
          우리가 아는 웹사이트는 <strong>브라우저 탭</strong> 안에서만 살아요. 주소창으로
          들어가고, 탭을 닫으면 사라지고, 인터넷이 끊기면 먹통이 되죠. 반대편엔{' '}
          <strong>네이티브 앱</strong>이 있어요. 스토어에서 받아 홈 화면에 깔리고, 오프라인에서도
          돌고, 카메라·센서 같은 기기 기능을 깊이 씁니다. 대신 안드로이드는 Kotlin, iOS는
          Swift로 <strong>따로</strong> 만들어야 하죠.
        </p>
        <p>
          <strong>PWA</strong>는 그 둘 사이의 중간 지대예요. 몸통은 여전히 웹(HTML·CSS·JS)인데,
          껍데기만 앱처럼 입습니다. 홈 화면에 아이콘이 깔리고, 주소창 없이 전체화면으로 열리고,
          오프라인에서도 일부 동작해요. 스토어 심사도, 새 언어도 없이요.
        </p>
        <Code>{CODE_SPECTRUM}</Code>
        <p>
          PWA를 앱답게 만드는 재료는 딱 세 가지입니다. <strong>① HTTPS</strong>(안전한 연결),{' '}
          <strong>② 매니페스트</strong>(명함 — 섹션 3), <strong>③ 서비스워커</strong>(중간
          관리인 — 섹션 4). 이 셋이 갖춰지면 브라우저가 "어, 이거 설치할 수 있는 앱이네?"
          하고 알아봅니다.
        </p>
        <div className="tip">
          <b>이름 뜻풀이</b> Progressive(점진적) Web App — "가능한 기기에선 앱처럼, 안 되는
          곳에선 그냥 웹처럼" <strong>단계적으로</strong> 좋아진다는 뜻이에요. 낡은 브라우저라도
          최소한 웹페이지로는 열립니다. 하나의 코드가 상황에 맞춰 눈높이를 바꾸는 셈이죠.
        </div>
      </Section>

      <Section n={2} title="우리 학습 플랫폼도 PWA가 될 수 있다" sub="이미 절반은 갖춰져 있다">
        <p>
          멀리 있는 얘기가 아니에요. <strong>지금 보고 있는 이 학습 플랫폼</strong>이 바로 유력한
          후보입니다. 우리는 이미 React로 만든 웹앱을 <span className="icode">Vite</span>로
          빌드하고, 운영 서버는 HTTPS로 서비스하죠. PWA의 재료 세 개 중{' '}
          <strong>HTTPS는 이미 있고</strong>, 나머지 둘(매니페스트·서비스워커)만 얹으면 됩니다.
        </p>
        <p>
          단, 정직하게 말할게요. <strong>지금 이 순간의 우리 앱은 아직 PWA가 아니에요.</strong>{' '}
          <span className="icode">frontend/index.html</span>을 열어 보면{' '}
          <span className="icode">&lt;link rel="manifest"&gt;</span>이 없고,{' '}
          <span className="icode">public/</span>엔 서비스워커 파일도 없습니다. 그래서 홈 화면에
          "설치" 버튼이 안 뜨는 거예요. 이 코스의 목표는 그 빠진 조각을 <strong>내 손으로</strong>{' '}
          채워 넣는 지형을 익히는 것입니다.
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 에디터에서{' '}
          <span className="icode">frontend/index.html</span>을 열어 <span className="icode">&lt;head&gt;</span>{' '}
          안을 보세요. 지금은 <span className="icode">&lt;title&gt;</span>과 뷰포트 설정뿐,
          manifest 링크가 없습니다. 섹션 3에서 여기에 딱 한 줄을 더하는 것부터 시작해요.
        </div>
        <div className="warn">
          <b>과장 금지</b> PWA를 얹는다고 우리 웹앱이 "네이티브 앱"이 되는 건 아니에요.
          같은 React 코드가 <strong>설치·오프라인·알림이라는 앱의 옷</strong>을 입는 것뿐입니다.
          이 구분을 흐리지 않는 게, 2개월 뒤에도 스스로 판단할 수 있는 개발자의 감각이에요.
        </div>
      </Section>

      <Section n={3} title="매니페스트 — 홈 화면 아이콘과 이름" sub="브라우저에게 건네는 앱 명함">
        <p>
          <strong>웹 앱 매니페스트</strong>는 <span className="icode">manifest.webmanifest</span>{' '}
          라는 작은 <strong>JSON 파일</strong>이에요. "내 이름은 뭐고, 아이콘은 이거고, 열리면
          이 주소부터 보여 주고, 주소창은 숨겨 줘" — 브라우저에게 건네는 <strong>앱 명함</strong>
          입니다. 사용자가 "홈 화면에 추가"를 누를 때 브라우저가 이 파일을 읽어 아이콘과
          이름을 만듭니다.
        </p>
        <Code>{CODE_MANIFEST}</Code>
        <p>
          만든 명함은 <span className="icode">index.html</span>에 걸어 둬야 브라우저가 찾아
          읽어요. 방식은 우리가 CSS 파일을 <span className="icode">&lt;link&gt;</span>로 거는 것과
          똑같습니다.
        </p>
        <Code>{CODE_MANIFEST_LINK}</Code>
        <p>
          핵심 필드는 <span className="icode">display</span>예요.{' '}
          <span className="icode">"standalone"</span>으로 두면 주소창·탭이 사라지고 전체화면으로
          열려, 겉보기엔 진짜 앱과 구분이 안 됩니다. <span className="icode">icons</span>의{' '}
          <span className="icode">"purpose": "maskable"</span>은 안드로이드가 아이콘을 원형이나
          둥근 사각형 틀에 맞춰 잘라 낼 때 여백을 확보해 주는 옵션이고요.
        </p>
        <div className="tip">
          <b>비유</b> 매니페스트는 <strong>가게 간판 신청서</strong>예요. 상호(name), 로고(icons),
          문 열면 보일 첫 화면(start_url), 간판 색(theme_color)을 적어 내면, OS가 홈 화면이라는
          거리에 우리 가게 간판을 대신 달아 줍니다. 간판만으론 장사가 안 되죠 — 실제 영업(오프라인
          동작)은 다음 섹션의 서비스워커 몫입니다.
        </div>
      </Section>

      <Section n={4} title="서비스워커 — 오프라인과 캐시의 원리" sub="브라우저와 네트워크 사이의 중간 관리인">
        <p>
          "인터넷이 끊겼는데 어떻게 화면이 뜨지?" PWA 오프라인의 비밀이 바로{' '}
          <strong>서비스워커(Service Worker)</strong>예요. 우리 앱과 네트워크{' '}
          <strong>사이에 끼어 앉은 자바스크립트</strong>인데, 앱이 보내는 모든 요청을 중간에서
          가로채(intercept) "이건 캐시에서 줄게, 이건 서버에 물어볼게"를 스스로 결정합니다.
        </p>
        <Code>{CODE_SW_REGISTER}</Code>
        <p>
          한번 등록되면 서비스워커는 <strong>페이지와 별개의 백그라운드에서</strong> 돌아요.
          그래서 탭을 닫아도, 심지어 앱이 꺼져 있어도 살아 있을 수 있죠(섹션 5의 푸시가 이걸
          씁니다). 대신 화면(DOM)은 못 만져요 — 오직 <strong>요청을 가로채고 캐시를 다루는</strong>{' '}
          문지기 역할만 합니다.
        </p>
        <svg className="diagram" viewBox="0 0 640 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="서비스워커가 앱과 네트워크 사이에 앉아 요청을 가로채는 구조 다이어그램. 앱이 데이터를 요청하면 서비스워커가 먼저 받아, 캐시에 있으면 즉시 돌려주고 없으면 서버로 요청을 넘긴다.">
          <rect x="30" y="120" width="130" height="60" rx="10" fill="var(--card)" stroke="var(--primary)" strokeWidth="2" />
          <text x="95" y="146" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">React 앱</text>
          <text x="95" y="166" textAnchor="middle" fontSize="11" fill="var(--muted)">화면 · fetch()</text>

          <rect x="255" y="110" width="130" height="80" rx="10" fill="var(--card)" stroke="var(--teal)" strokeWidth="2" />
          <text x="320" y="140" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">서비스워커</text>
          <text x="320" y="160" textAnchor="middle" fontSize="11" fill="var(--muted)">요청 가로채기</text>
          <text x="320" y="176" textAnchor="middle" fontSize="11" fill="var(--muted)">(문지기)</text>

          <rect x="480" y="40" width="130" height="56" rx="10" fill="var(--card)" stroke="var(--amber)" strokeWidth="2" />
          <text x="545" y="64" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">캐시</text>
          <text x="545" y="82" textAnchor="middle" fontSize="11" fill="var(--muted)">Cache API</text>

          <rect x="480" y="204" width="130" height="56" rx="10" fill="var(--card)" stroke="var(--line)" strokeWidth="2" />
          <text x="545" y="228" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">서버(네트워크)</text>
          <text x="545" y="246" textAnchor="middle" fontSize="11" fill="var(--muted)">Spring API</text>

          {/* 앱 → 서비스워커 */}
          <line x1="160" y1="150" x2="248" y2="150" stroke="var(--primary)" strokeWidth="2" />
          <path d="M255 150 L244 145 L244 155 Z" fill="var(--primary)" />
          <text x="204" y="140" textAnchor="middle" fontSize="11" fill="var(--muted)">요청</text>

          {/* 서비스워커 → 캐시 (있으면) */}
          <line x1="385" y1="135" x2="474" y2="80" stroke="var(--teal)" strokeWidth="2" />
          <path d="M480 76 L468 77 L473 87 Z" fill="var(--teal)" />
          <text x="430" y="98" textAnchor="middle" fontSize="11" fill="var(--teal)">① 있으면 캐시</text>

          {/* 서비스워커 → 서버 (없으면) */}
          <line x1="385" y1="165" x2="474" y2="222" stroke="var(--line)" strokeWidth="2" strokeDasharray="5 4" />
          <path d="M480 226 L468 216 L466 227 Z" fill="var(--muted)" />
          <text x="430" y="212" textAnchor="middle" fontSize="11" fill="var(--muted)">② 없으면 서버</text>

          <text x="320" y="290" textAnchor="middle" fontSize="12" fill="var(--muted)">오프라인이어도 캐시(①)만 있으면 화면이 뜬다</text>
        </svg>
        <p className="muted" style={{ fontSize: 12.5, textAlign: 'center', marginTop: 2 }}>그림: 서비스워커는 앱의 요청을 먼저 받아 — 캐시에 있으면 즉시 돌려주고, 없을 때만 서버로 넘긴다</p>
        <Code>{CODE_SW_FETCH}</Code>
        <div className="warn">
          <b>양날의 검, 캐시</b> 서비스워커가 옛 파일을 캐시에 붙들고 있으면, 새 버전을 배포해도
          사용자에겐 <strong>낡은 화면</strong>이 계속 보일 수 있어요. 그래서 캐시 이름에 버전
          (<span className="icode">mirim-v1</span> → <span className="icode">v2</span>)을 붙이고,
          새 버전이 켜질 때 옛 캐시를 지우는 처리가 꼭 필요합니다. 실무에선 이걸 자동화하려고
          섹션 7의 플러그인을 쓰죠.
        </div>
      </Section>

      <Section n={5} title="푸시 알림 개념" sub="앱이 꺼져 있어도 서버가 말을 거는 통로">
        <p>
          네이티브 앱의 상징 같은 기능, <strong>푸시 알림</strong>도 PWA가 할 수 있어요.
          원리는 이래요. 사용자가 <strong>허락</strong>하면, 브라우저가 그 기기만의 '푸시 주소'를
          발급해 줍니다. 그 주소를 우리 <strong>Spring 서버</strong>에 저장해 두면, 나중에 서버가
          그 주소로 메시지를 쏘고, 백그라운드의 서비스워커가 받아서 알림을 띄웁니다.
        </p>
        <Code>{CODE_PUSH}</Code>
        <p>
          중간 배달부는 <strong>OS의 푸시 서비스</strong>예요. 안드로이드·크롬은 FCM,
          애플은 APNs가 담당합니다. 우리 서버는 그 배달부에게 "이 주소로 이 내용 전해 줘"라고
          맡기는 거고요. 서명에 쓰는 열쇠 한 쌍을 <strong>VAPID 키</strong>라고 부르는데,
          "이 발신자는 진짜 우리 서버가 맞다"를 증명하는 신분증이라고 보면 됩니다.
        </p>
        <div className="warn">
          <b>권한은 사용자의 것</b> <span className="icode">Notification.requestPermission()</span>은{' '}
          말 그대로 <strong>물어보는</strong> 거예요. 사용자가 거절하면 알림을 보낼 방법이
          아예 없습니다. 그러니 앱 켜자마자 권한 창을 들이밀지 말고, "알림을 왜 켜면 좋은지"를
          보여 준 다음 필요한 순간에 요청하는 게 예의이자 성공률을 높이는 길이에요.
        </div>
        <div className="tip">
          <b>우리 스택과의 연결</b> 우리 백엔드는 <strong>Spring Boot</strong>예요. 구독 정보를
          받아 DB에 저장하고, 조건이 되면(예: 새 과제 등록) 저장해 둔 주소로 푸시를 쏘는
          <strong> 서버 API</strong>가 필요하죠. 프론트의 <span className="icode">/api/push/...</span>{' '}
          호출과 서버의 발송 로직이 짝을 이룹니다 — 웹에서 배운 "요청↔응답"이 알림에서도
          그대로예요.
        </div>
      </Section>

      <Section n={6} title="네이티브 대비 PWA의 한계" sub="특히 iOS — 어디까지 가고 어디서 막히나">
        <p>
          여기가 이 코스에서 가장 정직해야 할 대목이에요. PWA는 강력하지만{' '}
          <strong>네이티브를 완전히 대체하진 못합니다.</strong> 특히 <strong>iOS(아이폰)</strong>의
          제약이 커요. 왜냐하면 iOS에선 크롬·파이어폭스조차 속으로는 애플의 웹 엔진(WebKit)을
          쓰기 때문에, <strong>애플이 열어 준 만큼만</strong> 됩니다.
        </p>
        <p>대표적인 벽들:</p>
        <p style={{ marginTop: -6 }}>
          <strong>· 설치 유도가 어렵다</strong> — 안드로이드/크롬은 "설치하시겠어요?" 배너를
          띄울 수 있지만, iOS엔 그 자동 배너가 없어요. 사용자가 <strong>공유 → 홈 화면에
          추가</strong>를 손수 눌러야 합니다.
        </p>
        <p style={{ marginTop: -6 }}>
          <strong>· 푸시 알림의 조건</strong> — iOS는 <strong>16.4(2023년)부터</strong> 웹 푸시를
          지원하지만, 반드시 <strong>홈 화면에 설치된 PWA</strong>에서만 작동해요. 사파리 탭으로
          열어 둔 상태에선 안 됩니다.
        </p>
        <p style={{ marginTop: -6 }}>
          <strong>· 저장 공간·기능 제약</strong> — iOS는 캐시·저장소를 오래 안 쓰면 지워 버릴
          수 있고, 블루투스·NFC·정밀한 백그라운드 실행 같은 기능은 웹에 잘 열어 주지 않아요.
          안드로이드는 이보다 훨씬 관대합니다(크롬은 아예 WebAPK로 앱처럼 포장해 줘요).
        </p>
        <Code>{`기능              PWA(크롬/안드로이드)   PWA(iOS/사파리)   네이티브
────────────────────────────────────────────────────────────
홈 화면 설치         O (설치 배너 O)       O (수동 추가만)     O
오프라인 캐시        O                     O (지워질 수 있음)  O
푸시 알림            O                     △ 설치 시 16.4+     O
카메라·마이크        O                     O                  O
블루투스·NFC         일부                  X                  O
앱스토어 노출        X (URL로 공유)        X                  O`}</Code>
        <div className="tip">
          <b>그래서 언제 PWA인가</b> "설치·오프라인·알림 정도면 충분하고, 한 코드로 웹·안드로이드·
          iOS를 <strong>빠르게</strong> 덮고 싶다" → PWA가 최고의 가성비예요. 반대로 "블루투스
          기기 제어, 앱스토어 노출, 무거운 3D·센서가 핵심" → 그땐 네이티브(또는 React Native
          같은 크로스플랫폼)를 봐야 합니다. <strong>정답은 없고, 요구사항이 정합니다.</strong>
        </div>
      </Section>

      <Section n={7} title="우리 웹앱에 PWA를 얹는 실습 경로" sub="Vite 프로젝트라서 생각보다 짧다">
        <p>
          이제 지형을 알았으니 실제 길을 걸어 봅시다. 다행히 우리 프론트엔드는 이미{' '}
          <span className="icode">Vite</span> 기반이라, 서비스워커를 손으로 한 줄씩 짜는 대신{' '}
          <strong><span className="icode">vite-plugin-pwa</span></strong>에게 매니페스트와
          서비스워커 생성을 맡길 수 있어요. 캐시 버전 관리 같은 골치 아픈 부분까지 자동으로요.
        </p>
        <Code>{CODE_VITE_PWA}</Code>
        <p>
          순서를 정리하면 이렇습니다. <strong>① 아이콘</strong>(192·512px PNG)을{' '}
          <span className="icode">public/icons/</span>에 넣고 → <strong>② 위 설정</strong>을{' '}
          <span className="icode">vite.config.js</span>에 추가하고 → <strong>③{' '}
          <span className="icode">npm run build</span></strong> 후{' '}
          <span className="icode">npm run preview</span>로 확인 → <strong>④</strong> 개발자 도구로
          검증. 오프라인 테스트는 서버를 끄지 말고, 개발자 도구의 네트워크 탭에서 "오프라인"으로
          바꿔 보면 됩니다.
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 크롬에서 <span className="kbd">F12</span> →{' '}
          <span className="kbd">Application</span> 탭을 여세요. 왼쪽에{' '}
          <span className="icode">Manifest</span>와 <span className="icode">Service Workers</span>{' '}
          항목이 있어요. 지금 우리 앱을 열면 둘 다 비어 있을 겁니다(섹션 2에서 확인했듯 아직
          PWA가 아니니까요). 플러그인을 붙이고 빌드한 뒤 다시 보면, 여기에 우리 앱의 명함과
          문지기가 등록된 걸 <strong>눈으로</strong> 볼 수 있어요.
        </div>
        <div className="warn">
          <b>개발 서버에선 안 보일 수 있어요</b> 서비스워커는 보통 <span className="icode">npm run
          dev</span>가 아니라 <strong>빌드된 결과</strong>(<span className="icode">build</span> →{' '}
          <span className="icode">preview</span>)에서 온전히 동작해요. "설치 버튼이 안 떠요"의
          절반은 여기서 옵니다 — <span className="icode">dev</span>가 아니라{' '}
          <span className="icode">preview</span>로 열었는지, 그리고 HTTPS(또는 localhost)인지
          먼저 확인하세요.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>📱 여기까지 왔다면</h3>
          <p className="muted">
            이제 "앱을 만들려면 새 언어를 배워야 한다"는 막연함 대신, <strong>PWA라는 가장 가까운
            길</strong>의 지형을 읽을 수 있게 됐어요. 매니페스트(명함), 서비스워커(문지기), 푸시
            (알림 통로), 그리고 iOS의 벽까지 — 우리 React 웹앱을 앱처럼 만드는 재료와 한계를
            함께 손에 쥐었습니다. 그런데 앱이 되어 <strong>설치되고 알림을 받는 순간</strong>,
            챙길 게 하나 더 생겨요. 토큰은 어디에 저장하고, 오프라인 캐시엔 뭘 남기면 안 되고,
            푸시 권한은 어떻게 다뤄야 안전할까요? 다음{' '}
            <Link to="/learn/app-security"><strong>앱 보안 기초</strong></Link> 코스에서, 앱이라는
            새 껍데기에 맞는 보안 감각을 이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
