// 이 파일이 하는 일: "테스트 작성 실전: 자신 있게 고치는 힘" 코스 — 왜 테스트가 리팩터링의
// 안전망인지에서 출발해, Vitest 단위 테스트·무엇을 테스트할지 고르는 눈·supertest API 통합
// 테스트·테스트 DB 격리·Testing Library 컴포넌트 테스트·지속 가능한 습관까지 7개 섹션으로
// 안내하는 정적 학습 페이지. 프레이밍은 "고칠 때마다 손이 떨리지 않는 개발자" — 기초 문법을
// 아는 수강생이 실제로 돌아가는 서비스에 테스트를 '붙이는' 경험을 하도록 이끈다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_COST = `수동 확인의 하루            테스트가 있는 하루
──────────────────         ──────────────────
코드 수정                    코드 수정
→ 서버 재시작                → npm test (수 초)
→ 브라우저 열고 로그인        → 초록불 확인, 커밋
→ 화면 눌러 보며 확인
→ "다른 데는 안 깨졌겠지..?"
→ (사실 깨졌는데 모름)

핵심: 수동 확인은 '고친 곳'만 봅니다. 테스트는 '고치지 않은 곳'까지
기계가 매번 전부 다시 확인해 줍니다. 이게 회귀(regression) 버그를
잡는 안전망이고, 코드를 자신 있게 고칠 수 있는 힘의 정체예요.`;

const CODE_FIRST_TEST = `// src/utils/validate.js — 테스트할 순수 함수 (입력 → 출력, 부수효과 없음)
export function isValidEmail(email) {
  if (typeof email !== 'string') return false;
  const at = email.indexOf('@');
  const dot = email.lastIndexOf('.');
  return at > 0 && dot > at + 1 && dot < email.length - 1;
}

// src/utils/validate.test.js — 같은 폴더에 .test.js를 나란히 두는 관례
import { describe, it, expect } from 'vitest';
import { isValidEmail } from './validate';

describe('isValidEmail', () => {
  it('올바른 이메일이면 true를 돌려준다', () => {
    const input = 'intern@awesomedev.dev';   // ① 준비 (Arrange)
    const result = isValidEmail(input);       // ② 실행 (Act)
    expect(result).toBe(true);                // ③ 검증 (Assert)
  });

  it('@가 없으면 false를 돌려준다', () => {
    expect(isValidEmail('intern.awesomedev.dev')).toBe(false);
  });

  it('@로 시작하면 false를 돌려준다', () => {   // 선두 @ = 아이디가 비었다 (at === 0)
    expect(isValidEmail('@awesomedev.dev')).toBe(false);
  });
});`;

const CODE_RUN = `# frontend/ 에서 — Vitest 설치와 실행
npm install -D vitest

# package.json의 "scripts"에 한 줄:
#   "test": "vitest"

npm test          # 감시 모드: 파일을 저장할 때마다 관련 테스트 재실행
npx vitest run    # 한 번만 실행하고 종료 (CI에서 쓰는 방식)

# 실행 결과 읽는 법:
#  ✓ isValidEmail > 올바른 이메일이면 true를 돌려준다
#  ✗ isValidEmail > @가 없으면 false를 돌려준다
#    → expected true to be false  ← 어떤 '명세'가 깨졌는지 문장으로 보인다`;

const CODE_EDGE = `// 무엇을 테스트할까 — 정상 경로 1개보다 경계·실패 3개가 값지다
import { describe, it, expect } from 'vitest';
import { calcDiscount } from './price';

describe('calcDiscount', () => {
  // 정상 경로: 물론 필요하다. 하지만 여기서 멈추면 반쪽짜리.
  it('10000원에 10% 할인이면 9000원이다', () => {
    expect(calcDiscount(10000, 10)).toBe(9000);
  });

  // 경계값: 버그는 늘 '가장자리'에서 태어난다
  it('할인율 0%면 원가 그대로다', () => {
    expect(calcDiscount(10000, 0)).toBe(10000);
  });
  it('할인율 100%면 0원이다', () => {
    expect(calcDiscount(10000, 100)).toBe(0);
  });

  // 실패 케이스: 잘못된 입력에 어떻게 '행동해야 하는지'도 명세다
  it('할인율이 음수면 에러를 던진다', () => {
    expect(() => calcDiscount(10000, -5)).toThrow();
  });
});`;

const CODE_SUPERTEST = `// server/routes/auth.test.js — supertest로 '진짜 HTTP 요청'을 흉내 내기
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../app';   // listen() 하지 않은 Express 앱 객체를 그대로 넘긴다

describe('POST /api/login', () => {
  it('올바른 계정이면 200과 토큰을 돌려준다', async () => {
    const res = await request(app)
      .post('/api/login')
      .send({ email: 'intern@test.dev', password: 'pass1234!' });

    expect(res.status).toBe(200);                    // ① 상태 코드
    expect(res.body.token).toBeTypeOf('string');     // ② 응답 바디
    expect(res.headers['content-type']).toContain('json'); // ③ 헤더까지
  });

  it('비밀번호가 틀리면 401을 돌려준다', async () => {
    const res = await request(app)
      .post('/api/login')
      .send({ email: 'intern@test.dev', password: 'wrong!' });

    expect(res.status).toBe(401);
    expect(res.body.token).toBeUndefined();  // 실패했는데 토큰이 새면 안 된다
  });
});

describe('POST /api/signup', () => {
  it('이미 가입된 이메일이면 409를 돌려준다', async () => {
    await request(app).post('/api/signup')
      .send({ email: 'dup@test.dev', password: 'pass1234!' });
    const res = await request(app).post('/api/signup')
      .send({ email: 'dup@test.dev', password: 'pass1234!' });

    expect(res.status).toBe(409);   // Conflict — '중복'의 표준 대답
  });
});`;

const CODE_AUTH_FLOW = `// 로그인이 필요한 API 테스트 — 테스트 안에서 세션(토큰)을 직접 만든다
it('내 정보 조회는 로그인한 사용자에게만 열린다', async () => {
  // ① 준비: 로그인부터 해서 토큰을 손에 쥔다
  const login = await request(app)
    .post('/api/login')
    .send({ email: 'intern@test.dev', password: 'pass1234!' });
  const token = login.body.token;

  // ② 실행: 토큰을 붙여 보호된 API를 부른다
  const res = await request(app)
    .get('/api/me')
    .set('Authorization', 'Bearer ' + token);

  // ③ 검증
  expect(res.status).toBe(200);
  expect(res.body.email).toBe('intern@test.dev');
});

it('토큰 없이 부르면 401이다', async () => {
  const res = await request(app).get('/api/me');
  expect(res.status).toBe(401);
});`;

const CODE_TESTDB = `// tests/setup.js — 매 테스트 전에 '깨끗한 무대'를 다시 차린다
import { beforeEach } from 'vitest';
import { db } from '../server/db';

beforeEach(async () => {
  await db.query('DELETE FROM users');      // ① 초기화: 이전 테스트의 흔적 제거
  await db.query(                            // ② 시드: 늘 같은 출발점 심기
    "INSERT INTO users (email, password_hash) VALUES ('intern@test.dev', '...')"
  );
});

// .env.test — 테스트는 반드시 '자기 전용 DB'를 본다
// DATABASE_URL=postgres://localhost:5432/mirim_test   ← 개발 DB와 다른 이름!

// 위험 신호 체크리스트:
//  · 테스트를 한 개만 돌리면 실패하는데 전체를 돌리면 성공한다
//    → 앞 테스트가 남긴 데이터에 기대고 있다는 뜻 (순서 의존)
//  · 테스트 순서를 바꿨더니 결과가 달라진다 → 같은 병
//  · 로컬에선 되는데 동료 PC에선 깨진다 → 시드 없이 '내 DB 상태'에 기댄 것`;

const CODE_RTL = `// SignupForm.test.jsx — 사용자가 보는 대로: 역할과 라벨로 찾고, 입력하고, 확인한다
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SignupForm from './SignupForm';

describe('SignupForm', () => {
  it('이메일 없이 제출하면 검증 에러가 보인다', async () => {
    const user = userEvent.setup();
    render(<SignupForm />);                            // ① 준비: 화면에 그린다

    // ② 실행: 사용자가 하듯 — 버튼을 '가입하기'라는 이름으로 찾아 누른다
    await user.click(screen.getByRole('button', { name: '가입하기' }));

    // ③ 검증: 화면에 에러 문구가 '보이는가'
    expect(screen.getByText('이메일을 입력해 주세요')).toBeInTheDocument();
  });

  it('짧은 비밀번호를 입력하면 안내 문구가 보인다', async () => {
    const user = userEvent.setup();
    render(<SignupForm />);

    await user.type(screen.getByLabelText('이메일'), 'intern@test.dev');
    await user.type(screen.getByLabelText('비밀번호'), '123');   // 8자 미만
    await user.click(screen.getByRole('button', { name: '가입하기' }));

    expect(screen.getByText('비밀번호는 8자 이상이어야 해요')).toBeInTheDocument();
  });
});

// getByRole('button', { name: ... }) / getByLabelText(...) 가 기본기.
// CSS 클래스나 내부 state로 찾기 시작하면 '구현'에 붙는 테스트가 된다.`;

const CODE_HABIT = `// package.json — 테스트를 '흐름'에 심는다
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "precommit": "vitest run"   // 커밋 전에 자동 실행 (husky 등으로 연결)
  }
}

# CI(예: GitHub Actions / Jenkins)의 핵심 한 줄도 결국 같다:
#   npm ci && npx vitest run
# 초록불이어야만 병합 — '깨진 채로 합쳐지는 일'을 구조적으로 막는다.

# 커버리지는 참고 지표로만:
#   npx vitest run --coverage
# 숫자 100%보다 "핵심 시나리오 10개가 전부 초록"이 훨씬 믿을 만하다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다(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: '왜 테스트인가' },
  { n: 2, label: '첫 단위 테스트' },
  { n: 3, label: '무엇을 테스트할까' },
  { n: 4, label: 'API 통합 테스트' },
  { n: 5, label: '테스트용 DB' },
  { n: 6, label: '컴포넌트 테스트' },
  { n: 7, label: '지속 가능한 습관' },
];

export default function Lv2PracticalTestingPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>테스트 작성 실전<br />: 자신 있게 고치는 힘</h1>
        <p>
          코드를 고칠 때마다 "다른 데가 깨지진 않았을까" 손이 떨린 적 있나요? 그 떨림을 없애는
          도구가 <strong>테스트</strong>입니다. 이 코스에서는 순수 함수의 <strong>단위 테스트</strong>부터
          로그인 API의 <strong>통합 테스트</strong>, 회원가입 폼의 <strong>컴포넌트 테스트</strong>까지 —
          장식이 아니라 <strong>실제 코드에 붙는</strong> 테스트를 직접 써 봅니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 70분</span>
          <span className="chip">실습: 핵심 시나리오 테스트 10개</span>
          <span className="chip">선수 지식: JS 기초 · HTTP · 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="리팩터링의 안전망 — 회귀 버그를 기계가 잡는다">
        <p>
          기능을 하나 고치면, 정말 무서운 건 <strong>고친 곳</strong>이 아니라{' '}
          <strong>안 고친 곳</strong>이에요. 어제까지 멀쩡하던 로그인이 오늘 결제 코드를 만졌더니
          깨지는 것 — 이런 버그를 <strong>회귀(regression)</strong>라고 부릅니다. 사람 눈으로는
          매번 전부 다시 확인할 수 없지만, 테스트가 있으면 <strong>기계가 수 초 만에</strong> 전
          영역을 다시 확인해 줍니다.
        </p>
        <Code>{CODE_COST}</Code>
        <p>
          "테스트 쓸 시간에 기능 하나 더 만들지" 싶을 수 있어요. 처음 며칠은 맞는 말입니다.
          하지만 서비스가 커질수록 수동 확인 비용은 <strong>매번</strong> 들고, 테스트 작성 비용은{' '}
          <strong>한 번만</strong> 듭니다. 이 두 곡선이 교차하는 시점 — 대략 "고칠 때마다 확인할
          화면이 세 개를 넘는 순간" — 부터는 테스트가 있는 쪽이 무조건 빠릅니다.
        </p>
        <p>
          관점을 하나 더 얹을게요. 잘 쓴 테스트는 <strong>실행 가능한 명세서</strong>예요.
          "비밀번호가 틀리면 401을 돌려준다"라는 문서는 낡을 수 있지만, 같은 내용의 테스트는
          코드가 그 약속을 어기는 순간 <strong>빨간불</strong>로 알려 줍니다. 문서는 거짓말을 해도
          테스트는 못 하죠.
        </p>
        <div className="tip">
          <b>비유</b> 테스트는 <strong>등반가의 로프</strong>예요. 로프를 거는 데 시간이 들지만,
          로프가 있어야 과감하게 다음 홀드로 손을 뻗을 수 있죠. 리팩터링도 같습니다 — 안전망이
          있는 개발자만 코드를 <strong>자신 있게</strong> 고칩니다. 없는 개발자는 "건드리면
          깨질까 봐" 낡은 코드를 평생 껴안고 살아요.
        </div>
      </Section>

      <Section n={2} title="첫 단위 테스트" sub="Vitest로 순수 함수부터 — AAA 패턴">
        <p>
          가장 작은 단위부터 시작합니다. <strong>단위 테스트(unit test)</strong>는 함수 하나를
          떼어 놓고 "이 입력을 주면 이 출력이 나와야 한다"를 확인하는 테스트예요. 첫 대상으로는{' '}
          <strong>순수 함수</strong> — DB도 네트워크도 안 건드리고 입력만으로 출력이 정해지는
          함수 — 가 제일 좋습니다. 준비할 게 없어서 테스트가 짧고 빠르거든요.
        </p>
        <Code>{CODE_FIRST_TEST}</Code>
        <p>
          테스트 본문의 세 줄을 보세요. <strong>① 준비(Arrange)</strong> — 입력을 만들고,{' '}
          <strong>② 실행(Act)</strong> — 대상 함수를 부르고, <strong>③ 검증(Assert)</strong> —
          결과가 기대와 같은지 <span className="icode">expect</span>로 단언합니다. 이{' '}
          <strong>AAA 패턴</strong>은 앞으로 만나는 모든 테스트 — API든 컴포넌트든 — 의 공통
          뼈대예요. 테스트가 읽기 어렵다면 십중팔구 이 세 단계가 뒤엉켜 있는 겁니다.
        </p>
        <Code>{CODE_RUN}</Code>
        <p>
          테스트 <strong>이름</strong>도 기술입니다. <span className="icode">'test1'</span>이나{' '}
          <span className="icode">'works'</span>가 아니라 <strong>"무엇이 어때야 한다"</strong> —
          "올바른 이메일이면 true를 돌려준다"처럼 쓰세요. 그래야 실패했을 때 코드를 열어 보지
          않아도 <strong>어떤 약속이 깨졌는지</strong> 결과 화면만으로 읽힙니다. 섹션 1에서 말한
          '실행 가능한 명세서'는 바로 이 이름들의 목록이에요.
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 일부러 <span className="icode">isValidEmail</span>의{' '}
          <span className="icode">at &gt; 0</span>을 <span className="icode">at &gt;= 0</span>으로
          바꿔 보세요. 저장하는 순간 감시 모드의 테스트가 빨간불로 바뀝니다. 이 "고장 내면 즉시
          알아챈다"는 감각이 테스트의 본질입니다 — 초록불만 보다 끝나면 테스트를 배운 게 아니에요.
        </div>
      </Section>

      <Section n={3} title="무엇을 테스트할 것인가" sub="정상 경로보다 경계값·실패 케이스가 값지다">
        <p>
          도구를 익혔으니 이제 <strong>과녁</strong>을 고를 차례예요. 초보의 테스트는 대부분
          "잘 되는 경우"만 확인하고 끝납니다. 하지만 잘 되는 경우는 개발하면서 이미 수십 번
          눌러 봤잖아요? 버그는 <strong>가장자리</strong>에서 태어납니다 — 0%와 100%, 빈 문자열,
          음수, 딱 경계에 걸친 값. 테스트의 가성비는 여기서 나와요.
        </p>
        <Code>{CODE_EDGE}</Code>
        <p>
          어디부터 쓸지 막막하면 우선순위는 이렇습니다. <strong>① 검증 함수</strong>(이메일·비밀번호
          규칙 — 틀리면 가입이 막히거나 뚫린다), <strong>② 돈 계산</strong>(할인·수수료 — 틀리면
          실제 손해), <strong>③ 날짜·상태 전환 로직</strong>(마감 판정 같은 것). 공통점은{' '}
          <strong>틀렸을 때 아픈 순수 로직</strong>이라는 거예요. 화면 꾸미기 코드보다 이런 곳의
          테스트 한 개가 열 배 값집니다.
        </p>
        <p>
          그리고 한 가지 원칙 — <strong>구현이 아니라 동작을 테스트</strong>하세요. "내부에서
          <span className="icode"> indexOf</span>를 호출한다"를 검증하면, 나중에 정규식으로
          리팩터링하는 순간 <strong>기능은 멀쩡한데 테스트만 깨집니다</strong>. 안전망이어야 할
          테스트가 리팩터링을 방해하는 족쇄가 되는 거죠. 테스트는 바깥에서 보이는 약속 —
          입력과 출력 — 에만 붙이세요.
        </p>
        <div className="tip">
          <b>고르는 눈</b> 새 함수를 짰다면 스스로에게 물어보세요 — "이 함수가{' '}
          <strong>제일 이상한 입력</strong>을 받는다면 뭘까?" 그 답(빈 값, 음수, 한계값, 잘못된
          타입)이 곧 다음에 쓸 테스트 목록입니다. 정상 1개 + 경계 2개 + 실패 1개, 이 4종 세트를
          기본기로 삼으면 됩니다.
        </div>
      </Section>

      <Section n={4} title="API 통합 테스트" sub="supertest로 실제 요청·응답을 검증한다">
        <p>
          단위 테스트가 부품 검사라면, <strong>통합 테스트</strong>는 조립된 기계를 돌려 보는
          검사예요. 라우터·미들웨어·DB까지 이어진 <strong>진짜 요청↔응답</strong>을 확인합니다.
          Node 서버에서는 <strong><span className="icode">supertest</span></strong>가 표준 도구예요 —
          서버를 실제 포트에 띄우지 않고도 앱 객체에 HTTP 요청을 그대로 흉내 내 줍니다.
        </p>
        <Code>{CODE_SUPERTEST}</Code>
        <p>
          검증 대상은 세 층입니다. <strong>① 상태 코드</strong> — 200·401·409처럼 "무슨 일이
          일어났는지"의 요약, <strong>② 응답 바디</strong> — 토큰이 있는지, 실패했는데 있으면 안
          될 값이 <strong>새어 나오진 않는지</strong>, <strong>③ 헤더</strong> —{' '}
          <span className="icode">content-type</span> 같은 형식 약속. 특히 "틀린 비밀번호인데
          토큰이 <span className="icode">undefined</span>인지"처럼 <strong>없어야 할 것</strong>을
          확인하는 단언은 보안 사고를 미리 막는 테스트입니다.
        </p>
        <p>
          로그인이 필요한 API는 어떻게 할까요? 테스트 안에서 <strong>세션을 직접 만들면</strong>{' '}
          됩니다 — 준비 단계에서 로그인 API를 먼저 불러 토큰을 받고, 그 토큰을 붙여 보호된 API를
          부르는 거죠. AAA 패턴의 '준비'가 조금 길어질 뿐, 뼈대는 똑같습니다.
        </p>
        <Code>{CODE_AUTH_FLOW}</Code>
        <div className="tip">
          <b>실습 예고</b> 이 코스의 실습 과제 — 로그인 API의 <strong>성공 · 비밀번호 틀림 ·
          중복 가입</strong> 시나리오 — 가 바로 이 섹션의 패턴 그대로예요. 위 코드에서 상태
          코드(200 · 401 · 409)와 바디 검증을 여러분의 라우트에 맞게 옮겨 쓰는 것부터 시작하세요.
        </div>
      </Section>

      <Section n={5} title="테스트용 DB 다루기" sub="분리 · 초기화 · 시드 — 독립성이 전부다">
        <p>
          API 테스트가 DB를 건드리는 순간 새 문제가 생깁니다. 개발하며 쓰던 DB에 테스트를 돌리면{' '}
          <strong>테스트가 내 데이터를 지우고</strong>, 반대로 내가 넣어 둔 데이터 때문에{' '}
          <strong>테스트 결과가 그날그날 달라져요</strong>. 그래서 철칙 하나 —{' '}
          <strong>테스트는 자기 전용 DB를 본다.</strong>{' '}
          <span className="icode">mirim_dev</span>와 별개로 <span className="icode">mirim_test</span>를
          만들고, 테스트 실행 때만 그쪽을 가리키게 합니다.
        </p>
        <Code>{CODE_TESTDB}</Code>
        <p>
          두 번째 철칙은 <strong>독립성</strong>이에요. 모든 테스트는{' '}
          <span className="icode">beforeEach</span>에서 <strong>초기화(비우기) → 시드(기본 데이터
          심기)</strong>를 거쳐 항상 같은 출발점에서 시작해야 합니다. 그래야 어떤 테스트든{' '}
          <strong>혼자 돌려도, 어떤 순서로 돌려도</strong> 같은 결과가 나와요. 섹션 4의 중복 가입
          테스트가 늘 성공하는 이유도, 매번 users 테이블이 깨끗한 상태에서 시작하기 때문입니다.
        </p>
        <p>
          거꾸로 말하면 — "테스트 A를 먼저 돌려야 B가 통과한다" 같은 <strong>순서 의존</strong>이
          생겼다면 그건 위험 신호예요. 지금은 우연히 초록불이지만, 테스트를 병렬로 돌리거나 하나만
          골라 돌리는 순간 무너집니다. 위 코드 주석의 체크리스트 세 줄이 그 병의 대표 증상이니,
          보이면 바로 시드부터 점검하세요.
        </p>
        <div className="tip">
          <b>비유</b> 좋은 테스트는 <strong>연극 무대</strong>와 같아요. 매 공연(테스트) 전에
          무대를 싹 치우고(초기화) 소품을 제자리에 배치합니다(시드). 앞 공연이 어질러 놓은 무대에서
          다음 공연을 시작하면, 배우(코드)는 그대로인데 공연만 이상하게 망가지죠.
        </div>
      </Section>

      <Section n={6} title="React 컴포넌트 테스트" sub="Testing Library — 사용자가 보는 대로 테스트한다">
        <p>
          마지막 조각은 화면이에요. <strong>Testing Library</strong>의 철학은 한 문장입니다 —{' '}
          <strong>"사용자가 쓰는 방식대로 테스트하라."</strong> 사용자는 컴포넌트의 state나 CSS
          클래스를 몰라요. 그저 '가입하기'라고 적힌 <strong>버튼</strong>을 보고, '이메일'이라는{' '}
          <strong>라벨</strong>이 붙은 칸에 입력하죠. 테스트도 똑같이 찾고, 똑같이 행동해야 합니다.
        </p>
        <Code>{CODE_RTL}</Code>
        <p>
          요소를 찾는 기본기는 두 가지입니다. <strong><span className="icode">getByRole</span></strong> —
          "버튼·제목·링크 같은 <strong>역할</strong> + 보이는 이름"으로 찾기,{' '}
          <strong><span className="icode">getByLabelText</span></strong> — 입력 칸을{' '}
          <strong>라벨 텍스트</strong>로 찾기. 이 둘이 잘 안 통하는 화면은 대개 스크린 리더
          사용자에게도 불친절한 화면이에요 — 접근성과 테스트 용이성은 같은 뿌리에서 나옵니다.
        </p>
        <p>
          흐름은 역시 AAA예요. <span className="icode">render</span>로 그리고(준비),{' '}
          <span className="icode">userEvent</span>로 클릭·입력하고(실행), "화면에 이 문구가
          보이는가"를 단언합니다(검증). 주의할 점은 <span className="icode">userEvent</span>가{' '}
          <strong>비동기</strong>라는 것 — <span className="icode">await</span>를 빼먹으면
          이벤트가 끝나기 전에 검증이 먼저 달려서 엉뚱하게 실패합니다.
        </p>
        <div className="tip">
          <b>실습 예고</b> 실습 과제의 나머지 절반 — 회원가입 폼의 <strong>검증 에러 표시</strong> —
          가 이 패턴입니다. "빈 채로 제출하면 에러 문구가 보인다", "짧은 비밀번호면 안내가 보인다"
          같은 시나리오를 <span className="icode">getByRole</span>·
          <span className="icode">getByLabelText</span>만으로 써 보세요. 클래스 이름으로 찾고 싶어지는
          순간이 오면, 그게 바로 '구현에 붙은 테스트'로 미끄러지는 순간입니다.
        </div>
      </Section>

      <Section n={7} title="지속 가능한 테스트 습관" sub="숫자보다 시나리오, 손보다 자동화">
        <p>
          도구와 과녁을 다 익혔으니, 마지막은 <strong>오래가는 습관</strong>이에요. 먼저 흔한 함정
          하나 — <strong>커버리지 100%</strong>. 커버리지는 "코드의 몇 %가 테스트 중에 실행됐나"라는
          숫자일 뿐, <strong>제대로 검증했는가</strong>와는 다릅니다. 단언 없이 함수만 호출해도
          커버리지는 오르거든요. 숫자를 채우려 억지 테스트를 늘리는 것보다, <strong>아픈 시나리오
          10개</strong>가 확실히 초록인 쪽이 훨씬 믿을 만합니다.
        </p>
        <p>
          두 번째 함정은 <strong>깨지기 쉬운 테스트</strong>예요. 대표가 snapshot 남용 — 화면
          전체를 통째로 사진 찍어 비교하는 테스트는, 여백 한 줄만 바꿔도 깨집니다. 그러면 개발자는
          내용을 보지 않고 <strong>습관적으로 스냅샷을 갱신</strong>하게 되고, 그 순간 테스트는
          아무것도 지키지 않는 장식이 돼요. 자주 깨지는데 버그는 못 잡는 테스트가 보이면, 고치지
          말고 <strong>시나리오 테스트로 다시 쓰세요</strong>.
        </p>
        <Code>{CODE_HABIT}</Code>
        <p>
          마지막으로, 테스트는 <strong>사람의 기억이 아니라 흐름에</strong> 심어야 삽니다.
          "커밋 전에 꼭 돌려야지"는 반드시 잊게 돼요. 커밋 훅에서 자동으로 돌고, CI에서{' '}
          <span className="icode">vitest run</span>이 초록이어야만 병합되게 만들면 — 깨진 코드가
          합쳐지는 사고를 <strong>사람의 성실함이 아니라 구조가</strong> 막아 줍니다.
        </p>
        <div className="tip">
          <b>졸업 기준</b> 이 코스를 제대로 통과했다면, 이제 리팩터링 요청이 두렵지 않아야 해요.
          "일단 테스트부터 돌려 보고요" — 이 한마디가 자연스럽게 나오면 됩니다. 실습 과제(로그인
          API 3종 + 회원가입 폼 시나리오, 합계 10개)를 전부 초록으로 만들고 다음 강좌로 넘어가세요.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🧪 여기까지 왔다면</h3>
          <p className="muted">
            이제 여러분에게는 <strong>안전망</strong>이 생겼어요. 순수 함수를 AAA로 검증하고,
            경계값과 실패 케이스를 먼저 노리고, supertest로 API의 약속을 붙들고, 격리된 테스트
            DB 위에서 독립적으로 돌리고, 사용자가 보는 대로 컴포넌트를 확인하는 — 그리고 이 모두를
            커밋과 CI의 흐름에 심는 힘. 코드를 고치는 손이 더는 떨리지 않죠. 그럼 이 자신감으로
            무엇을 할까요? 처음부터 끝까지 — 설계하고, 만들고, 테스트하고, 세상에 내놓는 것까지.
            다음 <Link to="/learn/lv2-mini-project-deploy"><strong>미니 프로젝트: 설계부터
            배포까지</strong></Link> 강좌에서, 지금까지 배운 조각들을 하나의 진짜 서비스로
            완성해 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
