// 이 파일이 하는 일: "SQL 중급" 코스 — SELECT를 뗀 사람이 다음으로 만나는 벽,
// 집계(GROUP BY)·HAVING·서브쿼리·NULL·인덱스·EXPLAIN·트랜잭션을 8개 섹션으로 안내하는 정적 학습 페이지.
// (GROUP BY 집계 → HAVING → 서브쿼리 → NULL의 함정 → 인덱스 → EXPLAIN → 트랜잭션/ACID → 종합 실습)
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
// SQL 예제는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
// 선수 코스: SQL 입문 (SELECT, WHERE, ORDER BY를 알고 있다고 가정)

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

// ── SQL 예제 · 결과 표 상수들 ──

const CODE_GROUP_BY = `-- 우리 학습 플랫폼 DB의 제출 테이블(submission)이 이렇게 생겼다고 해요:
--   id | student_id | week | score | submitted_at

-- "주차별로 제출이 몇 건이었지?" — GROUP BY 없이는 못 세요.
SELECT week, COUNT(*) AS 제출수, AVG(score) AS 평균점수
FROM submission
GROUP BY week
ORDER BY week;

 week | 제출수 | 평균점수
------+--------+----------
    1 |     28 |     87.5
    2 |     26 |     82.1
    3 |     19 |     79.3   ← 3주차부터 제출이 줄고 있네요?!

-- GROUP BY week = "week 값이 같은 행끼리 한 팀으로 묶어라"
-- COUNT(*)     = 팀마다 행이 몇 개인지
-- AVG(score)   = 팀마다 score의 평균`;

const CODE_GROUP_BY_RULE = `-- GROUP BY의 철칙: SELECT에는 '묶은 기준 컬럼'과 '집계 함수'만!

SELECT week, student_id, COUNT(*)   -- ❌ 에러!
FROM submission
GROUP BY week;

-- ERROR: column "submission.student_id" must appear in the
--        GROUP BY clause or be used in an aggregate function

-- 왜? 1주차 팀에는 학생이 28명 들어 있는데,
-- "1주차의 student_id"라고 하면 28명 중 누구를 보여 달라는 건지
-- DB가 알 수 없기 때문이에요. 반 전체를 한 줄로 요약하면서
-- "그 줄의 이름은?" 하고 묻는 것과 같아요 — 답이 하나가 아니죠.`;

const CODE_HAVING = `-- "제출이 20건 넘는 주차만 보여줘" — 묶은 결과를 거르고 싶다면 HAVING!

SELECT week, COUNT(*) AS 제출수
FROM submission
WHERE score IS NOT NULL      -- ① 묶기 전에 행을 거르는 건 WHERE
GROUP BY week                -- ② 묶고
HAVING COUNT(*) > 20         -- ③ 묶은 '팀'을 거르는 건 HAVING
ORDER BY week;

WHERE  = 입장 검사 (팀을 만들기 전에 한 명씩 거름)
HAVING = 팀 심사   (팀이 다 만들어진 뒤 팀 단위로 거름)

-- WHERE COUNT(*) > 20 이라고 쓰면 에러예요.
-- 입장 검사 시점에는 아직 팀이 없어서 팀원 수를 셀 수 없거든요.`;

const CODE_SUBQUERY = `-- "평균보다 점수가 높은 제출만 보고 싶다"
-- 평균이 몇 점인지 먼저 알아야겠죠? 쿼리 안에 쿼리를 넣으면 됩니다.

SELECT student_id, week, score
FROM submission
WHERE score > (SELECT AVG(score) FROM submission);
--            └── 이 괄호가 먼저 실행돼 숫자 하나(예: 83.2)가 되고,
--                바깥 쿼리는 WHERE score > 83.2 처럼 동작해요.

-- IN과 함께 쓰면 "목록" 서브쿼리도 가능:
-- "3주차에 제출한 학생들의 모든 제출을 보여줘"
SELECT * FROM submission
WHERE student_id IN (SELECT student_id FROM submission WHERE week = 3);

-- 요리로 치면 서브쿼리는 '미리 만들어 두는 육수'예요.
-- 본 요리(바깥 쿼리)를 시작하기 전에 먼저 우려내서 재료로 씁니다.`;

const CODE_NULL = `-- NULL = "값이 없음"이 아니라 "모름". 이 차이가 사고를 부릅니다.

SELECT NULL = NULL;    -- 결과: NULL (true가 아님!)
-- "모르는 값"과 "모르는 값"이 같은지는... 그것도 모르죠.

-- 함정 ①: = 로는 NULL을 못 잡아요
SELECT * FROM submission WHERE score = NULL;     -- ❌ 항상 0건
SELECT * FROM submission WHERE score IS NULL;    -- ⭕ 미채점 제출 조회

-- 함정 ②: 집계 함수는 NULL을 조용히 건너뜁니다
--   제출 5건, score가 (90, 80, NULL, 70, NULL)일 때
SELECT COUNT(*)     FROM submission;   -- 5  (행 전부)
SELECT COUNT(score) FROM submission;   -- 3  (NULL 제외!)
SELECT AVG(score)   FROM submission;   -- 80 (240÷3, 240÷5가 아님!)

-- 함정 ③: NULL이 낀 계산은 통째로 NULL
SELECT 100 + NULL;   -- NULL. "100 + 모름 = 모름"

-- 구급약: COALESCE(값, 대체값) — NULL이면 대체값을 쓴다
SELECT COALESCE(score, 0) FROM submission;   -- 미채점을 0점으로 취급`;

const CODE_INDEX = `-- 인덱스 = 책 뒤의 '찾아보기'

인덱스 없이 검색           인덱스로 검색
──────────────────       ──────────────────
책을 1쪽부터 끝까지        찾아보기에서 "트랜잭션 → 214쪽"
한 장씩 넘기며 확인        확인하고 그 쪽으로 바로 이동
(풀 스캔, Seq Scan)       (Index Scan)

-- 학생 번호로 제출을 자주 찾는다면:
CREATE INDEX idx_submission_student ON submission (student_id);

-- 이제 WHERE student_id = 7 검색이 수십만 행에서도 즉시!

단, 공짜가 아니에요:
1) 인덱스도 디스크를 차지한다 (찾아보기 페이지만큼 책이 두꺼워짐)
2) INSERT/UPDATE 때마다 인덱스도 고쳐야 해서 쓰기가 느려짐
   (본문을 고칠 때마다 찾아보기도 다시 만드는 셈)

→ "자주 검색하는 컬럼에만" 만드는 게 원칙.
  모든 컬럼에 인덱스를 걸면 읽기 조금 빨라지고 쓰기는 확 느려져요.`;

const CODE_EXPLAIN = `-- 내 쿼리가 인덱스를 타는지 안 타는지, DB에게 직접 물어보세요.
-- 쿼리 앞에 EXPLAIN만 붙이면 '실행 계획서'를 보여줍니다.

EXPLAIN SELECT * FROM submission WHERE student_id = 7;

-- 인덱스가 없을 때:
Seq Scan on submission  (cost=0.00..431.00 rows=12 ...)
  Filter: (student_id = 7)
  → "처음부터 끝까지 다 읽겠음" (책 한 장씩 넘기기)

-- CREATE INDEX 후 다시 실행하면:
Index Scan using idx_submission_student on submission (...)
  Index Cond: (student_id = 7)
  → "찾아보기로 바로 가겠음"

-- cost의 두 번째 숫자(431.00)가 대략적인 '예상 수고'예요.
-- 쿼리가 느릴 때 제일 먼저 할 일 = EXPLAIN 붙여서 Seq Scan 찾기!`;

const CODE_TRANSACTION = `-- 트랜잭션 = "전부 성공 아니면 전부 취소"로 묶는 작업 단위

-- 예: 제출을 옮기는 두 문장이 '한 몸'이어야 한다면
BEGIN;                                          -- 트랜잭션 시작
UPDATE submission SET week = 4 WHERE id = 101;
UPDATE submission SET week = 4 WHERE id = 102;
COMMIT;                                         -- 둘 다 확정!
-- 중간에 문제가 생기면 COMMIT 대신:
ROLLBACK;                                       -- 둘 다 없던 일로

ACID — 트랜잭션이 지키는 4가지 약속
────────────────────────────────────
A tomicity   원자성: 전부 되거나 전부 안 되거나 (반만 없음)
C onsistency 일관성: 끝난 뒤에도 규칙(제약조건)이 안 깨짐
I solation   격리성: 동시에 실행돼도 서로 안 섞임
D urability  지속성: COMMIT했으면 정전이 나도 남는다

-- 계좌이체가 단골 비유죠: "내 돈 빠짐 + 친구 돈 늘어남"이
-- 반쪽만 실행되면 대참사. 그래서 은행은 전부 트랜잭션입니다.`;

const CODE_PRACTICE = `-- 미션: "학생별 제출 수" 집계 쿼리를 완성하세요.
-- 조건: ① 채점 완료(score가 NULL 아님)만 센다
--       ② 제출 3건 이상인 학생만 보여준다
--       ③ 제출 많은 순으로 정렬

SELECT student_id,
       ______(*) AS 제출수,          -- (a) 세는 함수는?
       ROUND(AVG(score), 1) AS 평균점수
FROM submission
WHERE score IS ______ NULL           -- (b) NULL이 '아님'은?
GROUP BY ______                      -- (c) 무엇으로 묶어야 할까?
______ COUNT(*) >= 3                 -- (d) 묶은 뒤 거르는 키워드는?
ORDER BY 제출수 DESC;

-- 기대 결과 (여러분 DB의 실제 숫자와는 다를 수 있어요):
 student_id | 제출수 | 평균점수
------------+--------+----------
          3 |      5 |     91.2
          7 |      4 |     85.0
         12 |      3 |     78.6`;

const CODE_PRACTICE_ANSWER = `-- 정답 (풀어 본 뒤에만 펼쳐 보기!):
--   (a) COUNT   (b) NOT   (c) student_id   (d) HAVING

SELECT student_id,
       COUNT(*) AS 제출수,
       ROUND(AVG(score), 1) AS 평균점수
FROM submission
WHERE score IS NOT NULL
GROUP BY student_id
HAVING COUNT(*) >= 3
ORDER BY 제출수 DESC;

-- 여기까지 술술 읽힌다면, 이 한 문장에
-- 오늘 배운 집계·NULL·HAVING이 전부 들어 있다는 걸 눈치챘을 거예요.`;

const CODE_PSQL = `# 우리 플랫폼처럼 Docker로 PostgreSQL을 띄웠다면, 컨테이너 안 psql로 접속:
docker exec -it <postgres컨테이너이름> psql -U <사용자> -d <DB이름>

# 접속되면 프롬프트가 이렇게 바뀌어요:
mydb=#

# 몸풀기 — 테이블 목록 보기:
\\dt

# 오늘 배운 것 바로 실험:
EXPLAIN SELECT * FROM submission WHERE student_id = 7;

# 나갈 때는:
\\q`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: 'GROUP BY와 집계' },
  { n: 2, label: 'HAVING' },
  { n: 3, label: '서브쿼리 맛보기' },
  { n: 4, label: 'NULL의 함정' },
  { n: 5, label: '인덱스' },
  { n: 6, label: 'EXPLAIN' },
  { n: 7, label: '트랜잭션과 ACID' },
  { n: 8, label: '종합 실습' },
];

export default function SqlIntermediatePage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 서버와 데이터</div>
        <h1>SQL 중급<br />— 세고, 묶고, 빨라지기</h1>
        <p>
          SELECT로 데이터를 <strong>꺼내는</strong> 법을 배웠다면, 이제
          <strong> 요약하고(집계) 빠르게 만드는(인덱스)</strong> 법을 배울 차례예요.
          우리 학습 플랫폼의 제출(submission) 데이터를 예제 삼아, 실무에서 매일 쓰는
          일곱 가지 무기를 하나씩 손에 쥐어 봅니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 90분</span>
          <span className="chip">실습 2개 포함</span>
          <span className="chip">선수: SQL 입문</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="GROUP BY와 집계 함수" sub="행을 '팀'으로 묶어 요약하기">
        <p>
          SQL 입문에서 배운 SELECT는 행을 <strong>한 줄씩 그대로</strong> 보여줬어요.
          그런데 실무에서 진짜 자주 받는 질문은 이런 거예요 — <strong>"주차별 제출이 몇 건이야?"</strong>,
          "학생별 평균 점수는?". 행 하나하나가 아니라 <strong>묶음의 요약</strong>을 묻는 질문이죠.
          이때 쓰는 게 <span className="icode">GROUP BY</span>와 집계 함수입니다.
        </p>
        <p>
          반 전체 시험지를 <strong>주차별로 쌓아 나눈 다음</strong>, 더미마다 "몇 장?"(COUNT),
          "평균 몇 점?"(AVG) 하고 포스트잇 한 장씩 붙이는 장면을 상상하세요.
          시험지 수십 장이 더미당 <strong>요약 한 줄</strong>로 바뀝니다.
        </p>
        <svg className="diagram" viewBox="0 0 640 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="GROUP BY 동작 도해: 섞여 있는 행들이 week 값이 같은 행끼리 팀으로 묶이고, 팀마다 COUNT와 AVG로 요약 한 줄이 만들어지는 과정">
          <text x="95" y="30" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">① submission 행들</text>
          <text x="325" y="30" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">② week가 같은 행끼리 묶기</text>
          <text x="540" y="30" textAnchor="middle" fontSize="13" fontWeight="600" fill="var(--ink)">③ 팀마다 요약 한 줄</text>
          <text x="540" y="46" textAnchor="middle" fontSize="12" fill="var(--muted)">COUNT(*) · AVG(score)</text>
          <line x1="167" y1="62" x2="246" y2="86" stroke="var(--primary)" strokeWidth="1.3" opacity="0.65" />
          <line x1="167" y1="92" x2="246" y2="171" stroke="var(--teal)" strokeWidth="1.3" opacity="0.65" />
          <line x1="167" y1="122" x2="246" y2="86" stroke="var(--primary)" strokeWidth="1.3" opacity="0.65" />
          <line x1="167" y1="152" x2="246" y2="248" stroke="var(--amber)" strokeWidth="1.3" opacity="0.65" />
          <line x1="167" y1="182" x2="246" y2="171" stroke="var(--teal)" strokeWidth="1.3" opacity="0.65" />
          <line x1="167" y1="212" x2="246" y2="86" stroke="var(--primary)" strokeWidth="1.3" opacity="0.65" />
          <rect x="25" y="50" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="62" r="5" fill="var(--primary)" />
          <text x="54" y="66" fontSize="12" fill="var(--ink)">week 1 · 90점</text>
          <rect x="25" y="80" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="92" r="5" fill="var(--teal)" />
          <text x="54" y="96" fontSize="12" fill="var(--ink)">week 2 · 85점</text>
          <rect x="25" y="110" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="122" r="5" fill="var(--primary)" />
          <text x="54" y="126" fontSize="12" fill="var(--ink)">week 1 · 78점</text>
          <rect x="25" y="140" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="152" r="5" fill="var(--amber)" />
          <text x="54" y="156" fontSize="12" fill="var(--ink)">week 3 · 92점</text>
          <rect x="25" y="170" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="182" r="5" fill="var(--teal)" />
          <text x="54" y="186" fontSize="12" fill="var(--ink)">week 2 · 80점</text>
          <rect x="25" y="200" width="140" height="24" rx="6" fill="var(--card)" stroke="var(--line)" />
          <circle cx="42" cy="212" r="5" fill="var(--primary)" />
          <text x="54" y="216" fontSize="12" fill="var(--ink)">week 1 · 88점</text>
          <text x="95" y="242" textAnchor="middle" fontSize="14" fill="var(--muted)">…</text>
          <rect x="250" y="48" width="150" height="76" rx="8" fill="var(--card)" stroke="var(--primary)" strokeDasharray="5 4" />
          <text x="262" y="66" fontSize="12.5" fontWeight="600" fill="var(--ink)">week = 1 팀</text>
          <rect x="262" y="74" width="100" height="8" rx="3" fill="var(--line)" />
          <rect x="262" y="86" width="100" height="8" rx="3" fill="var(--line)" />
          <rect x="262" y="98" width="100" height="8" rx="3" fill="var(--line)" />
          <text x="262" y="118" fontSize="12" fill="var(--muted)">… 총 28행</text>
          <rect x="250" y="139" width="150" height="64" rx="8" fill="var(--card)" stroke="var(--teal)" strokeDasharray="5 4" />
          <text x="262" y="157" fontSize="12.5" fontWeight="600" fill="var(--ink)">week = 2 팀</text>
          <rect x="262" y="165" width="100" height="8" rx="3" fill="var(--line)" />
          <rect x="262" y="177" width="100" height="8" rx="3" fill="var(--line)" />
          <text x="262" y="196" fontSize="12" fill="var(--muted)">… 총 26행</text>
          <rect x="250" y="218" width="150" height="60" rx="8" fill="var(--card)" stroke="var(--amber)" strokeDasharray="5 4" />
          <text x="262" y="236" fontSize="12.5" fontWeight="600" fill="var(--ink)">week = 3 팀</text>
          <rect x="262" y="244" width="100" height="8" rx="3" fill="var(--line)" />
          <rect x="262" y="256" width="100" height="8" rx="3" fill="var(--line)" />
          <text x="262" y="273" fontSize="12" fill="var(--muted)">… 총 19행</text>
          <line x1="402" y1="86" x2="440" y2="86" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="438,81 448,86 438,91" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <line x1="402" y1="171" x2="440" y2="171" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="438,166 448,171 438,176" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <line x1="402" y1="248" x2="440" y2="248" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="438,243 448,248 438,253" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <rect x="455" y="68" width="170" height="36" rx="8" fill="var(--card)" stroke="var(--primary)" strokeWidth="1.5" />
          <text x="467" y="84" fontSize="12.5" fontWeight="600" fill="var(--ink)">week 1 — 28건</text>
          <text x="467" y="98" fontSize="12" fill="var(--muted)">평균 87.5점</text>
          <rect x="455" y="153" width="170" height="36" rx="8" fill="var(--card)" stroke="var(--teal)" strokeWidth="1.5" />
          <text x="467" y="169" fontSize="12.5" fontWeight="600" fill="var(--ink)">week 2 — 26건</text>
          <text x="467" y="183" fontSize="12" fill="var(--muted)">평균 82.1점</text>
          <rect x="455" y="230" width="170" height="36" rx="8" fill="var(--card)" stroke="var(--amber)" strokeWidth="1.5" />
          <text x="467" y="246" fontSize="12.5" fontWeight="600" fill="var(--ink)">week 3 — 19건</text>
          <text x="467" y="260" fontSize="12" fill="var(--muted)">평균 79.3점</text>
          <text x="320" y="346" textAnchor="middle" fontSize="12" fill="var(--muted)">행 73개가 요약 3줄로 — 이게 GROUP BY의 전부예요</text>
        </svg>
        <p className="muted" style={{ fontSize: 12.5, textAlign: 'center', marginTop: 2 }}>그림: GROUP BY week — 행들이 week별 팀으로 묶이고, 팀마다 요약 한 줄이 된다</p>
        <Code>{CODE_GROUP_BY}</Code>
        <p>
          자주 쓰는 집계 함수는 다섯 개예요 — <span className="icode">COUNT</span>(개수),{' '}
          <span className="icode">AVG</span>(평균), <span className="icode">SUM</span>(합계),{' '}
          <span className="icode">MAX</span>/<span className="icode">MIN</span>(최대/최소).
          그리고 GROUP BY에는 초보자의 8할이 걸려 넘어지는 철칙이 하나 있습니다.
        </p>
        <Code>{CODE_GROUP_BY_RULE}</Code>
        <div className="tip">
          <b>기억법</b> GROUP BY를 쓰는 순간, 결과의 한 줄은 더 이상 '행'이 아니라
          <strong> '팀의 요약'</strong>이에요. SELECT에 쓸 수 있는 건 팀 이름(묶은 컬럼)과
          팀 성적표(집계 함수)뿐 — 팀원 개인 정보(그 외 컬럼)는 못 씁니다.
        </div>
      </Section>

      <Section n={2} title="HAVING" sub="묶은 '팀'을 거르는 두 번째 필터">
        <p>
          "제출 수가 20건 넘는 주차<strong>만</strong>" 보고 싶으면 어떻게 할까요?
          <span className="icode"> WHERE COUNT(*) &gt; 20</span>을 떠올렸다면 아주 자연스러운
          생각인데, 아쉽게도 에러가 납니다. WHERE는 <strong>묶기 전</strong>에 행 하나하나를
          검사하는 입장 검사라서, 그 시점엔 아직 셀 팀이 없거든요.
          팀을 만든 <strong>뒤에</strong> 팀 단위로 거르는 문이 따로 있어요 —{' '}
          <span className="icode">HAVING</span>입니다.
        </p>
        <Code>{CODE_HAVING}</Code>
        <p>
          정리하면 실행 순서는 <strong>WHERE(행 거르기) → GROUP BY(묶기) →
          HAVING(팀 거르기) → ORDER BY(정렬)</strong>. 동아리 오디션으로 치면
          WHERE는 <strong>지원 자격 심사</strong>(개인별), HAVING은 팀을 짠 뒤의{' '}
          <strong>팀 예선</strong>(팀별)이에요. 심사 대상이 다르니 문도 다른 거죠.
        </p>
        <svg className="diagram" viewBox="0 0 640 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="WHERE, GROUP BY, HAVING, ORDER BY 실행 순서 도해: 행을 먼저 거르고, week별 팀으로 묶고, 팀 단위로 거른 뒤 정렬한다">
          <text x="80" y="48" textAnchor="middle" fontSize="12.5" fontWeight="600" fill="var(--ink)">모든 행</text>
          <circle cx="60" cy="72" r="6" fill="var(--primary)" />
          <circle cx="80" cy="72" r="6" fill="var(--primary)" />
          <circle cx="100" cy="72" r="6" fill="var(--rose)" />
          <circle cx="60" cy="94" r="6" fill="var(--primary)" />
          <circle cx="80" cy="94" r="6" fill="var(--rose)" />
          <circle cx="100" cy="94" r="6" fill="var(--primary)" />
          <text x="80" y="130" textAnchor="middle" fontSize="12" fill="var(--rose)">붉은 점 = score NULL</text>
          <text x="240" y="48" textAnchor="middle" fontSize="12.5" fontWeight="600" fill="var(--ink)">걸러진 행들</text>
          <circle cx="228" cy="72" r="6" fill="var(--primary)" />
          <circle cx="252" cy="72" r="6" fill="var(--primary)" />
          <circle cx="228" cy="94" r="6" fill="var(--primary)" />
          <circle cx="252" cy="94" r="6" fill="var(--primary)" />
          <text x="240" y="130" textAnchor="middle" fontSize="12" fill="var(--muted)">NULL 행 탈락</text>
          <text x="400" y="48" textAnchor="middle" fontSize="12.5" fontWeight="600" fill="var(--ink)">팀 3개</text>
          <rect x="358" y="56" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--primary)" />
          <text x="400" y="69" textAnchor="middle" fontSize="12" fill="var(--ink)">1주 · 28행</text>
          <rect x="358" y="78" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--teal)" />
          <text x="400" y="91" textAnchor="middle" fontSize="12" fill="var(--ink)">2주 · 26행</text>
          <rect x="358" y="100" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--amber)" />
          <text x="400" y="113" textAnchor="middle" fontSize="12" fill="var(--ink)">3주 · 19행</text>
          <text x="400" y="130" textAnchor="middle" fontSize="12" fill="var(--muted)">week가 같은 행끼리</text>
          <text x="560" y="48" textAnchor="middle" fontSize="12.5" fontWeight="600" fill="var(--ink)">팀 2개</text>
          <rect x="518" y="56" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--primary)" />
          <text x="560" y="69" textAnchor="middle" fontSize="12" fill="var(--ink)">1주 · 28행</text>
          <rect x="518" y="78" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--teal)" />
          <text x="560" y="91" textAnchor="middle" fontSize="12" fill="var(--ink)">2주 · 26행</text>
          <rect x="518" y="100" width="84" height="18" rx="4" fill="var(--card)" stroke="var(--rose)" strokeDasharray="4 3" />
          <text x="560" y="113" textAnchor="middle" fontSize="12" fill="var(--muted)">3주 · 19행</text>
          <line x1="520" y1="109" x2="600" y2="109" stroke="var(--rose)" strokeWidth="1.5" />
          <text x="560" y="130" textAnchor="middle" fontSize="12" fill="var(--rose)">COUNT 20 이하 탈락</text>
          <line x1="80" y1="136" x2="80" y2="146" stroke="var(--line)" strokeDasharray="3 3" />
          <line x1="240" y1="136" x2="240" y2="146" stroke="var(--line)" strokeDasharray="3 3" />
          <line x1="400" y1="136" x2="400" y2="146" stroke="var(--line)" strokeDasharray="3 3" />
          <line x1="560" y1="136" x2="560" y2="146" stroke="var(--line)" strokeDasharray="3 3" />
          <rect x="15" y="150" width="130" height="64" rx="8" fill="var(--card)" stroke="var(--line)" />
          <text x="80" y="172" textAnchor="middle" fontSize="14" fontWeight="700" fill="var(--primary)">① WHERE</text>
          <text x="80" y="190" textAnchor="middle" fontSize="12" fill="var(--ink)">행 거르기</text>
          <text x="80" y="205" textAnchor="middle" fontSize="12" fill="var(--muted)">(입장 검사)</text>
          <rect x="175" y="150" width="130" height="64" rx="8" fill="var(--card)" stroke="var(--line)" />
          <text x="240" y="172" textAnchor="middle" fontSize="14" fontWeight="700" fill="var(--primary)">② GROUP BY</text>
          <text x="240" y="190" textAnchor="middle" fontSize="12" fill="var(--ink)">week로 묶기</text>
          <text x="240" y="205" textAnchor="middle" fontSize="12" fill="var(--muted)">(팀 만들기)</text>
          <rect x="335" y="150" width="130" height="64" rx="8" fill="var(--card)" stroke="var(--line)" />
          <text x="400" y="172" textAnchor="middle" fontSize="14" fontWeight="700" fill="var(--primary)">③ HAVING</text>
          <text x="400" y="190" textAnchor="middle" fontSize="12" fill="var(--ink)">팀 거르기</text>
          <text x="400" y="205" textAnchor="middle" fontSize="12" fill="var(--muted)">(팀 심사)</text>
          <rect x="495" y="150" width="130" height="64" rx="8" fill="var(--card)" stroke="var(--line)" />
          <text x="560" y="172" textAnchor="middle" fontSize="14" fontWeight="700" fill="var(--primary)">④ ORDER BY</text>
          <text x="560" y="190" textAnchor="middle" fontSize="12" fill="var(--ink)">정렬</text>
          <text x="560" y="205" textAnchor="middle" fontSize="12" fill="var(--muted)">(맨 마지막)</text>
          <line x1="147" y1="182" x2="168" y2="182" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="166,177 173,182 166,187" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <line x1="307" y1="182" x2="328" y2="182" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="326,177 333,182 326,187" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <line x1="467" y1="182" x2="488" y2="182" stroke="var(--muted)" strokeWidth="1.5" />
          <polyline points="486,177 493,182 486,187" fill="none" stroke="var(--muted)" strokeWidth="1.5" strokeLinecap="round" />
          <text x="320" y="244" textAnchor="middle" fontSize="12" fill="var(--muted)">WHERE COUNT(*)는 에러 — 입장 검사 때는 아직 팀이 없어서 셀 수 없어요</text>
        </svg>
        <p className="muted" style={{ fontSize: 12.5, textAlign: 'center', marginTop: 2 }}>그림: 실행 순서 — WHERE는 행을, HAVING은 팀을 거른다</p>
        <div className="warn">
          <b>흔한 실수</b> HAVING에 아무 조건이나 넣을 수 있다고 WHERE 대신 HAVING만
          쓰는 습관은 금물이에요. 행 단위 조건(<span className="icode">score IS NOT NULL</span> 같은)은
          WHERE에 두는 게 맞고, 보통 더 빠릅니다 — 묶기 전에 미리 걸러내면 묶을 양 자체가 줄거든요.
        </div>
      </Section>

      <Section n={3} title="서브쿼리 맛보기" sub="쿼리 안의 쿼리 — 미리 우려내는 육수">
        <p>
          "<strong>평균보다</strong> 높은 점수의 제출만 보여줘"라는 요청을 받았다고 해요.
          문제는 평균이 몇 점인지 <strong>쿼리를 돌려 봐야 안다</strong>는 것. 두 번 나눠 실행해도
          되지만, SQL은 쿼리 안에 괄호로 <strong>다른 쿼리를 통째로</strong> 넣는 걸 허용합니다.
          이게 <strong>서브쿼리</strong>예요.
        </p>
        <Code>{CODE_SUBQUERY}</Code>
        <p>
          괄호 안이 <strong>먼저</strong> 실행돼 값(또는 목록)이 되고, 바깥 쿼리가 그 값을 재료로
          씁니다. 라면을 끓이기 전에 육수부터 우려내는 것과 같아요 — 육수(서브쿼리)가 준비돼야
          본 요리(바깥 쿼리)가 시작되죠.
        </p>
        <div className="tip">
          <b>여기까지만</b> 서브쿼리는 JOIN과 섞이면 훨씬 깊은 세계가 열리는데,
          이번 코스에선 "괄호 안이 먼저 실행돼 재료가 된다"는 감각만 가져가면 충분해요.
          결과가 <strong>값 하나</strong>면 <span className="icode">=</span>·<span className="icode">&gt;</span>와,{' '}
          <strong>목록</strong>이면 <span className="icode">IN</span>과 짝지어 쓴다 — 이 두 패턴이 시작점입니다.
        </div>
      </Section>

      <Section n={4} title="NULL의 함정" sub="'없음'이 아니라 '모름' — 그래서 위험한 값">
        <p>
          <span className="icode">NULL</span>은 0도, 빈 문자열도 아니에요.
          <strong> "값을 모름"</strong>입니다. 답안지의 빈칸이 "0점"이 아니라
          "아직 채점 안 됨"인 것처럼요. 이 미묘한 차이가 실무에서 진짜 버그를 만듭니다 —
          그것도 에러 없이 <strong>조용히 틀린 숫자</strong>를 내놓는 최악의 방식으로요.
        </p>
        <Code>{CODE_NULL}</Code>
        <p>
          특히 함정 ②를 다시 보세요. <span className="icode">AVG(score)</span>가 미채점(NULL)을
          빼고 평균을 낸다는 건, <strong>"미채점을 0점으로 칠 거냐 말 거냐"를 여러분이 정해야
          한다</strong>는 뜻이에요. DB는 물어보지 않고 그냥 빼 버리니까요. 통계 숫자가 이상하게
          나올 때 첫 번째 용의자는 언제나 NULL입니다.
        </p>
        <div className="warn">
          <b>실전 체크리스트</b> 집계 쿼리를 쓰기 전에 스스로에게 물어보세요 —
          ① 이 컬럼에 NULL이 있을 수 있나? ② 있다면 세어야 하나, 빼야 하나?
          ③ 빼면 안 되면 <span className="icode">COALESCE</span>로 무슨 값으로 바꿀 건가?
        </div>
      </Section>

      <Section n={5} title="인덱스 — 책의 '찾아보기'" sub="왜 빨라질까, 그리고 왜 남용하면 안 될까">
        <p>
          500쪽짜리 전공 책에서 '트랜잭션'이 나온 쪽을 찾는다고 해봐요. 방법은 두 가지 —
          1쪽부터 한 장씩 넘기거나, 책 뒤의 <strong>찾아보기(index)</strong>에서
          "트랜잭션 → 214쪽"을 보고 바로 펴거나. DB의 <strong>인덱스</strong>가 정확히
          후자입니다. 컬럼 값들을 <strong>미리 정렬해 둔 별도의 목록</strong>을 만들어 놓고,
          검색할 때 그 목록으로 위치를 바로 찾아가는 거예요.
        </p>
        <Code>{CODE_INDEX}</Code>
        <p>
          그럼 "전부 인덱스 걸면 되겠네?" — 안 됩니다. 찾아보기가 붙은 책은
          <strong> 본문을 고칠 때마다 찾아보기도 고쳐야</strong> 하잖아요.
          데이터를 넣고 고칠 때마다(INSERT/UPDATE) 인덱스 유지 비용이 들어서,
          인덱스가 많을수록 <strong>쓰기가 느려집니다</strong>. 읽기와 쓰기의 거래(trade-off)예요.
          그래서 원칙은 하나 — <strong>WHERE나 JOIN에 자주 등장하는 컬럼에만</strong> 겁니다.
        </p>
        <div className="tip">
          <b>이미 하나 갖고 있어요</b> PRIMARY KEY(보통 <span className="icode">id</span>)에는
          PostgreSQL이 <strong>자동으로 인덱스</strong>를 만들어 줘요.
          <span className="icode"> WHERE id = 101</span>이 항상 빠른 이유가 이겁니다.
        </div>
      </Section>

      <Section n={6} title="EXPLAIN 훑어보기" sub="DB가 쓴 '실행 계획서' 읽는 법">
        <p>
          인덱스를 만들었는데 진짜 쓰이고 있을까요? 감으로 짐작할 필요 없어요.
          쿼리 앞에 <span className="icode">EXPLAIN</span>을 붙이면 PostgreSQL이
          "이 쿼리를 <strong>이런 방법으로 실행할 계획</strong>입니다" 하고 계획서를
          보여줍니다. 내비게이션이 출발 전에 경로를 미리 보여주는 것과 같아요.
        </p>
        <Code>{CODE_EXPLAIN}</Code>
        <p>
          지금 단계에선 두 단어만 구분하면 충분해요 —{' '}
          <span className="icode">Seq Scan</span>(순차 스캔)이 보이면 "책을 한 장씩 넘기는 중",{' '}
          <span className="icode">Index Scan</span>이 보이면 "찾아보기를 쓰는 중".
          느린 쿼리를 만나면 <strong>EXPLAIN 붙이기 → Seq Scan 찾기 → 그 컬럼에 인덱스
          걸까 고민하기</strong> — 이 3단 콤보가 백엔드 개발자의 기본기입니다.
        </p>
        <div className="tip">
          <b>알아두기</b> 데이터가 아주 적을 땐 인덱스가 있어도 DB가 일부러 Seq Scan을
          택하기도 해요. 두 쪽짜리 유인물은 찾아보기 없이 그냥 넘겨 보는 게 빠르니까요.
          "Seq Scan = 무조건 나쁨"이 아니라 <strong>"데이터가 많은데 Seq Scan = 점검 신호"</strong>로 읽으세요.
        </div>
      </Section>

      <Section n={7} title="트랜잭션과 ACID 한 입" sub="'전부 아니면 전무'라는 약속">
        <p>
          여러 문장이 <strong>한 몸처럼</strong> 실행돼야 할 때가 있어요. 계좌이체가 대표 선수죠 —
          "내 잔액 감소"와 "상대 잔액 증가" 중 <strong>하나만</strong> 실행되고 서버가 꺼지면
          돈이 증발합니다. 이런 참사를 막는 장치가 <strong>트랜잭션</strong>이에요.
          <span className="icode"> BEGIN</span>으로 묶기 시작해서 <span className="icode">COMMIT</span>(확정)
          또는 <span className="icode">ROLLBACK</span>(전부 취소)으로 끝냅니다.
        </p>
        <Code>{CODE_TRANSACTION}</Code>
        <p>
          우리 스택에서도 매일 쓰이고 있어요 — Spring Boot에서{' '}
          <span className="icode">@Transactional</span>이 붙은 메서드는 안의 DB 작업 전체가
          트랜잭션 하나로 묶여서, 중간에 예외가 터지면 <strong>자동으로 ROLLBACK</strong>됩니다.
          어노테이션 한 줄 뒤에서 오늘 배운 BEGIN/COMMIT이 돌고 있는 거예요.
        </p>
        <div className="warn">
          <b>주의</b> COMMIT하기 전의 변경은 <strong>나에게만</strong> 보여요(격리성).
          psql에서 UPDATE를 했는데 웹 화면에 반영이 안 된다면, COMMIT을 안 했을 가능성부터
          의심하세요. 트랜잭션을 열어 둔 채 자리를 뜨는 건 금고 문을 열어 두고 퇴근하는 것과 같습니다.
        </div>
      </Section>

      <Section n={8} title="종합 실습 — '학생별 제출 수' 완성하기" sub="오늘 배운 무기 전부 꺼내기">
        <p>
          멘토가 이런 요청을 했다고 해요 — <strong>"성실하게 제출하는 학생이 누군지 보고 싶어.
          채점 끝난 것만 세서, 3건 이상 낸 학생을 제출 많은 순으로 뽑아줘."</strong>
          이 한 문장 안에 NULL 처리(4섹션), GROUP BY(1섹션), HAVING(2섹션)이 전부 들어 있어요.
          빈칸 네 개를 채워 쿼리를 완성해 보세요.
        </p>
        <Code>{CODE_PRACTICE}</Code>
        <div className="tip">
          <b>직접 확인해 보기 ①</b> 빈칸을 종이나 메모장에 먼저 채운 뒤, 아래 정답과
          비교하세요. 하나라도 틀렸다면 해당 섹션으로 돌아가 그 부분만 다시 읽고 오기 —
          그게 가장 빠른 복습 경로예요.
        </div>
        <Code>{CODE_PRACTICE_ANSWER}</Code>
        <div className="tip">
          <b>직접 확인해 보기 ②</b> 이제 진짜 DB에서 돌려 봅시다. 우리 플랫폼은
          PostgreSQL을 Docker로 띄우죠 — 컨테이너 속 <span className="icode">psql</span>에
          접속해서 완성한 쿼리와 <span className="icode">EXPLAIN</span>을 직접 실행해 보세요.
          (운영 DB 말고 <strong>내 로컬 개발 DB</strong>에서만! SELECT와 EXPLAIN은 안전하지만,
          습관을 처음부터 바르게.)
        </div>
        <Code>{CODE_PSQL}</Code>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🗃️ 여기까지 왔다면</h3>
          <p className="muted">
            이제 데이터를 <strong>묶어서 요약하고</strong>(GROUP BY·HAVING),
            NULL에 속지 않고, 인덱스와 EXPLAIN으로 <strong>왜 느린지 진단</strong>하고,
            트랜잭션으로 <strong>안전하게 바꾸는</strong> 법까지 알게 됐어요.
            다음 걸음은 두 갈래 — 여러 테이블을 이어 붙이는 <strong>JOIN</strong>을 파고들거나,{' '}
            <Link to="/learn/spring-intro"><strong>Spring Boot 입문</strong></Link> 코스에서
            Spring Boot가 이 쿼리들을 어떻게 대신 써 주는지(JPA) 이어서 배워 보세요.
            완성한 실습 쿼리는 과제 게시판에 제출해 멘토 피드백을 받아 보는 것, 잊지 말고요!
          </p>
        </div>
      </div>
    </div>
  );
}
