// 이 파일이 하는 일: "DB 연동과 트랜잭션: 데이터가 사는 집" 코스 — 서버를 재시작하면
// 증발하는 메모리 배열에서 출발해, 테이블 설계·SQL 실행·CRUD·JOIN을 지나
// all-or-nothing 트랜잭션과 롤백 실습까지 7개 섹션으로 안내하는 정적 학습 페이지.
// 프레이밍은 "블로그 API의 저장소 교체" — 이전 과정에서 만든 메모리 배열 기반 블로그 API를
// 그대로 들고 와서, 저장소만 PostgreSQL로 갈아 끼우며 DB의 존재 이유를 몸으로 익힌다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_MEMORY_LOSS = `// server.js — 지금까지의 블로그 API (저장소 = 메모리 배열)
let posts = [];          // "데이터베이스"라고 부르기엔 민망한, 그냥 변수
let nextId = 1;

app.post('/api/posts', (req, res) => {
  const post = { id: nextId++, title: req.body.title, body: req.body.body };
  posts.push(post);      // 배열에 넣는다 = RAM에 넣는다
  res.status(201).json(post);
});

// 직접 목격하기:
// 1) POST로 글을 3개 만든다 → GET /api/posts 에 3개가 보인다
// 2) 터미널에서 Ctrl+C → node server.js 로 서버 재시작
// 3) GET /api/posts → []   ...... 글이 전부 증발했다
//
// 이유: 변수 posts는 프로세스의 메모리(RAM) 위에 산다.
// 프로세스가 죽으면 메모리도 반납된다. 데이터의 집이 아니라 임시 텐트였던 것.`;

const CODE_SCHEMA = `-- schema.sql — 글·댓글·사용자를 테이블로 옮기기
CREATE TABLE users (
  id         BIGSERIAL PRIMARY KEY,     -- PK: 이 행의 '주민번호'
  name       VARCHAR(50) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE posts (
  id            BIGSERIAL PRIMARY KEY,
  author_id     BIGINT NOT NULL REFERENCES users(id),  -- FK: users로 가는 연결고리
  title         VARCHAR(200) NOT NULL,
  body          TEXT NOT NULL,
  comment_count INT NOT NULL DEFAULT 0,  -- 6·7섹션 실습에서 트랜잭션으로 갱신할 값
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE comments (
  id         BIGSERIAL PRIMARY KEY,
  post_id    BIGINT NOT NULL REFERENCES posts(id),  -- 댓글 N개 → 글 1개 (1:N)
  author_id  BIGINT NOT NULL REFERENCES users(id),
  body       TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 1:N 관계 읽는 법: "comments.post_id가 posts.id를 가리킨다"
-- = 글 하나에 댓글 여러 개가 매달린다. 관계는 '외래키 컬럼 하나'로 표현된다.`;

const CODE_POOL = `// db.js — 커넥션 풀: 미리 뚫어 둔 전화선 여러 가닥
import pg from 'pg';

export const pool = new pg.Pool({
  host: 'localhost',
  database: 'blog',
  user: 'blog',
  password: process.env.DB_PASSWORD,  // 비밀번호는 코드에 두지 않는다
  max: 10,                            // 동시에 유지할 연결 수
});

// 왜 풀인가?
// DB 연결(connect)은 TCP 접속 + 인증을 거치는 '비싼' 작업이다.
// 요청마다 새로 연결하면: 요청 100개 = 연결 100번 = 느리고, DB도 지친다.
// 풀은 연결 10개를 미리 만들어 두고 빌려 줬다 돌려받는다.
// 요청 100개가 와도 연결은 10개를 돌려 쓴다 — 식당의 '회전하는 테이블'처럼.

const result = await pool.query('SELECT now()');  // 빌리고-쓰고-반납을 알아서
console.log(result.rows[0]);`;

const CODE_INJECTION = `// 위험한 코드 — 문자열 조립으로 SQL을 만드는 순간, 남이 내 SQL을 쓴다
app.get('/api/users/search', async (req, res) => {
  const name = req.query.name;
  // 절대 금지: 사용자 입력을 SQL 문장에 '이어붙이기'
  const sql = "SELECT * FROM users WHERE name = '" + name + "'";
  const result = await pool.query(sql);
  res.json(result.rows);
});

// 공격자가 보낸 name:  ' OR '1'='1
// 완성되는 SQL:  SELECT * FROM users WHERE name = '' OR '1'='1'
// → '1'='1'은 항상 참 → 전체 사용자가 통째로 유출된다.
// name에  '; DROP TABLE users; --  를 넣으면? 테이블이 날아갈 수도 있다.

// 안전한 코드 — 파라미터 바인딩($1): 값은 값의 자리로만
const result2 = await pool.query(
  'SELECT * FROM users WHERE name = $1',  // SQL의 구조는 여기서 확정
  [name]                                  // 값은 별도 전달 → 문장이 될 수 없음
);
// DB가 "이건 어떤 내용이든 그냥 문자열 값"으로 취급하므로
// OR이든 DROP이든 전부 '이름 글자'일 뿐이다. 인젝션이 원천 차단된다.`;

const CODE_CRUD = `// posts 라우터 — 메모리 배열 CRUD를 SQL로 다시 쓰기

// C: INSERT ... RETURNING — 만든 결과(자동 생성된 id 포함)를 바로 돌려받기
app.post('/api/posts', async (req, res) => {
  const { title, body, authorId } = req.body;
  const result = await pool.query(
    'INSERT INTO posts (author_id, title, body) VALUES ($1, $2, $3) RETURNING *',
    [authorId, title, body]
  );
  res.status(201).json(result.rows[0]);  // id·created_at까지 채워진 완성본
});

// U: UPDATE — WHERE를 빼먹으면 '모든 글'이 같은 제목이 된다 (실제 사고 단골)
//   UPDATE posts SET title = $1              ← 전 행 수정. 참사.
//   UPDATE posts SET title = $1 WHERE id=$2  ← 이 한 줄이 회사를 살린다
app.put('/api/posts/:id', async (req, res) => {
  const result = await pool.query(
    'UPDATE posts SET title = $1, body = $2 WHERE id = $3',
    [req.body.title, req.body.body, req.params.id]
  );
  if (result.rowCount === 0) return res.status(404).json({ error: '없는 글' });
  res.json({ ok: true });  // rowCount: 실제로 영향받은 행 수 → 0이면 그 id가 없다
});

// D: DELETE — 같은 원리. rowCount로 "지울 게 있었는지"를 판단
app.delete('/api/posts/:id', async (req, res) => {
  const result = await pool.query('DELETE FROM posts WHERE id = $1', [req.params.id]);
  if (result.rowCount === 0) return res.status(404).json({ error: '없는 글' });
  res.status(204).end();
});`;

const CODE_JOIN = `-- 화면: "글 목록 + 작성자 이름 + 댓글 수" — 쿼리 한 방으로

-- ① 작성자 이름 붙이기: INNER JOIN (FK를 따라 두 테이블을 이어 붙인다)
SELECT p.id, p.title, u.name AS author_name
FROM posts p
INNER JOIN users u ON u.id = p.author_id;
-- "p.author_id가 가리키는 users 행을 찾아 옆에 붙여라"

-- ② 댓글 수까지: LEFT JOIN + GROUP BY
SELECT p.id, p.title, u.name AS author_name,
       COUNT(c.id) AS comment_count
FROM posts p
INNER JOIN users u ON u.id = p.author_id
LEFT JOIN comments c ON c.post_id = p.id   -- 댓글 0개인 글도 목록엔 나와야 하니 LEFT
GROUP BY p.id, u.name                       -- 글 단위로 접어서(COUNT) 센다
ORDER BY p.created_at DESC;`;

const CODE_N_PLUS_ONE = `// N+1 문제 — 눈으로 확인하기
// "글 목록을 가져온 뒤, 글마다 작성자를 또 조회"하는 순진한 코드:
const posts = (await pool.query('SELECT * FROM posts')).rows;      // 쿼리 1번
for (const post of posts) {
  const author = await pool.query(                                  // 글마다 +1번
    'SELECT name FROM users WHERE id = $1', [post.author_id]
  );
  post.authorName = author.rows[0].name;
}
// 글이 100개면? 1 + 100 = 101번의 쿼리. 글이 늘수록 선형으로 느려진다.
// DB 로그를 켜고(postgresql.conf: log_statement = 'all') 위 코드를 돌려 보면
// SELECT가 폭포처럼 쏟아지는 걸 직접 볼 수 있다.

// 해결: 섹션 5의 JOIN 한 방이면 쿼리 1번으로 끝
const rows = (await pool.query(
  'SELECT p.*, u.name AS author_name FROM posts p JOIN users u ON u.id = p.author_id'
)).rows;
// 반복문 속의 await pool.query(...)가 보이면 일단 의심할 것.`;

const CODE_TRANSFER = `-- 계좌이체: 트랜잭션이 없으면 생기는 일
-- A가 B에게 10,000원을 보낸다 = 사실은 UPDATE 두 번이다
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';  -- ① A에서 빼고
-- ← 만약 서버가 정확히 여기서 죽는다면?
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';  -- ② B에 더한다

-- ①만 실행되고 죽으면: A의 돈은 사라졌는데 B는 못 받았다. 10,000원 증발.
-- 이 둘은 "함께 성공하거나, 함께 없던 일"이어야 한다 = all-or-nothing.

BEGIN;                                                          -- 여기부터 한 묶음
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
COMMIT;   -- 둘 다 성공했을 때만 '진짜로' 반영
-- 중간에 무엇이 잘못되든 ROLLBACK; 하면 BEGIN 이전으로 완전 복귀.
-- COMMIT 전의 변경은 '연필로 쓴 초안', COMMIT이 '볼펜으로 덮어쓰기'다.`;

const CODE_TX_PRACTICE = `// 실습 핵심 — '댓글 작성 + 글의 댓글수 갱신'을 하나의 트랜잭션으로
app.post('/api/posts/:id/comments', async (req, res) => {
  const client = await pool.connect();   // 트랜잭션은 '같은 연결' 위에서만 성립!
  try {
    await client.query('BEGIN');

    const comment = await client.query(
      'INSERT INTO comments (post_id, author_id, body) VALUES ($1, $2, $3) RETURNING *',
      [req.params.id, req.body.authorId, req.body.body]
    );
    const updated = await client.query(
      'UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1',
      [req.params.id]
    );
    if (updated.rowCount === 0) throw new Error('없는 글');  // → catch → ROLLBACK

    await client.query('COMMIT');        // 둘 다 성공 → 한 번에 확정
    res.status(201).json(comment.rows[0]);
  } catch (e) {
    await client.query('ROLLBACK');      // 하나라도 실패 → 둘 다 없던 일로
    res.status(400).json({ error: e.message });
  } finally {
    client.release();                    // 빌린 연결은 반드시 풀에 반납
  }
});
// 주의: pool.query()를 섞어 쓰면 안 된다 — 풀이 매번 '다른 연결'을 빌려 줄 수 있어
// BEGIN과 INSERT가 서로 남남인 연결에서 실행될 수 있다. 트랜잭션 중엔 client만.`;

const CODE_ROLLBACK_TEST = `// 실패 시나리오 실습 ① — 일부러 중간에 에러를 던져 롤백 확인
await client.query('BEGIN');
await client.query(
  'INSERT INTO comments (post_id, author_id, body) VALUES ($1, $2, $3)',
  [postId, authorId, '이 댓글은 저장되면 안 됩니다']
);
throw new Error('일부러 터뜨림');   // ← COMMIT에 도달하지 못한다
// catch에서 ROLLBACK 실행 후, psql에서 확인:
//   SELECT count(*) FROM comments;   → 늘어나지 않았다. INSERT가 없던 일이 됐다.

-- 실습 ② — 커밋 전·후를 '다른 세션'에서 조회해 보기 (격리 맛보기)
-- [터미널1] psql        │ [터미널2] psql (또 하나의 접속 = 다른 세션)
-- BEGIN;                │
-- INSERT INTO comments  │ SELECT count(*) FROM comments;
--   (...) VALUES (...); │ → 안 보인다! 아직 '초안'이라 남들 눈엔 없음
-- COMMIT;               │ SELECT count(*) FROM comments;
--                       │ → 이제 보인다. COMMIT이 '공개' 스위치다.
-- 이렇게 각 트랜잭션이 서로의 미완성 작업을 보지 못하게 하는 성질이 '격리(isolation)'.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '왜 DB인가' },
  { n: 2, label: '테이블 설계' },
  { n: 3, label: 'Node에서 SQL' },
  { n: 4, label: 'CRUD를 SQL로' },
  { n: 5, label: 'JOIN' },
  { n: 6, label: '트랜잭션' },
  { n: 7, label: '실패 시나리오' },
];

export default function Lv2DbTransactionsPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>DB 연동과 트랜잭션:<br />데이터가 사는 집</h1>
        <p>
          지금까지 우리 블로그 API의 데이터는 <strong>서버의 변수</strong> 안에 살았어요.
          서버를 껐다 켜면? 전부 증발합니다. 이 코스에서는 그 데이터를{' '}
          <strong>PostgreSQL</strong>이라는 진짜 집으로 이사시키고, "댓글도 달렸는데 댓글수는
          안 올라간" 어정쩡한 상태가 생기지 않도록 <strong>트랜잭션(all-or-nothing)</strong>
          으로 묶는 법까지 익힙니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 80분</span>
          <span className="chip">실습: 메모리 배열 → PostgreSQL 교체</span>
          <span className="chip">선수 지식: JS 기초 · HTTP API</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="왜 메모리가 아니라 DB인가" sub="서버 재시작 한 번이면 알게 되는 것">
        <p>
          백문이 불여일견이에요. 지금 만든 블로그 API에 글을 몇 개 올린 다음,{' '}
          <strong>서버를 껐다 켜 보세요.</strong> 정성 들여 쓴 글이 흔적도 없이 사라집니다.
          변수는 프로세스의 <strong>메모리(RAM)</strong> 위에 살고, 프로세스가 죽으면 메모리는
          운영체제에 반납되니까요.
        </p>
        <Code>{CODE_MEMORY_LOSS}</Code>
        <p>
          데이터베이스는 이 문제를 포함해 크게 <strong>세 가지</strong>를 해결해 줍니다.{' '}
          <strong>① 영속성</strong> — 데이터를 디스크에 안전하게 기록해서, 서버가 죽든 정전이
          나든 살아남게 합니다. <strong>② 동시성</strong> — 사용자 100명이 동시에 글을 쓰고
          지워도 데이터가 꼬이지 않게 교통정리를 해 줍니다(배열에 동시에 push하는 상황을
          상상해 보세요). <strong>③ 검색</strong> — "제목에 'DB'가 들어간 글을 최신순으로
          10개"를, 데이터가 백만 건이어도 순식간에 찾아 줍니다. 배열이었다면 매번 전체를
          훑어야 했겠죠.
        </p>
        <p>
          그중에서도 우리는 <strong>관계형 DB(PostgreSQL)</strong>를 선택합니다. 기준은
          단순해요. 우리 데이터는 글·댓글·사용자처럼 <strong>서로 관계를 맺는 정형
          데이터</strong>이고, "댓글수와 실제 댓글이 어긋나면 안 된다" 같은{' '}
          <strong>정합성</strong>이 중요하며, 뒤에서 배울 <strong>트랜잭션</strong>이
          필요하니까요. 이 세 조건에 관계형 DB만큼 검증된 도구가 없습니다.
        </p>
        <div className="tip">
          <b>비유</b> 메모리 배열은 <strong>화이트보드</strong>예요 — 쓰긴 빠르지만 퇴근할 때
          지워질 수 있죠. DB는 <strong>등기소의 장부</strong>입니다. 기록이 남고, 여러 창구가
          동시에 써도 관리되고, 색인으로 빨리 찾을 수 있어요. 서비스의 데이터는 화이트보드가
          아니라 장부에 살아야 합니다.
        </div>
      </Section>

      <Section n={2} title="테이블 설계와 관계" sub="글·댓글·사용자를 표로 옮기기">
        <p>
          관계형 DB의 세계에서 데이터는 <strong>테이블(표)</strong>에 삽니다. 우리 블로그의
          등장인물은 셋 — <strong>사용자, 글, 댓글</strong>. 각각을 표 하나씩으로 옮기고,
          그 사이의 관계를 선으로 잇는 게 설계의 전부예요.
        </p>
        <Code>{CODE_SCHEMA}</Code>
        <p>
          핵심 개념 두 개만 잡으면 됩니다. <strong>PK(기본키, Primary Key)</strong>는 각 행의{' '}
          <strong>주민번호</strong>예요. 같은 이름의 사용자가 두 명이어도{' '}
          <span className="icode">id</span>는 절대 겹치지 않아서, "그 행"을 정확히 집어낼 수
          있죠. <strong>FK(외래키, Foreign Key)</strong>는 다른 테이블의 PK를 가리키는{' '}
          <strong>연결고리</strong>입니다. <span className="icode">comments.post_id</span>가{' '}
          <span className="icode">posts.id</span>를 가리키는 순간, "이 댓글은 저 글의 것"이라는
          관계가 데이터에 새겨져요.
        </p>
        <p>
          "글 하나에 댓글 여러 개"처럼 <strong>1:N 관계</strong>는 항상{' '}
          <strong>N 쪽 테이블에 외래키 컬럼 하나</strong>를 두는 것으로 표현합니다. 댓글(N)이
          자기 몸에 <span className="icode">post_id</span>를 적어 두는 거예요. 글(1) 쪽에
          "내 댓글 목록"을 배열로 넣지 않는다는 점이 배열 시절과 가장 다른 사고방식입니다.
        </p>
        <div className="tip">
          <b>FK의 보너스</b> <span className="icode">REFERENCES posts(id)</span>라고 선언해
          두면, DB가 <strong>존재하지 않는 글에 댓글 다는 것을 스스로 거부</strong>해 줍니다.
          앱 코드가 실수해도 데이터의 마지막 방어선이 지켜지는 셈 — 이런 걸{' '}
          <strong>참조 무결성</strong>이라고 불러요.
        </div>
      </Section>

      <Section n={3} title="Node에서 SQL 실행하기" sub="커넥션 풀, 그리고 $1이 지키는 것">
        <p>
          이제 Node 서버에서 PostgreSQL에 말을 걸 차례예요. 공식에 가까운 드라이버인{' '}
          <span className="icode">pg</span> 패키지를 쓰는데, 시작부터 습관 하나를 제대로
          들입시다 — 연결은 <strong>커넥션 풀(Pool)</strong>로.
        </p>
        <Code>{CODE_POOL}</Code>
        <p>
          DB 연결 한 번에는 TCP 접속과 인증이라는 비용이 들어요. 요청이 올 때마다 새로
          연결하면 그 비용을 매번 치르고, DB 쪽도 연결 수가 폭증해 버팁니다. 풀은{' '}
          <strong>연결 몇 개를 미리 만들어 두고 빌려 줬다 돌려받는</strong> 구조라, 요청이
          아무리 많아도 연결 수는 일정하게 유지돼요.
        </p>
        <p>
          그리고 이 코스에서 <strong>가장 중요한 보안 습관</strong>이 나옵니다. SQL에 사용자
          입력을 넣을 땐 반드시 <strong>파라미터 바인딩(<span className="icode">$1</span>,{' '}
          <span className="icode">$2</span>…)</strong>을 쓰세요. 문자열을 이어붙여 SQL을 만드는
          순간, 사용자가 <strong>내 SQL 문장을 대신 쓰게</strong> 됩니다.
        </p>
        <Code>{CODE_INJECTION}</Code>
        <div className="warn">
          <b>SQL 인젝션은 이론이 아니다</b> 위 시연처럼{' '}
          <span className="icode">' OR '1'='1</span> 한 줄이면 전체 데이터가 유출되고,{' '}
          <span className="icode">DROP TABLE</span>이 섞이면 테이블이 통째로 사라질 수 있어요.
          실제 대형 유출 사고의 단골 원인입니다. 방어법은 외우지 않아도 될 만큼 단순해요 —{' '}
          <strong>SQL 문장과 값을 절대 한 문자열로 섞지 않는다.</strong> 값은 언제나{' '}
          <span className="icode">$1</span> 자리로만. DB는 바인딩된 값을 "어떤 내용이든 그냥
          값"으로 취급하므로 문장 구조를 바꿀 수 없습니다.
        </div>
      </Section>

      <Section n={4} title="CRUD를 SQL로 다시 쓰기" sub="배열 메서드를 SQL 문장으로 번역하기">
        <p>
          준비가 끝났으니 본격 이사입니다. 배열 시절의{' '}
          <span className="icode">push</span> · <span className="icode">find</span> ·{' '}
          <span className="icode">splice</span>를 각각{' '}
          <span className="icode">INSERT</span> · <span className="icode">SELECT</span> ·{' '}
          <span className="icode">UPDATE</span>/<span className="icode">DELETE</span>로
          번역해요. 라우터의 겉모양(URL·상태코드)은 그대로 두고 <strong>속만 갈아
          끼우는</strong> 게 포인트 — API를 쓰는 쪽에서는 이사한 걸 눈치채지 못해야 합니다.
        </p>
        <Code>{CODE_CRUD}</Code>
        <p>
          새로 익힐 무기가 둘 있어요. 첫째, <strong><span className="icode">INSERT ...
          RETURNING *</span></strong> — 방금 만든 행을 (DB가 채워 준{' '}
          <span className="icode">id</span>·<span className="icode">created_at</span>까지 포함해)
          그 자리에서 돌려받습니다. INSERT 후 다시 SELECT할 필요가 없죠. 둘째,{' '}
          <strong><span className="icode">rowCount</span></strong> — 쿼리가 실제로 영향을 준
          행의 수예요. UPDATE·DELETE에서 이 값이 <span className="icode">0</span>이면 "그런
          id의 글은 없었다"는 뜻이니, 그대로 <strong>404</strong>로 응답하면 됩니다. 배열
          시절의 <span className="icode">find()</span>가{' '}
          <span className="icode">undefined</span>를 돌려주던 것과 같은 역할이에요.
        </p>
        <div className="warn">
          <b>WHERE 없는 UPDATE — 전설의 참사</b>{' '}
          <span className="icode">UPDATE posts SET title = '테스트'</span>처럼{' '}
          <strong>WHERE를 빼먹으면 테이블의 모든 행</strong>이 바뀝니다. DELETE도 마찬가지 —
          WHERE 없는 DELETE 한 줄로 전체 데이터가 사라진 사고가 실제로 심심찮게 일어나요.
          습관 하나로 예방합시다: UPDATE·DELETE를 쓸 땐 <strong>WHERE부터 먼저 쓰고</strong>{' '}
          앞부분을 채우기. 그리고 운영 DB에서 손으로 실행할 땐 다음 섹션의 트랜잭션으로 감싸고
          확인 후 COMMIT하는 것이 안전벨트입니다.
        </div>
      </Section>

      <Section n={5} title="JOIN으로 화면 데이터 한 번에" sub="글 목록 + 작성자 이름 + 댓글 수를 쿼리 한 방에">
        <p>
          블로그 목록 화면을 떠올려 보세요. 글 제목 옆에 <strong>작성자 이름</strong>이 있고,
          아래엔 <strong>댓글 수</strong>가 붙죠. 그런데 우리 테이블에서 이름은{' '}
          <span className="icode">users</span>에, 댓글은 <span className="icode">comments</span>에
          살아요. 흩어진 데이터를 <strong>FK를 따라 이어 붙이는</strong> 도구가{' '}
          <strong>JOIN</strong>입니다.
        </p>
        <Code>{CODE_JOIN}</Code>
        <p>
          <strong>INNER JOIN</strong>은 "<span className="icode">p.author_id</span>가 가리키는{' '}
          <span className="icode">users</span> 행을 찾아 옆에 붙여라"예요. 섹션 2에서 심어 둔
          연결고리(FK)가 여기서 길이 됩니다. 댓글 수는{' '}
          <strong>LEFT JOIN + GROUP BY</strong> 조합 — 글마다 매달린 댓글들을 글 단위로
          접어서(<span className="icode">GROUP BY p.id</span>){' '}
          <span className="icode">COUNT</span>로 셉니다. 댓글이 0개인 글도 목록에는 나와야
          하니, 짝이 없어도 행을 남겨 주는 <strong>LEFT</strong> JOIN을 쓴다는 점도 눈여겨
          두세요(INNER였다면 댓글 없는 글이 목록에서 사라집니다).
        </p>
        <p>
          JOIN을 모르면 자연스럽게 이런 코드를 쓰게 돼요 — "글 목록을 가져온 다음, 글마다
          작성자를 또 조회". 이것이 악명 높은 <strong>N+1 쿼리 문제</strong>입니다.
        </p>
        <Code>{CODE_N_PLUS_ONE}</Code>
        <div className="tip">
          <b>냄새 감지법</b> <strong>반복문 안의 <span className="icode">await
          pool.query(...)</span></strong>가 보이면 일단 N+1을 의심하세요. 데이터 10개일 땐
          멀쩡하다가 1,000개가 되는 순간 느려지는 API의 단골 원인이에요. 해결의 방향은 늘
          같습니다 — <strong>반복 조회를 JOIN(또는 IN 절) 한 방으로</strong> 합치기.
        </div>
      </Section>

      <Section n={6} title="트랜잭션: all-or-nothing" sub="함께 성공하거나, 함께 없던 일이거나">
        <p>
          이 코스의 심장부입니다. 질문 하나로 시작할게요 — <strong>계좌이체</strong>는 DB
          입장에서 무엇일까요? "A에서 빼는 UPDATE"와 "B에 더하는 UPDATE",{' '}
          <strong>두 번의 쓰기</strong>예요. 그런데 첫 번째만 실행된 순간 서버가 죽는다면?
        </p>
        <Code>{CODE_TRANSFER}</Code>
        <p>
          이런 "여러 개의 쓰기가 반드시 한 덩어리여야 하는" 상황을 위해 DB가 주는 도구가{' '}
          <strong>트랜잭션</strong>입니다. <span className="icode">BEGIN</span>으로 묶음을
          열고, 그 안의 변경은 <strong>연필로 쓴 초안</strong> 상태로 쌓이다가,{' '}
          <span className="icode">COMMIT</span>에서 한꺼번에 확정돼요. 중간에 무엇이든
          잘못되면 <span className="icode">ROLLBACK</span> — <span className="icode">BEGIN</span>{' '}
          이후의 모든 변경이 <strong>통째로 없던 일</strong>이 됩니다. 절반만 반영된 어정쩡한
          상태는 존재할 수 없어요. 그래서 <strong>all-or-nothing</strong>.
        </p>
        <p>
          우리 블로그에도 똑같은 구조가 있어요 — 실습 과제인 <strong>"댓글 작성 + 글의
          댓글수(<span className="icode">comment_count</span>) 갱신"</strong>. 댓글 INSERT만
          되고 댓글수 UPDATE가 실패하면, 화면엔 "댓글 3개"라는데 실제론 4개가 달린 이상한
          글이 됩니다. 묶읍시다.
        </p>
        <Code>{CODE_TX_PRACTICE}</Code>
        <div className="warn">
          <b>트랜잭션은 '같은 연결' 위에서만</b> 풀에서 빌린{' '}
          <span className="icode">client</span> 하나로 <span className="icode">BEGIN</span>부터{' '}
          <span className="icode">COMMIT</span>까지 전부 실행해야 해요. 중간에{' '}
          <span className="icode">pool.query()</span>를 섞으면 풀이 <strong>다른 연결</strong>을
          빌려 줄 수 있고, 그 쿼리는 내 트랜잭션 바깥에서 실행됩니다. 그리고{' '}
          <span className="icode">finally</span>의 <span className="icode">client.release()</span>를
          잊으면 연결이 반납되지 않아, 풀이 말라붙어 서버 전체가 멈춰요.
        </div>
        <div className="tip">
          <b>경계를 정하는 감각</b> 트랜잭션은 "<strong>이것들이 반쪽만 반영되면 데이터가
          거짓말을 하게 되는가?</strong>"로 경계를 긋습니다. 댓글 INSERT와 댓글수 UPDATE →
          반쪽이면 거짓말 → 한 트랜잭션. 반면 "댓글 작성 + 조회수 로그 남기기"처럼 하나가
          실패해도 다른 하나가 유효한 작업까지 욕심내서 묶으면, 로그 실패가 댓글 작성까지
          취소시키는 <strong>과잉 결합</strong>이 돼요. 필요한 만큼만, 짧게 — 가 원칙입니다.
        </div>
      </Section>

      <Section n={7} title="실패 시나리오 실습" sub="일부러 부수면서 롤백과 격리를 체감하기">
        <p>
          트랜잭션은 성공 경로만 봐서는 절대 체득되지 않아요. <strong>일부러 부수는 것</strong>이
          이번 섹션의 실습입니다. 두 가지를 눈으로 확인해 봅시다.
        </p>
        <Code>{CODE_ROLLBACK_TEST}</Code>
        <p>
          <strong>실습 ①</strong>은 INSERT 다음에 고의로 에러를 던져{' '}
          <span className="icode">COMMIT</span>에 도달하지 못하게 하는 거예요. catch에서{' '}
          <span className="icode">ROLLBACK</span>이 돌고 나면, 분명 INSERT가 실행됐는데도
          테이블엔 아무 흔적이 없습니다. <strong>실습 ②</strong>는 psql 터미널을{' '}
          <strong>두 개</strong> 열고, 한쪽에서 <span className="icode">BEGIN</span> 후
          INSERT만 한 상태로 다른 쪽에서 조회해 보는 것. 커밋 전엔 <strong>안 보이고</strong>,
          커밋하는 순간 보입니다. 각 트랜잭션이 서로의 미완성 작업을 보지 못하게 하는 이
          성질이 <strong>격리(isolation)</strong>예요 — 깊은 격리 수준 이야기는 다음 기회에
          다루고, 오늘은 "커밋 전의 변경은 남에게 없는 것"이라는 감각만 가져가면 충분합니다.
        </p>
        <p>
          마지막으로 실무 감각 하나. <strong>트랜잭션 안에서 외부 API를 호출하면 안
          됩니다.</strong> 이유는 두 겹이에요. 첫째, 트랜잭션이 열려 있는 동안 그 연결(그리고
          DB가 잡은 잠금)은 <strong>계속 점유</strong>되는데, 외부 API가 3초 걸리면 그 3초
          내내 다른 요청들이 줄을 서요. 느린 외부 호출 하나가 풀 전체를 말려 버릴 수 있습니다.
          둘째, DB는 롤백이 되지만 <strong>외부 세계는 롤백이 안 돼요.</strong> 트랜잭션
          안에서 알림 메일을 보냈는데 그 뒤 롤백되면? 댓글은 없던 일이 됐는데 메일은 이미
          날아갔죠. 그래서 원칙은 — <strong>트랜잭션은 DB 작업만으로 짧게 닫고, 외부 호출은
          커밋이 확정된 다음에.</strong>
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 실습 ②를 할 때, BEGIN만 하고 COMMIT을 안 한 채 psql을
          그냥 꺼 보세요. PostgreSQL은 연결이 끊긴 트랜잭션을 <strong>자동으로 롤백</strong>
          합니다 — "중간에 죽으면?"의 답이 바로 이거예요. 서버가 어떻게 죽든, 커밋에 도달하지
          못한 변경은 DB가 알아서 없던 일로 만들어 줍니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🗄️ 여기까지 왔다면</h3>
          <p className="muted">
            이제 우리 블로그의 데이터는 서버를 껐다 켜도 살아남는 <strong>진짜 집</strong>에
            삽니다. 테이블과 관계(PK·FK), 커넥션 풀과 <strong>$1 바인딩</strong>, RETURNING과
            rowCount, JOIN과 N+1, 그리고 <strong>all-or-nothing 트랜잭션</strong>까지 —
            "돌아가는 API"를 "믿을 수 있는 API"로 끌어올리는 재료를 손에 쥐었어요. 그런데
            데이터가 영원히 남는 순간, 새 질문이 따라옵니다. <strong>"이 요청을 보낸 사람이
            누구인지"</strong>는 어떻게 믿죠? 비밀번호는 DB에 어떻게 저장해야 유출돼도
            안전할까요? 다음{' '}
            <Link to="/learn/lv2-auth-session-hash"><strong>로그인 구현: 세션과 비밀번호
            해시</strong></Link> 코스에서, 이 집에 <strong>잠금장치</strong>를 달아 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
