// 이 파일이 하는 일: "에러 처리 패턴: 무너지지 않는 서비스" 코스 — 에러를 '없애는' 게 아니라
// '다스리는' 기술을 백엔드(Express 에러 미들웨어·커스텀 에러 클래스·응답 포맷)부터
// 프론트(에러 바운더리·다시 시도 폴백 UI)·로깅까지 7개 섹션으로 안내하는 정적 학습 페이지.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_TWO_KINDS = `① 예상된 실패 (operational error) — 정상 운영 중에도 '일어나는' 일
   · 없는 글 번호로 조회       → 404
   · 제목 없이 글 등록 시도     → 400
   → 코드는 멀쩡하다. 사용자에게 "무엇이 잘못됐는지" 알려 주면 된다.

② 버그 (programmer error) — 코드가 잘못돼서 '일어나면 안 되는' 일
   · undefined에서 .title을 읽음 (TypeError)
   → 사용자가 할 수 있는 게 없다. 로그에 남기고 500.

핵심: ①은 '사용자에게 설명'이 답, ②는 '개발자에게 보고'가 답.`;

const CODE_SWALLOW = `// 최악의 코드: 에러를 '삼키는' catch
try {
  await savePost(post);
} catch (e) {
  // ...아무것도 안 함. 저장이 실패했는데 성공한 척 흘러간다
}
showToast('작성 완료!');   // 화면엔 완료가 뜨는데 DB엔 글이 없다

// 에러가 사라진 게 아니라 에러를 '추적할 단서'가 사라진 것.
// 처리 못 하겠으면 최소한: catch (e) { console.error(e); throw e; }`;

const CODE_ASYNC_LEAK = `// 함정: await가 없으면 try/catch는 장식이 된다
async function loadPosts() {
  try {
    fetchPosts();          // ← await 없음! Promise가 그냥 떠내려간다
  } catch (e) {
    // fetchPosts가 실패해도 여기는 '절대' 실행되지 않는다
  }
}
// await가 없으면 에러는 try 블록이 이미 끝난 뒤 허공에서 터진다.
// 콘솔의 "Unhandled promise rejection"이 바로 이 장면.`;

const CODE_ASYNC_RIGHT = `// 올바른 형태 — await로 에러를 '이 자리로' 끌어온다
async function loadPosts() {
  try {
    const posts = await fetchPosts();   // 실패하면 여기서 throw
    return posts;
  } catch (e) {
    if (e instanceof NetworkError) {
      return [];         // ① 처리할 수 있는 에러 → 여기서 처리
    }
    throw e;             // ② 모르는 에러 → 다시 던진다 (삼키지 말 것!)
  } finally {
    hideSpinner();       // ③ 성공하든 실패하든 '반드시' 실행 — 정리 작업
  }
}`;

const CODE_CUSTOM_ERRORS = `// errors.js — '의미 있는' 에러 클래스 만들기
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = this.constructor.name;  // 'NotFoundError' 같은 클래스명
    this.statusCode = statusCode;       // HTTP 상태를 에러에 실어 나른다
    this.isExpected = true;             // "예상된 실패" 표식
  }
}

class NotFoundError extends AppError {
  constructor(message) {
    super(message || '리소스를 찾을 수 없습니다', 404);
  }
}

// ValidationError(400), UnauthorizedError(401)도 같은 방식으로 추가`;

const CODE_CUSTOM_USE = `// 던지는 쪽 — 라우트 코드가 '무슨 일인지' 말하게 된다
const post = await Post.findById(id);
if (!post) throw new NotFoundError('글 ' + id + '번을 찾을 수 없습니다');
if (!req.body.title) throw new ValidationError('제목은 필수입니다');

// 받는 쪽 — instanceof로 에러의 '종류'를 분기한다
if (err instanceof ValidationError) {
  // 400: 사용자가 고칠 수 있는 문제
} else if (err instanceof AppError) {
  // err.statusCode: 에러가 자기 상태코드를 스스로 들고 왔다
} else {
  // 그 외 전부 = 예상 밖의 버그 → 500
}`;

const CODE_MIDDLEWARE = `// app.js — Express 에러 미들웨어: 인자가 '4개'면 에러 담당이 된다
app.use((err, req, res, next) => {
  // 어느 라우트에서든 next(err)로 넘긴 에러가 전부 여기로 모인다
  const status = err.isExpected ? err.statusCode : 500;
  res.status(status).json({
    error: { code: err.name, message: err.message },
  });
});
// 위치가 중요: 모든 라우트 등록 '뒤에' 둬야 마지막 그물이 된다.
// 동기 라우트는 throw만 해도 Express가 알아서 여기로 배달해 준다.`;

const CODE_ASYNC_WRAP = `// 함정: async 라우트의 에러는 Express 4가 '못' 받는다
app.get('/api/posts', async (req, res) => {
  const posts = await Post.findAll();  // 여기서 터지면? 미들웨어에 안 닿고
  res.json(posts);                     // 요청이 영원히 매달린다
});

// 해결: catch로 붙잡아 next(err)로 '손수 배달'하는 래퍼
const wrap = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/api/posts', wrap(async (req, res) => {
  const posts = await Post.findAll();  // 실패하면 wrap이 next(err) 호출
  res.json(posts);
}));
// (참고: Express 5부터는 async 에러도 자동으로 전달된다)`;

const CODE_FORMAT = `실패 응답은 '항상' 이 모양 — 전 API 공통
{
  "error": {
    "code": "POST_NOT_FOUND",
    "message": "글을 찾을 수 없습니다"
  }
}

- code:    기계가 읽는 이름표. 프론트가 분기할 때 쓴다 (불변)
- message: 사람이 읽는 설명. 화면에 그대로 보여 줄 수 있는 문장`;

const CODE_FORMAT_MIDDLEWARE = `// 에러 미들웨어 완성형 — 예상된 실패와 버그를 다르게 내보낸다
app.use((err, req, res, next) => {
  if (err.isExpected) {
    // ① 예상된 실패: 준비된 상태코드·메시지를 그대로 전달
    return res.status(err.statusCode).json({
      error: { code: err.name, message: err.message },
    });
  }
  // ② 예상 밖 에러(버그): 세부는 감추고 범용 메시지만
  console.error(err);            // 스택은 '로그'에만 남긴다
  res.status(500).json({
    error: { code: 'INTERNAL_ERROR', message: '일시적인 오류가 발생했습니다' },
  });
});

// 절대 금지: res.status(500).json({ message: err.stack })
// 스택엔 파일 경로·폴더 구조·라이브러리 버전이 담긴다 — 공격자용 내부 지도`;

const CODE_BOUNDARY = `// ErrorBoundary.jsx — '렌더링 중' 터진 에러를 받아 내는 안전망
import { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };       // 자식이 터지면 폴백 모드로 전환
  }

  componentDidCatch(error, info) {
    console.error('렌더링 에러:', error, info);  // 로그 전송 자리
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;    // 무너진 자리에 대신 세울 화면
    }
    return this.props.children;      // 평소엔 자식을 그대로 렌더
  }
}

// 쓰는 곳: <ErrorBoundary fallback={...}> <PostList /> </ErrorBoundary>
// → PostList가 터져도 헤더·사이드바·다른 영역은 멀쩡히 산다`;

const CODE_RETRY = `// PostList.jsx — '다시 시도' 버튼이 있는 폴백 (요청 실패용)
function PostList() {
  const [state, setState] = useState({ status: 'loading', posts: [] });

  const load = () => {
    setState({ status: 'loading', posts: [] });
    fetch('/api/posts')
      .then((res) => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then((posts) => setState({ status: 'ok', posts }))
      .catch(() => setState({ status: 'error', posts: [] }));
  };

  useEffect(load, []);

  if (state.status === 'error') {
    return (
      <div>
        <p>글 목록을 불러오지 못했어요.</p>
        <button onClick={load}>다시 시도</button>
      </div>
    );
  }
  // ...loading이면 스피너, ok면 목록 렌더
}`;

const CODE_LOGGING = `// ① 레벨 있는 로깅 — 심각도에 따라 다른 서랍에 넣는다
logger.info('서버 시작: 포트 3000');          // 정상 흐름의 기록
logger.warn('로그인 5회 연속 실패: user42');   // 이상 징후, 아직 장애는 아님
logger.error('DB 연결 끊김', err);            // 지금 살펴야 할 문제

// console.log만 쓰면 전부 같은 줄로 쏟아져서, 로그 만 줄 속에서
// "지금 무엇이 심각한가"를 걸러 낼 방법이 없다.
// 레벨이 있으면 "error만 보여 줘" 한 번으로 소음이 걷힌다.`;

const CODE_REQUEST_ID = `// ② 요청 ID — 에러와 요청을 실 하나로 잇기
app.use((req, res, next) => {
  req.id = crypto.randomUUID();     // 요청마다 고유한 번호표를 발급
  next();
});

// 에러 미들웨어에서: 로그에는 번호표 + 스택 전부
console.error('[' + req.id + ']', err);

// 응답에는 번호표만 — 스택은 절대 안 나간다
res.status(500).json({
  error: {
    code: 'INTERNAL_ERROR',
    message: '일시적인 오류가 발생했습니다',
    requestId: req.id,
  },
});

// 사용자가 "requestId a1b2c3에서 오류"라고 문의하면,
// 개발자는 로그에서 a1b2c3 검색 → 그 요청 하나의 전 과정을 추적한다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '에러의 두 종류' },
  { n: 2, label: 'async와 try/catch' },
  { n: 3, label: '커스텀 에러 클래스' },
  { n: 4, label: '에러 미들웨어' },
  { n: 5, label: '응답 포맷 통일' },
  { n: 6, label: '폴백 UI' },
  { n: 7, label: '로깅 기초' },
];

export default function Lv2ErrorHandlingPatternsPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>🚨 에러 처리 패턴<br />— 무너지지 않는 서비스</h1>
        <p>
          에러는 <strong>없앨 수 없어요. 다스릴 뿐입니다.</strong> 네트워크는 끊기고, 사용자는
          예상 밖의 값을 넣고, DB는 가끔 응답이 늦죠. 좋은 서비스의 기준은 "에러가 나느냐"가
          아니라 <strong>에러가 났을 때의 표정</strong>이에요. 백엔드의 에러 미들웨어부터
          프론트의 '다시 시도' 폴백 UI까지, 실패의 순간에도 품위를 지키는 설계를 한 바퀴
          돌아봅니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 65분</span>
          <span className="chip">실습: 블로그 앱 에러 응답 통일 + 폴백 UI</span>
          <span className="chip">선수 지식: JS 기초·Express·React 기본</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="에러의 두 종류" sub="예상된 실패 vs 버그 — 같은 throw, 다른 처방">
        <p>
          모든 에러 처리는 이 구분에서 출발해요. <strong>예상된 실패</strong>는 코드가 멀쩡해도
          일어나는 일이에요. 없는 글 번호로 조회하거나 제목 없이 글을 올리려 하는 것 — 서비스가
          살아 있는 한 <strong>매일 일어나는 정상적인 사건</strong>이죠. 반면{' '}
          <strong>버그</strong>는 코드가 잘못돼서 일어나면 안 되는 게 일어난 거예요.{' '}
          <span className="icode">undefined.title</span>을 읽는 순간 같은 거요.
        </p>
        <Code>{CODE_TWO_KINDS}</Code>
        <p>
          왜 나눠야 할까요? <strong>처방이 정반대</strong>이기 때문이에요. 예상된 실패는
          사용자에게 "제목을 입력해 주세요"라고 <strong>설명</strong>하면 끝나지만, 버그는
          사용자가 할 수 있는 게 없어요 — 로그에 남겨 <strong>개발자에게 보고</strong>해야 하죠.
          그리고 두 종류 모두에게 통하는 단 하나의 금기가 있습니다. 에러를{' '}
          <strong>삼키는</strong> 것이에요.
        </p>
        <Code>{CODE_SWALLOW}</Code>
        <div className="warn">
          <b>왜 '삼키기'가 최악인가</b> 에러가 나는 것보다 나쁜 건, 에러가 났는데{' '}
          <strong>아무도 모르는 것</strong>이에요. 빈 catch는 에러를 없애는 게 아니라 에러의{' '}
          <strong>증거를 인멸</strong>합니다. 사용자는 "글이 증발했다"며 떠나고, 개발자는 재현할
          단서조차 없죠. 처리할 수 없으면 최소한 로그를 남기고 다시 던지세요.
        </div>
      </Section>

      <Section n={2} title="async 함수에서 try/catch 제대로" sub="await 없는 Promise 에러가 새는 경로">
        <p>
          자바스크립트 에러 처리에서 가장 흔한 사고 지점이 여기예요.{' '}
          <span className="icode">try/catch</span>로 감쌌는데도 에러가 잡히지 않고 콘솔에{' '}
          <span className="icode">Unhandled promise rejection</span>이 뜬다면, 십중팔구{' '}
          <strong><span className="icode">await</span>를 빼먹은 것</strong>입니다.
        </p>
        <Code>{CODE_ASYNC_LEAK}</Code>
        <p>
          <span className="icode">try/catch</span>는 <strong>지금 이 자리에서 터진 에러만</strong>{' '}
          잡아요. <span className="icode">await</span>가 없으면 함수는 Promise의 결과를 기다리지
          않고 지나가 버리고, 실패는 <span className="icode">try</span> 블록이 이미 끝난 뒤 —
          catch의 손이 닿지 않는 곳에서 — 터집니다. <span className="icode">await</span>는
          "결과(성공이든 에러든)를 <strong>이 자리로 끌어와라</strong>"는 뜻이에요.
        </p>
        <Code>{CODE_ASYNC_RIGHT}</Code>
        <p>
          <span className="icode">catch</span> 안에서의 판단 기준은 딱 하나입니다.{' '}
          <strong>"여기서 이 에러를 해결할 수 있는가?"</strong> 해결할 수 있으면(빈 목록으로
          대체, 재시도) 처리하고, 없으면 <strong>다시 던져서</strong> 해결할 수 있는 곳까지 올려
          보내세요. 스피너 끄기, 연결 닫기 같은 <strong>정리 작업</strong>은{' '}
          <span className="icode">finally</span>에 두면 성공·실패 어느 쪽이든 반드시 실행됩니다.
        </p>
      </Section>

      <Section n={3} title="커스텀 에러 클래스" sub="에러에 이름표와 상태코드를 실어 나르기">
        <p>
          <span className="icode">throw new Error('없음')</span> 같은 맨몸 에러의 문제는, 받는
          쪽이 <strong>이게 어떤 종류의 실패인지 알 길이 없다</strong>는 거예요. 404를 줘야 할지
          400을 줘야 할지, message 문자열을 사람이 읽고 추측하는 수밖에 없죠. 그래서 에러에{' '}
          <strong>이름표(클래스)와 상태코드</strong>를 실어 나르는 설계가 나옵니다.
        </p>
        <Code>{CODE_CUSTOM_ERRORS}</Code>
        <p>
          핵심은 세 가지예요. <strong>① 클래스명이 곧 에러의 종류</strong>가 되고,{' '}
          <strong>② <span className="icode">statusCode</span></strong>를 에러 객체에 실어 두면
          "이 에러엔 몇 번 응답"이라는 지식이 <strong>던지는 쪽에</strong> 한 번만 적히며,{' '}
          <strong>③ <span className="icode">isExpected</span></strong> 표식으로 "예상된
          실패"(섹션 1)임을 알립니다 — 이 표식이 없는 에러는 전부 버그로 취급하면 돼요.
        </p>
        <Code>{CODE_CUSTOM_USE}</Code>
        <div className="tip">
          <b>instanceof의 힘</b> <span className="icode">e instanceof ValidationError</span>는
          문자열 비교가 아니라 <strong>타입 검사</strong>예요. 상속 관계도 이해해서{' '}
          <span className="icode">NotFoundError</span> 인스턴스는{' '}
          <span className="icode">instanceof AppError</span>에도 <span className="icode">true</span>
          입니다. 그래서 "구체적인 종류 먼저, 넓은 부모는 나중에" 순서로 분기하는 게 정석이에요.
        </div>
      </Section>

      <Section n={4} title="백엔드: 에러 미들웨어 일원화" sub="흩어진 try/catch를 한 곳으로 모으는 그물">
        <p>
          라우트가 10개면 try/catch도 10벌 — 그렇게 짜면 응답 모양이 조금씩 달라지고, 하나쯤은
          꼭 빠뜨리게 돼요. Express의 답은 <strong>에러 미들웨어</strong>입니다. 인자를{' '}
          <strong>4개</strong>(<span className="icode">err, req, res, next</span>) 받는 미들웨어를
          등록하면, Express는 그걸 "에러 담당"으로 인식하고 <strong>어디서 터진 에러든 전부
          그리로 배달</strong>해 줘요.
        </p>
        <Code>{CODE_MIDDLEWARE}</Code>
        <p>
          그런데 여기 유명한 함정이 하나 있어요. <strong>동기</strong> 라우트에서 던진 에러는
          Express가 알아서 받아 주지만, <strong>async 라우트</strong> 안에서 터진 에러는 Express
          4가 받지 못합니다(Promise 거절을 감지하지 못해요). 그래서 async 핸들러를 감싸{' '}
          <span className="icode">next(err)</span>로 배달해 주는 <strong>래퍼 패턴</strong>이
          실무의 표준이 됐죠.
        </p>
        <Code>{CODE_ASYNC_WRAP}</Code>
        <div className="tip">
          <b>구조가 주는 것</b> 이 패턴이 자리 잡으면 라우트의 규칙이 단순해져요 —{' '}
          <strong>"문제가 생기면 그냥 던져라(throw)."</strong> 어떻게 응답할지는 미들웨어 한
          곳의 책임이라, 응답 정책을 바꿀 때 고칠 파일이 하나뿐입니다.
        </div>
      </Section>

      <Section n={5} title="일관된 에러 응답 포맷" sub="전 API가 같은 모양으로 실패하게 만들기">
        <p>
          에러가 미들웨어 한 곳으로 모였으니, 이제 <strong>나가는 모양</strong>을 통일할
          차례예요. API마다 실패 응답이 제각각이면(어떤 건 문자열, 어떤 건{' '}
          <span className="icode">msg</span>, 어떤 건 <span className="icode">error</span>...)
          프론트는 API 개수만큼 에러 처리 코드를 짜야 합니다. 규약은 단순할수록 좋아요:{' '}
          <strong><span className="icode">code</span> + <span className="icode">message</span></strong>.
        </p>
        <Code>{CODE_FORMAT}</Code>
        <p>
          <span className="icode">code</span>는 <strong>기계가 읽는</strong> 이름표라 절대 바뀌면
          안 되고(프론트 분기가 여기 걸려 있어요), <span className="icode">message</span>는{' '}
          <strong>사람이 읽는</strong> 문장이라 자유롭게 다듬어도 됩니다. 이 역할 분리가 없으면
          "메시지 문구 하나 고쳤더니 프론트 분기가 깨지는" 사고가 나요.
        </p>
        <Code>{CODE_FORMAT_MIDDLEWARE}</Code>
        <div className="warn">
          <b>스택 트레이스는 절대 응답에 싣지 마세요</b> 스택에는 서버의 파일 경로, 폴더 구조,
          사용 중인 라이브러리와 버전이 그대로 담겨요. 공격자에게는 "어떤 취약점을 시도해 볼지"
          고르는 <strong>내부 지도</strong>가 됩니다. 예상 밖 에러는 사용자에겐{' '}
          <strong>500 + 범용 메시지</strong>만, 진짜 내용은 <strong>서버 로그에만</strong> —
          이 원칙은 예외가 없어요.
        </div>
        <div className="tip">
          <b>우리 실습과의 연결</b> 실습 전반부가 바로 이 작업이에요. 블로그 앱의 모든 API가{' '}
          <span className="icode">error.code / error.message</span> 구조로 실패하게 다듬으면,
          후반부(섹션 6)의 폴백 UI가 <strong>어떤 API 실패든 같은 코드로</strong> 받아 냅니다.
        </div>
      </Section>

      <Section n={6} title="프론트: 폴백 UI와 에러 바운더리" sub="화면 전체가 아니라 '영역'만 무너지게">
        <p>
          백엔드가 아무리 잘해도 프론트에서 렌더링 중 에러가 나면 React는 기본적으로{' '}
          <strong>화면 전체를 비워 버려요</strong> — 사용자 눈엔 하얀 화면만 남죠. 이걸 막는
          안전망이 <strong>에러 바운더리(Error Boundary)</strong>입니다. 자식 컴포넌트가 렌더링
          중에 던진 에러를 받아 내고, 무너진 자리에 <strong>대체 화면(폴백)</strong>을 세우는
          컴포넌트예요.
        </p>
        <Code>{CODE_BOUNDARY}</Code>
        <p>
          설계의 핵심은 <strong>어디에 두르느냐</strong>예요. 앱 전체를 하나로 감싸면 "전부
          죽거나 전부 살거나"가 되죠. 대신 <strong>글 목록, 댓글, 사이드바처럼 영역 단위로</strong>{' '}
          두르면 한 영역이 터져도 나머지는 멀쩡히 삽니다 — 배가 침수돼도 격벽 한 칸만 잠기는
          것처럼요.
        </p>
        <p>
          중요한 구분 하나: 에러 바운더리는 <strong>렌더링 중의 에러</strong>를 잡는 도구라,{' '}
          <span className="icode">fetch</span> 실패 같은 <strong>요청 에러는 잡지
          못합니다</strong>(이벤트 핸들러·비동기 코드는 범위 밖이에요). 요청 실패에는 상태
          기반의 폴백 — <strong>'다시 시도' 버튼</strong>이 있는 에러 화면 — 을 직접 만듭니다.
        </p>
        <Code>{CODE_RETRY}</Code>
        <div className="tip">
          <b>'다시 시도'가 주는 경험</b> 네트워크 에러의 상당수는 <strong>일시적</strong>이에요
          (지하철 터널 구간, 와이파이 전환). 새로고침을 강요하는 대신 실패한 영역에 버튼 하나를
          두면, 사용자는 <strong>입력 중이던 내용과 스크롤 위치를 지키면서</strong> 그 영역만
          복구할 수 있어요. 실습 후반부에서 블로그 글 목록에 이걸 직접 답니다.
        </div>
      </Section>

      <Section n={7} title="로깅 기초" sub="사용자에게 보여줄 것과 로그에만 남길 것">
        <p>
          지금까지의 원칙을 한 줄로 줄이면 이래요 — <strong>사용자에겐 친절한 요약, 로그엔
          냉정한 전부.</strong> 그럼 로그는 어떻게 남겨야 나중에 쓸모가 있을까요? 첫걸음은{' '}
          <span className="icode">console.log</span> 하나로 버티던 습관에서{' '}
          <strong>레벨(level)</strong>이 있는 로깅으로 넘어가는 것입니다.
        </p>
        <Code>{CODE_LOGGING}</Code>
        <p>
          <span className="icode">info</span>는 정상 흐름의 발자국,{' '}
          <span className="icode">warn</span>은 "아직 장애는 아니지만 눈여겨볼 것",{' '}
          <span className="icode">error</span>는 "지금 살펴야 할 문제"예요. 레벨이 있어야 운영
          중에 "error만 보여 줘"라고 <strong>걸러 볼 수</strong> 있고, 심각한 것만 알림으로 받는
          자동화도 걸 수 있습니다. 두 번째 기둥은 <strong>요청 ID</strong>예요. 서버엔 수많은
          요청이 섞여 들어오는데, 로그 줄들이 <strong>어느 요청의 것인지</strong> 묶어 주는 실이
          없으면 에러 한 건의 앞뒤 맥락을 복원할 수 없거든요.
        </p>
        <Code>{CODE_REQUEST_ID}</Code>
        <div className="warn">
          <b>로그에도 금기가 있다</b> 비밀번호, 토큰, 주민번호 같은 민감정보는{' '}
          <strong>로그에도 남기면 안 돼요.</strong> 로그 파일은 생각보다 많은 사람과 시스템이
          들여다봅니다. "로그엔 전부"의 '전부'는 <strong>기술적 맥락의 전부</strong>지, 비밀의
          전부가 아니에요.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🚨 여기까지 왔다면</h3>
          <p className="muted">
            이제 에러는 두렵기만 한 대상이 아니라 <strong>설계할 수 있는 대상</strong>이 됐어요.
            예상된 실패와 버그를 가르고, 커스텀 에러 클래스에 상태코드를 실어, 미들웨어 한 곳에서{' '}
            <span className="icode">code/message</span>로 통일해 내보내고, 프론트에선 영역 단위
            폴백과 '다시 시도'로 받아 내는 흐름 — 그리고 그 전 과정을 요청 ID로 추적하는 로깅까지.
            그런데 이렇게 다듬은 에러 처리가 <strong>정말 약속대로 동작하는지</strong>는 어떻게
            보장할까요? 손으로 매번 눌러 볼 수는 없잖아요. 다음{' '}
            <Link to="/learn/lv2-practical-testing"><strong>테스트 작성 실전: 자신 있게 고치는
            힘</strong></Link> 강좌에서, 오늘 만든 에러 응답과 폴백이 깨지면 <strong>기계가 먼저
            알려 주는</strong> 안전망을 이어서 만들어 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
