// 이 파일이 하는 일: "폼 검증: 사용자를 돕고 서버를 지키기" 코스 — 회원가입 폼을 소재로
// 클라이언트 검증(UX)과 서버 검증(보안)을 왜 '둘 다' 해야 하는지에서 출발해, 제어 컴포넌트·
// 규칙 설계·에러 메시지 UX·400 응답 설계·서버 에러 렌더링·제출 흐름까지 7개 섹션으로 안내하는
// 정적 학습 페이지. 프레이밍은 "검증은 두 번 한다 — 프론트에서 친절하게, 서버에서 단호하게".
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_TWO_LAYERS = `[사용자] ──입력──▶ [프론트 검증]           [서버 검증] ──통과──▶ [DB 저장]
                    "이메일 형식이         "이 요청, 정말
                     아니에요" (즉시)       믿어도 되나?" (최종)

프론트 검증 = UX 담당   : 제출 전에 바로 알려줘서 사용자의 시간을 아낀다
서버 검증   = 보안 담당 : 프론트를 거치지 않은 요청까지 전부 걸러 낸다

핵심: 프론트 검증은 '건너뛸 수 있는' 안내판이고,
서버 검증은 '건너뛸 수 없는' 검문소입니다.
안내판이 아무리 친절해도, 검문소가 없으면 문은 뚫려 있는 거예요.`;

const CODE_BYPASS = `// 프론트 검증이 왜 '보안'이 못 되는지 — 개발자도구 콘솔에서 직접 시연
// 브라우저의 required, 우리가 짠 React 검증… 전부 건너뛰고
// 서버에 요청을 '직접' 쏠 수 있습니다. 폼을 거치지 않고요.

fetch('/api/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: '이건이메일이아님',     // 프론트라면 걸렸을 값
    password: '1',                 // 8자 최소길이? 무시
    nickname: '<script>공격</script>',
  }),
});

// 서버가 이 값을 검사 없이 저장하면?
// → 깨진 데이터, 뚫린 정책, 최악엔 스크립트 주입까지.
// 프론트 검증은 '착한 사용자'만 통과합니다. 서버는 모두를 상대해야 해요.`;

const CODE_CONTROLLED = `// 제어 컴포넌트 — 입력값의 주인을 DOM이 아니라 React 상태로 옮긴다
// 필드가 4개라도 useState 4번이 아니라, 객체 '하나'로 관리하는 패턴.
const [form, setForm] = useState({
  email: '',
  password: '',
  passwordConfirm: '',
  nickname: '',
});

// name 속성을 열쇠로 써서, 핸들러 '하나'가 모든 필드를 처리
function handleChange(e) {
  const { name, value } = e.target;
  setForm((prev) => ({ ...prev, [name]: value }));  // 해당 필드만 갈아끼움
}

// JSX: value(상태 → 화면)와 onChange(화면 → 상태)를 둘 다 묶어야 '제어'다
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />`;

const CODE_INPUT_TYPES = `// 타입별로 '값이 담기는 곳'이 다르다 — 여기서 자주 미끄러져요
function handleChange(e) {
  const { name, type, value, checked } = e.target;
  setForm((prev) => ({
    ...prev,
    // 체크박스만 value가 아니라 checked(true/false)를 읽는다
    [name]: type === 'checkbox' ? checked : value,
  }));
}

// <select>는 value로 읽으면 되지만, 숫자 입력은 문자열로 들어오니
// 계산에 쓰려면 Number(value)로 바꿔야 합니다.
<input name="agree" type="checkbox" checked={form.agree} onChange={handleChange} />
<select name="job" value={form.job} onChange={handleChange}>
  <option value="student">학생</option>
  <option value="developer">개발자</option>
</select>`;

const CODE_RULES = `// 검증 규칙을 if문 더미가 아니라 '데이터(객체)'로 선언한다
// 규칙이 데이터면 → 프론트·서버가 같은 표를 보고 검사할 수 있고, 고칠 곳도 한 곳.
const RULES = {
  email: {
    required: true,
    pattern: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,   // '무언가@무언가.무언가' 골격
    message: '이메일 형식으로 입력해 주세요. 예: mirim@awesomedev.dev',
  },
  password: {
    required: true,
    minLength: 8,
    message: '비밀번호는 8자 이상이어야 해요.',
  },
  nickname: {
    required: true,
    minLength: 2,
    maxLength: 12,
    message: '닉네임은 2~12자로 지어 주세요.',
  },
};

// 검사기는 규칙 표를 '읽기만' 한다 — 새 규칙이 생겨도 이 함수는 안 바뀜
function validateField(name, value) {
  const rule = RULES[name];
  if (!rule) return '';
  if (rule.required && value.trim() === '') return '필수 입력 항목이에요.';
  if (rule.minLength && value.length < rule.minLength) return rule.message;
  if (rule.maxLength && value.length > rule.maxLength) return rule.message;
  if (rule.pattern && !rule.pattern.test(value)) return rule.message;
  return '';  // 빈 문자열 = 통과
}`;

const CODE_CROSS = `// 교차 검증 — '비밀번호 확인'은 규칙 표만으론 못 잡는다
// 한 필드의 정답이 '다른 필드의 값'에 달려 있기 때문. 폼 전체를 보는 단계가 따로 필요.
function validateForm(form) {
  const errors = {};

  // ① 필드별 규칙 검사 (RULES 표 기반)
  for (const name of Object.keys(RULES)) {
    const msg = validateField(name, form[name]);
    if (msg) errors[name] = msg;
  }

  // ② 필드 사이를 건너 보는 교차 검증
  if (form.passwordConfirm !== form.password) {
    errors.passwordConfirm = '비밀번호가 서로 달라요. 다시 확인해 주세요.';
  }

  return errors;  // 비어 있으면({}) 전부 통과
}`;

const CODE_ERROR_TIMING = `// 에러를 '언제' 보여줄까 — touched(만진 필드)로 타이밍을 제어한다
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});   // 사용자가 떠나 본 필드 기록

// blur(필드를 떠날 때): 이 필드를 '만졌다'고 기록하고 그때부터 검사
function handleBlur(e) {
  const { name, value } = e.target;
  setTouched((prev) => ({ ...prev, [name]: true }));
  setErrors((prev) => ({ ...prev, [name]: validateField(name, value) }));
}

// 화면: '만진 필드'의 에러만 보여준다 — 타이핑 시작도 전에 빨간 글씨 금지
{touched.email && errors.email && (
  <p className="field-error">{errors.email}</p>
)}

// 정리하면:
//  입력 중(onChange) 검사 → 너무 성급함 (한 글자 쳤는데 "형식 오류"라니)
//  blur 시 검사          → 기본값으로 무난함 (다 쓰고 떠날 때 알려줌)
//  제출 시 검사          → 최후의 그물 (안 만진 필드까지 전부 훑는다)`;

const CODE_FOCUS = `// 제출을 눌렀는데 에러가 있다면 — '첫 에러 필드'로 포커스를 옮겨 주는 배려
function handleSubmit(e) {
  e.preventDefault();
  const nextErrors = validateForm(form);
  setErrors(nextErrors);
  setTouched({ email: true, password: true, passwordConfirm: true, nickname: true });

  const firstErrorField = Object.keys(nextErrors)[0];
  if (firstErrorField) {
    // name 속성으로 해당 input을 찾아 포커스 — 긴 폼에서 특히 고마운 동작
    document.querySelector('[name="' + firstErrorField + '"]')?.focus();
    return;  // 에러가 있으면 서버로 보내지 않는다
  }
  // ...여기부터 서버 전송 (섹션 7)
}`;

const CODE_SERVER_VALIDATE = `// 서버(Express 예시) — 프론트와 '같은 규칙'을 서버 언어로 한 번 더
// 프론트 코드를 아무리 잘 짜도, 서버는 그걸 본 적이 없다는 듯 다시 검사해야 한다.
function validateSignup(req, res, next) {
  const { email, password, passwordConfirm, nickname } = req.body;
  const errors = {};

  if (!email || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
    errors.email = '이메일 형식이 올바르지 않습니다.';
  }
  if (!password || password.length < 8) {
    errors.password = '비밀번호는 8자 이상이어야 합니다.';
  }
  if (password !== passwordConfirm) {
    errors.passwordConfirm = '비밀번호가 일치하지 않습니다.';
  }
  if (!nickname || nickname.length < 2 || nickname.length > 12) {
    errors.nickname = '닉네임은 2~12자여야 합니다.';
  }

  if (Object.keys(errors).length > 0) {
    // 400 Bad Request: "요청 자체가 규칙에 어긋난다"는 상태코드
    return res.status(400).json({ message: '입력값을 확인해 주세요.', errors });
  }
  next();  // 통과 → 다음 단계(실제 가입 처리)로
}

// 미들웨어로 분리했으니 라우트는 '검증 끝난 값'만 받는다
app.post('/api/signup', validateSignup, signupHandler);`;

const CODE_400_FORMAT = `// 400 응답 포맷 — '필드별' 에러를 담는 게 핵심 설계 포인트
// 나쁜 예: { "message": "입력값이 잘못됐습니다" }
//   → 프론트가 '어느 칸'이 틀렸는지 몰라서 입력칸 아래에 못 그려 준다.

// 좋은 예: 필드 이름을 열쇠로 쓰는 errors 객체
{
  "message": "입력값을 확인해 주세요.",
  "errors": {
    "email": "이메일 형식이 올바르지 않습니다.",
    "nickname": "닉네임은 2~12자여야 합니다."
  }
}

// 프론트의 errors 상태({ email: '...', nickname: '...' })와
// 모양이 '같다'는 게 보이나요? 그래서 다음 섹션에서
// 서버 에러를 setErrors(data.errors) 한 줄로 폼에 부어 넣을 수 있습니다.`;

const CODE_APPLY_SERVER_ERRORS = `// 서버의 400/409 응답을 폼에 '다시 그리기'
try {
  const res = await fetch('/api/signup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(form),
  });

  if (res.status === 400) {
    // 검증 실패: 필드별 에러를 그대로 폼 에러 상태에 붓는다
    const data = await res.json();
    setErrors(data.errors);          // 각 입력칸 아래에 표시됨
    return;
  }

  if (res.status === 409) {
    // 409 Conflict: '이미 있는 것과 부딪힘' — 이메일 중복처럼
    // 서버(DB)만 알 수 있는 에러. 프론트가 미리 잡을 방법이 없다.
    setErrors({ email: '이미 가입된 이메일이에요. 로그인해 보시겠어요?' });
    return;
  }

  if (!res.ok) throw new Error('서버 오류');   // 500 등 나머지
  // ...성공 처리 (섹션 7)
} catch (err) {
  // fetch가 throw로 떨어지는 건 '응답 자체가 없는' 네트워크 실패
  // 검증 실패(400)와는 성격이 다르다 — 사용자 잘못이 아니니 문구도 달라야 한다
  setGlobalError('연결이 불안정해요. 잠시 후 다시 시도해 주세요.');
}`;

const CODE_SUBMIT_FLOW = `// 제출 흐름 완성 — 중복 제출 방지 + 성공/실패 뒷정리
const [submitting, setSubmitting] = useState(false);

async function handleSubmit(e) {
  e.preventDefault();
  if (submitting) return;              // 이중 안전장치

  const nextErrors = validateForm(form);
  if (Object.keys(nextErrors).length > 0) {
    setErrors(nextErrors);
    return;                            // 프론트에서 걸리면 서버까지 안 간다
  }

  setSubmitting(true);                 // ① 버튼 잠금 — 연타로 가입 2번 방지
  try {
    const res = await fetch('/api/signup', { /* ...섹션 6과 동일... */ });
    if (res.ok) {
      setForm({ email: '', password: '', passwordConfirm: '', nickname: '' });
      navigate('/welcome');            // ② 성공: 폼 초기화 후 이동
    }
    // 400/409 처리는 섹션 6 — 이때 setForm을 부르지 않는 게 포인트!
    // ③ 실패해도 입력값은 보존: 사용자가 처음부터 다시 치게 만들지 않는다
  } finally {
    setSubmitting(false);              // 성공이든 실패든 반드시 잠금 해제
  }
}

<button type="submit" disabled={submitting}>
  {submitting ? '가입 처리 중...' : '가입하기'}
</button>`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '제어 컴포넌트' },
  { n: 3, label: '검증 규칙 설계' },
  { n: 4, label: '에러 메시지 UX' },
  { n: 5, label: '서버 검증과 400' },
  { n: 6, label: '서버 에러 그리기' },
  { n: 7, label: '제출 흐름 완성' },
];

export default function Lv2FormValidationPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>✅ 폼 검증<br />— 사용자를 돕고 서버를 지키기</h1>
        <p>
          "프론트에서 검증했으니까 서버는 믿어도 되겠지?" — 이 한 문장이 실제 서비스 사고의
          단골 원인이에요. 검증은 <strong>두 번</strong> 합니다. 프론트에서는{' '}
          <strong>친절하게</strong>(사용자의 시간을 아끼는 UX), 서버에서는{' '}
          <strong>단호하게</strong>(어떤 요청도 그냥 통과 못 하는 보안). 회원가입 폼 하나를
          끝까지 만들며 이 이중 검증의 감각을 손에 붙여 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 60분</span>
          <span className="chip">실습: 회원가입 폼 이중 검증</span>
          <span className="chip">선수 지식: React 기초·HTTP 기초</span>
        </div>
      </div>

      <Section n={1} title="왜 두 번 검증하는가" sub="클라이언트 검증은 UX, 서버 검증은 보안">
        <p>
          먼저 역할부터 갈라 봅시다. <strong>클라이언트(프론트) 검증</strong>의 임무는{' '}
          <strong>사용자 경험</strong>이에요. 이메일 칸에 오타를 낸 사용자에게 서버까지 갔다 오는
          긴 왕복 없이 <strong>그 자리에서 즉시</strong> "형식이 달라요"라고 알려 주죠. 반면{' '}
          <strong>서버 검증</strong>의 임무는 <strong>보안과 데이터 무결성</strong>입니다.
          서버에 도착한 요청이 규칙에 맞는지를 <strong>최종적으로</strong> 판정하고, 아니면
          거절해요.
        </p>
        <Code>{CODE_TWO_LAYERS}</Code>
        <p>
          "프론트에서 이미 막았는데 서버가 또 검사할 필요가 있나?"라는 의문이 들 수 있어요.
          핵심은 이겁니다 — <strong>서버는 요청이 우리 폼에서 왔는지 알 수 없어요.</strong>{' '}
          브라우저 개발자도구 콘솔에서 <span className="icode">fetch</span> 한 줄이면, 우리가 짠
          React 검증을 <strong>한 글자도 거치지 않고</strong> 서버에 직접 요청을 보낼 수 있거든요.
        </p>
        <Code>{CODE_BYPASS}</Code>
        <p>
          '프론트만 믿은' 서비스가 겪는 사고 유형은 정해져 있어요. <strong>① 깨진 데이터</strong>{' '}
          — 이메일 아닌 값이 이메일 칸에 저장돼 메일 발송이 줄줄이 실패하고,{' '}
          <strong>② 정책 붕괴</strong> — "비밀번호 8자 이상"이 뚫려 1자짜리 계정이 생기고,{' '}
          <strong>③ 주입 공격</strong> — 검사 없이 저장된 스크립트 문자열이 다른 사용자의 화면에서
          실행됩니다. 셋 다 서버 검증 하나로 막히는 사고예요.
        </p>
        <div className="tip">
          <b>한 줄 원칙</b> 프론트 검증은 <strong>있으면 친절</strong>이고, 서버 검증은{' '}
          <strong>없으면 사고</strong>입니다. 순서를 굳이 정하면 서버 검증이 먼저예요 — UX는
          나중에 입힐 수 있지만, 보안 구멍은 열린 순간부터 위험이니까요.
        </div>
      </Section>

      <Section n={2} title="제어 컴포넌트로 폼 상태 잡기" sub="value + onChange — 입력의 주인을 React로">
        <p>
          검증을 하려면 먼저 <strong>지금 입력칸에 뭐가 적혀 있는지</strong>를 React가 알아야
          해요. 그래서 쓰는 게 <strong>제어 컴포넌트(controlled component)</strong> 패턴입니다.{' '}
          <span className="icode">value</span>로 상태를 화면에 흘려보내고,{' '}
          <span className="icode">onChange</span>로 화면의 변화를 상태에 되돌리는 —{' '}
          <strong>양방향으로 묶인</strong> 입력이죠. 이렇게 하면 입력값의 주인이 DOM이 아니라
          React 상태가 되고, 검증 함수는 그 상태만 들여다보면 됩니다.
        </p>
        <p>
          회원가입 폼엔 필드가 넷(이메일·비밀번호·비밀번호 확인·닉네임)이에요.{' '}
          <span className="icode">useState</span>를 네 번 부르는 대신, <strong>객체 하나</strong>에
          담고 <span className="icode">name</span> 속성을 열쇠 삼아 핸들러 하나로 처리하는 패턴을
          쓰면 필드가 늘어나도 코드가 늘지 않습니다.
        </p>
        <Code>{CODE_CONTROLLED}</Code>
        {/* 학습 포인트: [name]: value 의 대괄호는 '계산된 속성 이름' — 변수의 값을 객체 키로 쓴다 */}
        <p>
          한 가지 함정이 있어요. 입력 타입마다 <strong>값이 담기는 자리</strong>가 달라요.
          텍스트·셀렉트는 <span className="icode">value</span>지만, 체크박스는{' '}
          <span className="icode">checked</span>(불리언)를 읽어야 합니다. 이걸 놓치면 체크박스
          값이 항상 <span className="icode">"on"</span> 같은 이상한 문자열로 잡혀요.
        </p>
        <Code>{CODE_INPUT_TYPES}</Code>
        <div className="tip">
          <b>비유</b> 제어 컴포넌트는 <strong>칠판과 출석부</strong>예요. 학생(사용자)이 칠판(입력칸)에
          쓰는 즉시 선생님(React)이 출석부(상태)에 옮겨 적고, 칠판에 보이는 글씨도 항상 출석부
          기준으로 다시 씁니다. 출석부가 단일한 진실이니, 검증도 출석부만 보면 되는 거죠.
        </div>
      </Section>

      <Section n={3} title="검증 규칙 설계" sub="규칙을 코드가 아니라 데이터로 선언한다">
        <p>
          이제 "무엇이 올바른 입력인가"를 정할 차례예요. 회원가입 폼의 규칙은 세 종류로
          정리됩니다. <strong>① 필수(required)</strong> — 비어 있으면 안 된다,{' '}
          <strong>② 최소/최대 길이</strong> — 비밀번호 8자 이상, 닉네임 2~12자,{' '}
          <strong>③ 포맷(정규식)</strong> — 이메일은 '무언가@무언가.무언가' 골격을 갖춰야 한다.
        </p>
        <p>
          이 규칙들을 <span className="icode">if</span>문으로 필드마다 흩뿌리면, 나중에 "닉네임
          최대 길이를 12에서 16으로" 바꿀 때 코드를 뒤져야 해요. 대신 규칙을{' '}
          <strong>데이터(객체)</strong>로 한곳에 선언하고, 검사기는 그 표를 읽기만 하게 만듭니다.
        </p>
        <Code>{CODE_RULES}</Code>
        {/* 학습 포인트: 로직과 데이터의 분리 — 규칙(변하는 것)과 검사기(안 변하는 것)를 갈라 둔다 */}
        <p>
          그런데 <strong>비밀번호 확인</strong>은 이 표로 못 잡아요. "올바른 값"이 고정된 규칙이
          아니라 <strong>다른 필드(비밀번호)의 현재 값</strong>에 달려 있으니까요. 이런 걸{' '}
          <strong>교차 검증(cross-field validation)</strong>이라 부르고, 필드 하나가 아니라{' '}
          <strong>폼 전체</strong>를 받아 보는 단계에서 처리합니다.
        </p>
        <Code>{CODE_CROSS}</Code>
        <div className="tip">
          <b>규칙을 데이터로 두는 진짜 보상</b> 섹션 5에서 서버에도 같은 검사를 만들 텐데,
          규칙이 표로 정리돼 있으면 서버 코드가 <strong>같은 표를 옮겨 적기</strong>만 하면
          됩니다. 프론트는 8자, 서버는 6자 — 이런 <strong>규칙 어긋남</strong>이 이중 검증의
          단골 버그인데, 표 하나가 그걸 막아 줘요.
        </div>
      </Section>

      <Section n={4} title="에러 메시지 UX" sub="'틀렸음'이 아니라 '어떻게 고칠지'를 말한다">
        <p>
          검증 로직이 있어도 <strong>보여 주는 타이밍</strong>이 나쁘면 사용자를 괴롭히는 폼이
          됩니다. 이메일 첫 글자 <span className="icode">m</span>을 치자마자 "이메일 형식이
          아닙니다"라고 빨간 글씨가 뜨면 어떤 기분일까요? 아직 쓰는 중인데요. 그래서 보통{' '}
          <strong>blur(필드를 떠날 때)</strong>를 기본 타이밍으로 잡습니다 — 다 쓰고 다음 칸으로
          넘어갈 때 조용히 알려 주는 거죠.
        </p>
        <Code>{CODE_ERROR_TIMING}</Code>
        {/* 학습 포인트: touched는 '검증 결과'가 아니라 '보여줄지 말지'를 정하는 별도 상태다 */}
        <p>
          문구도 UX예요. <strong>"잘못된 입력입니다"</strong>는 판정문이지 안내문이 아닙니다.
          사용자가 다음에 할 행동이 문구에 들어 있어야 해요 — "이메일 형식으로 입력해 주세요.
          예: mirim@awesomedev.dev"처럼 <strong>고치는 방법과 예시</strong>까지요. 섹션 3의 규칙
          표에 <span className="icode">message</span>를 함께 둔 이유가 이겁니다. 규칙과 안내문은
          한 몸이에요.
        </p>
        <p>
          마지막 배려는 <strong>포커스 이동</strong>입니다. 제출 버튼을 눌렀는데 화면 위쪽 어딘가의
          필드가 틀렸다면? 사용자가 스크롤하며 빨간 글씨를 찾아 헤매게 하지 말고, <strong>첫 번째
          에러 필드로 포커스를 옮겨</strong> 주세요. 키보드만 쓰는 사용자와 화면낭독기 사용자에게는
          배려를 넘어 필수입니다.
        </p>
        <Code>{CODE_FOCUS}</Code>
        <div className="tip">
          <b>타이밍 3단 정리</b> 처음 쓰는 중엔 <strong>조용히</strong>(onChange 검사는 성급함) →
          필드를 떠나면 <strong>알려주고</strong>(blur가 기본기) → 제출 때{' '}
          <strong>전부 훑는다</strong>(안 만진 필드까지 최후의 그물). 단, 한 번 에러가 표시된
          필드는 다시 입력하는 즉시 에러를 지워 주면 "고치고 있어요"가 실시간으로 반영돼 더
          친절해요.
        </div>
      </Section>

      <Section n={5} title="서버측 검증과 400 응답" sub="같은 규칙을, 이번엔 단호하게">
        <p>
          이제 검문소를 세울 차례예요. 섹션 1에서 봤듯 서버는 <strong>어떤 요청도 믿지 않고</strong>{' '}
          같은 규칙을 다시 검사합니다. 프론트의 규칙 표를 서버 코드로 옮겨 적는 거죠 — 필수·길이·
          포맷·교차 검증 전부요. 그리고 검증 로직을 라우트 핸들러 안에 섞지 않고{' '}
          <strong>미들웨어로 분리</strong>하면, 가입 처리 코드는 "검증이 끝난 깨끗한 값"만 받는
          다는 확신을 가질 수 있어요.
        </p>
        <Code>{CODE_SERVER_VALIDATE}</Code>
        {/* 학습 포인트: 미들웨어 = 요청이 핸들러에 닿기 전 거치는 관문. 통과는 next(), 거절은 즉시 응답 */}
        <p>
          거절할 땐 상태코드 <strong>400 Bad Request</strong>를 씁니다 — "요청 자체가 규칙에
          어긋난다"는 뜻이에요. 그런데 상태코드만큼 중요한 게 <strong>응답 몸통의 포맷</strong>
          입니다. "뭔가 잘못됐어요" 한 줄짜리 메시지로는 프론트가 <strong>어느 입력칸</strong>에
          에러를 그려야 할지 알 수 없거든요.
        </p>
        <Code>{CODE_400_FORMAT}</Code>
        <div className="tip">
          <b>포맷은 약속이다</b> <span className="icode">errors</span> 객체의 열쇠를{' '}
          <strong>폼 필드의 name과 똑같이</strong> 맞추는 것 — 이게 프론트·백 협업의 작은 계약서예요.
          이 약속만 지키면 프론트는 서버 에러를 받아 <span className="icode">setErrors(data.errors)</span>{' '}
          한 줄로 화면에 뿌릴 수 있습니다. 다음 섹션에서 바로 해 볼 거예요.
        </div>
      </Section>

      <Section n={6} title="서버 에러를 폼에 다시 그리기" sub="400·409·네트워크 실패 — 셋은 다르게 다룬다">
        <p>
          서버가 단호하게 거절했다면, 프론트는 그 거절을 <strong>친절하게 번역</strong>해서
          보여 줘야 해요. 섹션 5의 약속(필드명 열쇠의 <span className="icode">errors</span> 객체)
          덕분에, 400 응답의 필드 에러는 프론트의 에러 상태에 <strong>그대로 부어 넣으면</strong>{' '}
          각 입력칸 아래에 표시됩니다.
        </p>
        <p>
          그런데 <strong>서버만 아는 에러</strong>가 있어요. 대표가 <strong>이메일 중복</strong>
          입니다. 형식이 아무리 완벽해도 이미 가입된 이메일인지는 <strong>DB를 가진 서버만</strong>{' '}
          알 수 있죠. 이런 '이미 있는 것과 부딪히는' 상황엔 400 대신{' '}
          <strong>409 Conflict</strong>를 쓰는 관례가 있고, 프론트는 이를 받아 "이미 가입된
          이메일이에요. 로그인해 보시겠어요?"처럼 <strong>다음 행동을 제안하는</strong> 문구로
          안내합니다.
        </p>
        <Code>{CODE_APPLY_SERVER_ERRORS}</Code>
        {/* 학습 포인트: fetch는 400·409·500에도 catch로 안 떨어진다 — 상태코드 분기는 우리 몫,
            catch에 오는 건 응답 자체가 없는 네트워크 실패다 */}
        <p>
          마지막 갈래는 <strong>네트워크 실패</strong>예요. 와이파이가 끊겼거나 서버가 죽어서{' '}
          <strong>응답 자체가 오지 않은</strong> 경우죠. 이건 사용자의 입력이 틀린 게 아니니까,
          입력칸 옆 빨간 글씨가 아니라 <strong>폼 상단의 안내</strong>로 "연결이 불안정해요.
          잠시 후 다시 시도해 주세요"처럼 구분해서 보여 줍니다. 검증 실패는{' '}
          <strong>사용자가 고칠 수 있는</strong> 문제, 네트워크 실패는{' '}
          <strong>기다리거나 재시도할</strong> 문제 — 안내가 달라야 해요.
        </p>
        <div className="tip">
          <b>세 갈래 요약</b> <strong>400</strong> = 입력이 규칙 위반 → 필드별로 그린다 ·{' '}
          <strong>409</strong> = 서버만 아는 충돌(중복) → 다음 행동을 제안한다 ·{' '}
          <strong>네트워크 실패</strong> = 응답 없음 → 재시도를 안내한다. 에러의{' '}
          <strong>원인 주체</strong>(사용자/데이터/환경)가 다르면 화면도 달라집니다.
        </div>
      </Section>

      <Section n={7} title="제출 흐름 완성" sub="중복 제출 방지 · 성공 뒷정리 · 입력값 보존">
        <p>
          부품이 다 모였으니 제출 버튼을 눌렀을 때의 <strong>전체 흐름</strong>을 완성합시다.
          여기서 챙길 세 가지: <strong>① 제출 중 버튼 비활성화</strong> — 응답을 기다리는 동안
          버튼을 연타하면 같은 가입 요청이 두 번 날아가요. <span className="icode">submitting</span>{' '}
          상태로 버튼을 잠그고, 문구도 "가입 처리 중..."으로 바꿔 <strong>기다리는 중임을</strong>{' '}
          보여 줍니다.
        </p>
        <p>
          <strong>② 성공 시 뒷정리</strong> — 폼을 초기화하고 환영 페이지로 이동합니다. 성공했는데
          입력값이 그대로 남아 있으면 "된 건가?" 싶어 또 누르게 되거든요.{' '}
          <strong>③ 실패 시 입력값 보존</strong> — 반대로 실패했을 땐 절대 폼을 비우지 않습니다.
          닉네임 하나 때문에 거절당했는데 이메일·비밀번호까지 처음부터 다시 치게 만드는 것만큼
          사용자를 떠나보내는 지름길이 없어요.
        </p>
        <Code>{CODE_SUBMIT_FLOW}</Code>
        {/* 학습 포인트: finally는 성공·실패 어느 길로 빠져도 반드시 실행 — 잠금 해제를 여기 두면
            "에러 났는데 버튼이 영영 안 눌려요" 같은 잠금 고착 버그가 원천 차단된다 */}
        <div className="tip">
          <b>실습 마무리 — 우회 공격 방어 확인</b> 폼이 완성됐다면 섹션 1의 시연을{' '}
          <strong>스스로에게</strong> 해 보세요. 개발자도구 콘솔에서 엉터리 값으로{' '}
          <span className="icode">fetch('/api/signup', ...)</span>을 직접 쏘는 겁니다. 서버가{' '}
          <strong>400으로 단호하게 거절</strong>하면 성공이에요. 프론트 검증을 지워도 서버가
          막아 준다 — 이걸 눈으로 확인하는 순간, "검증은 두 번"이 지식이 아니라 감각이 됩니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>✅ 여기까지 왔다면</h3>
          <p className="muted">
            회원가입 폼 하나에 이렇게 많은 설계가 숨어 있었어요. 프론트의 친절(제어 컴포넌트·
            규칙 표·blur 타이밍·포커스 이동)과 서버의 단호함(미들웨어 검증·400 포맷·409 중복),
            그리고 둘을 잇는 약속(필드명 열쇠의 errors 객체)까지 — 이제 여러분의 폼은 사용자를
            돕고 서버를 지킵니다. 그런데 이번 코스에서 스친 질문이 하나 남아 있어요. 500 에러는?
            응답이 오다 만 경우는? 실패가 겹칠 땐 어디서부터 수습하죠? 다음{' '}
            <Link to="/learn/lv2-error-handling-patterns"><strong>에러 처리 패턴: 무너지지
            않는 서비스</strong></Link> 코스에서, 실패를 전제로 설계하는 감각을 이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
