# AI 꾸러미 — 고정 크기 화면 확대 — Fixed-resolution stage scaling (letterbox)
> 게임 화면을 1600×900 같은 고정 크기 판 하나로 만들고, 화면 크기에 맞춰 transform: scale 로 통째로 키우거나 줄여 PC · 태블릿 · 폰에서 배치가 똑같게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u68

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 고정 크기 화면 확대 — Fixed-resolution stage scaling (letterbox)

#### 1. 목표
HTML 로 만든 게임 화면 (서류 · 카드 · 판정 단추)을(를) 1600×900 고정 크기 판으로 만들고 화면에 맞춰 통째로 확대 · 축소해 줘 — PC · 태블릿 · 가로 폰에서 배치가 똑같이, 남는 곳은 검은 띠.

#### 2. 핵심 기술 용어
- **Fixed-resolution stage scaling (letterbox)** — 고정 크기 판을 통째로 확대 · 남는 곳은 띠
- **transform: scale() + transform-origin** — CSS 로 판 전체 키우기
- **Aspect-fit (min of width / height ratio)** — 가로 · 세로 배율 중 작은 쪽으로 맞추기
- **ResizeObserver** — 화면 크기가 바뀌면 다시 맞추기

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 브라우저 — PC · 폰 모두

#### 4. 조건
- 판 안의 모든 크기는 판 좌표(px) 기준 — vw · vh · 화면 단위 금지
- transform-origin: 0 0 으로 두고 가운데 맞춤은 left · top 으로 따로 계산
- 화면 크기 변화는 ResizeObserver(또는 resize)로 받아 다시 계산
- 가로 폰 844×390 에서 글씨 · 단추가 읽히고 눌리는지 확인 (작으면 폰용 기준 폭을 따로)

#### 5. 완성 기준 (이게 보이면 성공)
- PC 1920×1200 · 태블릿 1180×820 · 가로 폰 844×390 에서 판 배치가 똑같고 비율만 다르다
- 판이 화면 밖으로 잘리지 않고, 남는 곳은 띠로 채워진다
- 창 크기를 끌어 바꾸면 바로 다시 맞춰진다
- 판 안 클릭 위치가 정확하다 (배율을 고려해 좌표를 바꿀 필요 없이 DOM 이 알아서)

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 모든 배치를 1600×900 좌표 하나로 짠다 — 글씨 · 단추 크기도 그 판 기준.
- 배율 s = min(화면 폭 / 1600, 화면 높이 / 900). 판을 s 배 하면 어느 쪽도 넘치지 않는다.
- 판을 가운데에 두고 남는 곳은 띠(검정 · 흐린 배경)로 채운다. 판 안 그림 · 글씨는 비율이 그대로라 어디서나 같은 모습.
- 수학 검문소는 한 걸음 더: 폰(폭 1100 미만)은 기준 폭 1100, 세로가 긴 화면은 판 높이를 4:3 까지 늘려 띠를 줄인다.

## 핵심 코드 — 고정 판을 화면에 맞추기 (띠 방식 + 판 늘리기)
(발췌: demos/demosDraw2d.ts u68 의 배율 식과 game/games/checkpoint/CheckpointGame.ts resize() 를 정리)
```ts
// 띠 방식: 1600×900 판을 화면 안에 꼭 맞게
function fitBoard(stage: HTMLElement, layer: HTMLElement) {
  const w = stage.clientWidth, h = stage.clientHeight;
  const s = Math.min(w / 1600, h / 900);
  Object.assign(layer.style, {
    position: 'absolute', width: '1600px', height: '900px', transformOrigin: '0 0',
    left: (w - 1600 * s) / 2 + 'px', top: (h - 900 * s) / 2 + 'px', transform: 'scale(' + s + ')',
  });
}

// 판 늘리기 방식 (수학 검문소): 폭에 맞추고, 세로가 길면 4:3 까지 판을 늘린다
function fitLayer(stage: HTMLElement, layer: HTMLElement) {
  const w = stage.clientWidth, h = stage.clientHeight;
  if (w <= 0 || h <= 0) return;
  const compact = w < 1100;                    // 폰은 기준 폭을 줄여 글씨를 크게
  const base = compact ? 1100 : 1600;
  const minH = compact ? 480 : 740;
  let s = w / base, lw = base, lh = h / s;
  if (lh > base * 0.75) { s = h / (base * 0.75); lh = base * 0.75; lw = w / s; }
  if (lh < minH) { s = h / minH; lh = minH; lw = w / s; }
  Object.assign(layer.style, { width: lw + 'px', height: lh + 'px', transform: 'scale(' + s + ')', transformOrigin: '0 0' });
  layer.classList.toggle('is-compact', compact);
}

new ResizeObserver(() => fitLayer(stageEl, layerEl)).observe(stageEl);
```

## 흔한 실수 · 확인 목록
- [ ] **판 안에서 vw · vh 를 쓰면 배율과 겹쳐 두 번 줄어든다** — 판 안은 판 좌표(px)만 쓴다.
- [ ] **폰에서 1600 판을 통째로 줄이면 글씨가 읽을 수 없게 작다** — 폰은 기준 폭을 1100 으로 줄이고 .is-compact 규칙으로 배치를 따로 다듬는다.
- [ ] **transform-origin 을 가운데로 두면 판이 화면 밖으로 반쯤 나간다** — 원점은 0 0 으로, 가운데 맞춤은 left · top 으로 계산한다.
- [ ] **getBoundingClientRect 로 잰 크기를 판 좌표로 쓰면 틀린다** — 잰 값을 배율 s 로 나눠 판 좌표로 바꾼다.

## 완성 기준 체크리스트
- [ ] PC 1920×1200 · 태블릿 1180×820 · 가로 폰 844×390 에서 판 배치가 똑같고 비율만 다르다
- [ ] 판이 화면 밖으로 잘리지 않고, 남는 곳은 띠로 채워진다
- [ ] 창 크기를 끌어 바꾸면 바로 다시 맞춰진다
- [ ] 판 안 클릭 위치가 정확하다 (배율을 고려해 좌표를 바꿀 필요 없이 DOM 이 알아서)

## 이 기술 정보
- id: `u68` · 분류: 2D · 화면 › CSS · 화면 틀 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 크기가 바뀔 때 transform 하나만 바꾼다. 매 프레임 비용 없음.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u68
- 쓰면 좋을 때: 서류 · 카드처럼 배치가 촘촘해 반응형으로 다시 짜기 어려운 화면 / 그림 위 정확한 자리에 글씨 · 단추를 얹는 화면
- 쓰지 말 때: 글이 긴 읽기 화면 — 폰에서 글씨가 너무 작아진다. 그럴 땐 반응형 배치 / 판 안에서 다시 vw · vh 단위를 쓰기 — 이중으로 줄어든다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u68 견본 항목 — `src/demos/demosDraw2d.ts:1568`
```ts
  u68: {
    kind: '2d',
    caption: '1600×900 판 하나를 PC · 태블릿 · 가로 폰 화면에 맞춰 통째로 확대/축소 (남는 곳은 검은 띠)',
    make() {
      const DEV: [string, number, number][] = [
        ['PC 1920×1200', 1920, 1200],
        ['태블릿 1180×820', 1180, 820],
        ['가로 폰 844×390', 844, 390],
      ];
      const board = (g: G): void => {
        const gr = g.createLinearGradient(0, 0, 0, 900);
        gr.addColorStop(0, '#6b4a2e');
        gr.addColorStop(1, '#3d2816');
        g.fillStyle = gr;
        g.fillRect(0, 0, 1600, 900);
        g.fillStyle = '#1c1430';
        g.fillRect(0, 0, 1600, 110);
        txt(g, '1600 × 900 판', 800, 58, 62, '#ffe28a', 'center', TF, 400);
        g.fillStyle = '#fff6e0';
        rr(g, 70, 170, 560, 650, 24);
        g.fill();
        g.fillStyle = '#c9b48a';
        for (let i = 0; i < 7; i++) g.fillRect(120, 260 + i * 70, 460, 14);
        txt(g, '자료', 350, 215, 46, '#6e4510', 'center', TF, 400);
        for (let i = 0; i < 3; i++) {
          g.fillStyle = ['#7fbcff', '#ffcb7a', '#ffa6c9'][i]!;
          rr(g, 700 + i * 290, 170, 250, 330, 22);
          g.fill();
        }
        g.fillStyle = '#e8c25a';
        rr(g, 1150, 640, 380, 150, 75);
        g.fill();
        txt(g, '판정', 1340, 717, 70, '#4a2a00', 'center', TF, 400);
      };
      return {
        draw(g, w, h, t) {
          const u = scaleOf(w, h);
          bg(g, w, h, '#2a3a6e', '#121a3a');
          const P = 2.6;
          const i0 = Math.floor(t / P) % 3;
          const i1 = (i0 + 1) % 3;
          const k = ease(((t % P) - 1.8) / 0.8);
          const d0 = DEV[i0]!;
          const d1 = DEV[i1]!;
          const rw = lerp(d0[1], d1[1], k);
          const rh = lerp(d0[2], d1[2], k);
          // 실제 크기 → 그림 크기 (모든 기기 같은 비율로 줄여 그림)
          const vs = Math.min((w * 0.86) / 1920, (h * 0.66) / 1200);
          const sw = rw * vs;
          const sh = rh * vs;
          const sx = (w - sw) / 2;
          const sy = h * 0.43 - sh / 2;
          const bez = 5 * u;
          g.fillStyle = '#0b0d18';
          rr(g, sx - bez, sy - bez, sw + bez * 2, sh + bez * 2, 7 * u);
          g.fill();
          g.strokeStyle = '#59607e';
          g.lineWidth = 1.5 * u;
          g.stroke();
          // 화면 + 판
          g.save();
          g.beginPath();
          g.rect(sx, sy, sw, sh);
          g.clip();
          g.fillStyle = '#000';
          g.fillRect(sx, sy, sw, sh);
          g.strokeStyle = 'rgba(255,255,255,.07)';
          g.lineWidth = 1;
          g.beginPath();
          for (let x = -sh; x < sw; x += 6 * u) {
            g.moveTo(sx + x, sy + sh);
            g.lineTo(sx + x + sh, sy);
          }
          g.stroke();
          const s = Math.min(sw / 1600, sh / 900);
          const bw = 1600 * s;
          const bh = 900 * s;
          g.translate(sx + (sw - bw) / 2, sy + (sh - bh) / 2);
          g.scale(s, s);
          board(g);
          g.restore();
          const real = Math.min(rw / 1600, rh / 900);
          txt(g, k < 0.5 ? d0[0] : d1[0], w / 2, h * 0.84, 10 * u, '#fff');
          pill(g, `× ${real.toFixed(2)}`, w / 2, h * 0.93, 8.5 * u, '#ff6fa8');
        },
      };
    },
  }
```

## 관련 기술
- 다음에 해 볼 기술: [넘칠 때만 줄이기 (자동 축소)](https://ai-techstudio.web.app/ai/t/u70.md) `u70` · [화면 방향 고정 · 노치 피하기](https://ai-techstudio.web.app/ai/t/u71.md) `u71`
- 참고 문서: [MDN — ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
