GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

그래프 그리기 9종 — 계산으로 그리는 데이터 시각화 차트

아홉 종류 중 셋은 리본처럼 흐르고, 셋은 막대로 끊어지고, 둘은 원으로 뭉치고, 하나는 깔때기 모양으로 좁아진다. 그래프 그리기 아홉 개를 값 하나가 바뀔 때마다 좌표를 다시 계산해서 그리는 방식으로 모았다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

그래프 그리기 9가지 — 값 하나가 차트의 모양을 어떻게 바꾸는가

아홉 개 모두 라이브러리 없이 캔버스 2D 위에 좌표를 계산해 그린다. 산키(01)는 리본 굵기가 흐름 값에 비례하고, 워터폴(02)은 막대 높이가 누적값을 따라가며, 퍼널(03)은 사다리꼴 폭이 단계별 비율로 좁아진다. 버블팩(04)은 값 크기가 원 반지름이 되어 서로 겹치지 않을 때까지 자리를 반복 계산하고, 박스플롯(05)은 정렬한 값의 사분위 경계가 상자 높이와 수염 길이로 바뀐다. 평행 좌표(06)는 값 하나마다 세로축 위 점 하나를 정해 선으로 잇고, 콕스콤(07)은 값 크기가 반지름이 되어 부채꼴로 펼쳐지며, 범프(08)는 순위가 세로 좌표가 되어 두 구간의 순위가 바뀔 때 선이 교차한다. 캔들스틱(09)은 시가·고가·저가·종가 네 값을 몸통과 꼬리 두 도형으로 압축한다.

차트 종류별 코드

항목마다 좌표를 계산하는 함수 본문만 그대로 옮겨 실었다 — 나머지 초기화·이벤트 코드는 압축 파일 안에 있다.

01굵기가 흐름 값을 나타내는 산키 리본 다이어그램

리본 하나의 굵기를 그 구간이 나르는 값의 크기에 맞춰 계산해, 굵은 리본일수록 더 많은 양이 흘러가는 것을 한눈에 보여준다. 예산 배분이나 트래픽 유입 경로처럼 값이 여러 갈래로 나뉘어 흐르는 자리에 쓴다.

굵기가 흐름 값을 나타내는 산키 리본 다이어그램
새 창에서 열기

01 · 굵기가 흐름 값을 나타내는 산키 리본 다이어그램 — 측정 누적면적 31.643% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SANKEY_DRAW_DURATION_MSRIBBON_STAGGER_MSbuildLayout
  function drawRibbon(r, progress, fadeAlpha) {
    var startX = layout.leftX + layout.nodeW;
    var endX = layout.rightX;
    var midX = (startX + endX) / 2;
    var clipRight = lerp(startX, endX, progress);
    ctx.save();
    ctx.beginPath();
    ctx.rect(0, 0, clipRight, ch);
    ctx.clip();
    ctx.beginPath();
    ctx.moveTo(startX, r.leftTop);
    ctx.bezierCurveTo(midX, r.leftTop, midX, r.rightTop, endX, r.rightTop);
    ctx.lineTo(endX, r.rightBot);
    ctx.bezierCurveTo(midX, r.rightBot, midX, r.leftBot, startX, r.leftBot);
    ctx.closePath();
    var rgb = r.j === 0 ? COLOR_P : COLOR_Q;
    ctx.fillStyle = 'rgba(' + rgb + ',' + (0.62 * fadeAlpha) + ')';
    ctx.fill();
    ctx.restore();

02증감을 이어 붙이는 워터폴 브리지 차트

막대 하나가 끝나는 높이에서 다음 막대가 시작하도록 누적값을 계산해, 값이 오르내리는 과정이 다리처럼 이어져 보인다. 월별 매출 증감 브리핑 슬라이드에 쓴다.

증감을 이어 붙이는 워터폴 브리지 차트
새 창에서 열기

02 · 증감을 이어 붙이는 워터폴 브리지 차트 — 측정 누적면적 10.019% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BAR_GROW_MSBAR_STAGGER_MSCOLOR_DOWN
    for (i = 0; i < n; i += 1) {
      var b = bars[i];
      var delay = i * BAR_STAGGER_MS;
      var progress = easeOutCubic(clamp((t - delay) / BAR_GROW_MS, 0, 1));
      var currentVal = lerp(b.anchor, b.target, progress);
      var topVal = Math.max(b.anchor, currentVal);
      var botVal = Math.min(b.anchor, currentVal);
      var x = marginX + slotW * i + (slotW - barW) / 2;
      var y1 = yFor(topVal);
      var y2 = yFor(botVal);
      var color = b.kind === 'total' ? COLOR_TOTAL : (b.value >= 0 ? COLOR_UP : COLOR_DOWN);
      ctx.fillStyle = color;
      ctx.fillRect(x, y1, barW, Math.max(1, y2 - y1));
    }

03단계마다 폭이 줄어드는 퍼널 차트

각 단계의 값 비율만큼 사다리꼴 폭을 계산해 좁혀, 이전 단계에서 얼마나 이탈했는지가 폭 차이로 드러난다. 가입부터 결제까지 전환 단계 리포트에 쓴다.

단계마다 폭이 줄어드는 퍼널 차트
새 창에서 열기

03 · 단계마다 폭이 줄어드는 퍼널 차트 — 측정 누적면적 29.776% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

makeFunnelDATASET_ASTAGE_COLORS
    for (i = 0; i < values.length; i += 1) {
      var topVal = values[i];
      var botVal = i < values.length - 1 ? values[i + 1] : values[i] * 0.86;
      var topW = (topVal / 100) * maxWidth;
      var botW = (botVal / 100) * maxWidth;
      var y0 = marginY + bandH * i;
      var y1 = y0 + bandH;

      ctx.beginPath();
      ctx.moveTo(cx - topW / 2, y0);
      ctx.lineTo(cx + topW / 2, y0);
      ctx.lineTo(cx + botW / 2, y1);
      ctx.lineTo(cx - botW / 2, y1);
      ctx.closePath();
      ctx.fillStyle = lerpColor(STAGE_COLORS[i], STAGE_COLORS_B[i], blend);
      ctx.fill();

04값 크기만큼 원이 서로 밀어내는 버블팩 차트

값이 큰 항목일수록 반지름을 크게 잡고 원끼리 겹치지 않을 때까지 위치를 반복 계산해, 크기만으로 값이 비교된다. 이 반복 횟수를 BUBBLE_PACK_COLLISION_ITERATIONS 값으로 조절하며, 카테고리별 매출 비중 비교 카드에 쓴다.

값 크기만큼 원이 서로 밀어내는 버블팩 차트
새 창에서 열기

04 · 값 크기만큼 원이 서로 밀어내는 버블팩 차트 — 측정 누적면적 25.791% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BUBBLE_PACK_COLLISION_ITERATIONSsnapshotsradii
  function draw(t) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);

    var et = Math.max(0, t - T0);
    var seg = (LOOP_MS - T0) / PACKS.length;
    var segIdx = Math.min(PACKS.length - 1, Math.floor(et / seg));
    var localT = et - segIdx * seg;
    var unit = Math.max(0, Math.min(1, localT / seg));
    var tri = unit < 0.5 ? unit * 2 : 2 - unit * 2;
    // 완화 초반(적은 반복)이 위치 변화가 크고 후반은 변화가 작아, 시간을 선형으로 매핑하면
    // 뒤쪽 프레임이 정지해 보인다 — frac 을 볼록(위로 가속)하게 눌러 반복 인덱스가
    // 초반엔 느리게(그래도 위치는 크게 바뀜), 후반엔 빠르게 지나가 프레임마다 변화가 이어진다.
    var frac = Math.pow(tri, 3.6);
    var iterFrac = frac * BUBBLE_PACK_COLLISION_ITERATIONS;
    var lo = Math.floor(iterFrac);
    var hi = Math.min(lo + 1, BUBBLE_PACK_COLLISION_ITERATIONS);
    var f2 = iterFrac - lo;
    var pack = PACKS[segIdx];
    var snapLo = pack.snapshots[lo], snapHi = pack.snapshots[hi];

05사분위수를 상자와 수염으로 그리는 박스플롯

값들을 정렬해 사분위 경계를 계산한 뒤 상자 높이와 수염 길이로 옮겨 그려, 중앙값과 흩어진 정도가 한 도형에 담긴다. 두 구간 사이를 사분위 다섯 값 전부 보간해 넘기고, 값이 한쪽으로 쏠릴 때만 이상치 점을 조건부로 찍는다. 그룹별 응답 시간 분포 비교에 쓴다.

사분위수를 상자와 수염으로 그리는 박스플롯
새 창에서 열기

05 · 사분위수를 상자와 수염으로 그리는 박스플롯 — 측정 누적면적 12.065% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GROUP_A_1quartileslerpStats
  function draw(phase) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);
    var t = Math.max(0, phase - T0);
    var half = (LOOP_MS - T0) / 2;
    var blend = t < half ? t / half : 2 - t / half;
    blend = clamp(blend, 0, 1);
    var statsA = lerpStats(qA1, qA2, blend);
    var statsB = lerpStats(qB1, qB2, blend);
    drawBox(GROUP_X[0], statsA, BOX_FILL[0], sx, sy, blend > 0.62);
    drawBox(GROUP_X[1], statsB, BOX_FILL[1], sx, sy, blend < 0.38);
  }

06여러 축을 가로지르는 선으로 값을 잇는 평행 좌표 차트

항목 하나의 여러 값을 각 세로축 위 좌표로 계산해 선분으로 잇고, 축 하나를 드래그로 훑으면 그 구간을 지나는 선만 강조된다. 다변량 상품 스펙 비교표에 쓰며, 마우스가 없어도 화살표 위·아래 키로 같은 구간을 훑을 수 있다.

여러 축을 가로지르는 선으로 값을 잇는 평행 좌표 차트
새 창에서 열기

06 · 여러 축을 가로지르는 선으로 값을 잇는 평행 좌표 차트 — 측정 누적면적 46.867% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BRUSH_AXIS_INDEXsetBrushCenterautoDemoStep
    for (var it = 0; it < ITEM_COUNT; it += 1) {
      var v = DATA[it][BRUSH_AXIS_INDEX];
      var inBand = v >= lo && v <= hi;
      ctx.strokeStyle = inBand ? '#2f6df6' : 'rgba(23,20,26,.22)';
      ctx.lineWidth = inBand ? 2.5 : 1;
      ctx.beginPath();
      for (var a2 = 0; a2 < AXIS_COUNT; a2 += 1) {
        var px = axisX(a2), py = valueY(DATA[it][a2]);
        if (a2 === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
      }
      ctx.stroke();
    }

07값마다 반지름이 다른 꽃잎으로 펼치는 콕스콤 차트

항목 수만큼 각도를 나눠 부채꼴을 그리고 값의 크기를 반지름으로 환산해, 같은 폭이라도 값이 클수록 꽃잎이 바깥까지 뻗는다. 꽃잎 사이 각도를 COXCOMB_PETAL_GAP_DEG 값으로 벌리며, 월별 반복 지표 비교 대시보드 카드에 쓴다.

값마다 반지름이 다른 꽃잎으로 펼치는 콕스콤 차트
새 창에서 열기

07 · 값마다 반지름이 다른 꽃잎으로 펼치는 콕스콤 차트 — 측정 누적면적 19.5% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COXCOMB_PETAL_GAP_DEGtargetRadiiradiusAt
  function draw(t) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    var scale = (sx + sy) / 2;
    ctx.clearRect(0, 0, cw, ch);
    var cx = CENTER_X * sx, cy = CENTER_Y * sy;
    var startDeg = -90;
    for (var i = 0; i < COUNT; i += 1) {
      var r = radiusAt(i, t) * scale;
      var a0 = startDeg * Math.PI / 180;
      var a1 = (startDeg + anglePerPetal) * Math.PI / 180;
      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.arc(cx, cy, r, a0, a1);
      ctx.closePath();
      ctx.fillStyle = 'rgba(' + FILL_COLORS[i % 4] + ',.75)';
      ctx.fill();
      ctx.strokeStyle = 'rgba(' + INK_STROKE + ',.9)';
      ctx.lineWidth = 1;
      ctx.stroke();
      startDeg += anglePerPetal + COXCOMB_PETAL_GAP_DEG;
    }

08순위가 바뀔 때 선이 교차하는 범프 차트

구간마다 값 순서를 다시 매겨 순위를 세로 좌표로 계산해, 두 항목의 순위가 뒤바뀌는 지점에서 선이 서로 교차한다. 분기별 인기 상품 순위 변화표에 쓴다.

순위가 바뀔 때 선이 교차하는 범프 차트
새 창에서 열기

08 · 순위가 바뀔 때 선이 교차하는 범프 차트 — 측정 누적면적 42.761% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ranksOfPERIOD_COUNTSEG_DUR
    for (var s = 0; s < SEG_COUNT; s += 1) {
      var raw = (t - T0 - s * SEG_MS) / SEG_DUR;
      if (raw < 0) continue;
      var flashing = raw >= 0.9 && raw <= 1.2;
      if (!flashing) continue;
      for (var a = 0; a < ITEM_COUNT; a += 1) {
        for (var b = a + 1; b < ITEM_COUNT; b += 1) {
          var y1a = RANKS[s][a], y1b = RANKS[s + 1][a];
          var y2a = RANKS[s][b], y2b = RANKS[s + 1][b];
          var Da = y1a - y2a, Db = y1b - y2b;
          if ((Da > 0) === (Db > 0)) continue;
          var f = Da / (Da - Db);
          if (f < 0 || f > 1) continue;
          crossings.push({ s: s, f: f, rank: y1a + (y1b - y1a) * f });
        }
      }

09몸통과 꼬리로 시가·고가·저가·종가를 그리는 캔들스틱 차트

시가와 종가 사이를 몸통 사각형으로, 고가·저가까지를 얇은 꼬리선으로 계산해 그려, 막대 하나에 그날의 값 네 개가 겹쳐 담긴다. 아홉 항목 중 유일하게 움직인 프레임이 23/23 으로 잡혀, 캔들이 하나씩 몸통을 채우는 동안 화면이 한 번도 쉬지 않는다. 코드로 만드는 주가·환율 히스토리 패널에 쓴다.

몸통과 꼬리로 시가·고가·저가·종가를 그리는 캔들스틱 차트
새 창에서 열기

09 · 몸통과 꼬리로 시가·고가·저가·종가를 그리는 캔들스틱 차트 — 측정 누적면적 13.603% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CANDLE_REVEAL_STAGGER_MSgrowPnearestCandle
    for (var idx = 0; idx < CANDLE_COUNT; idx += 1) {
      var c = candles[idx];
      var grow = easeOutCubic(growP(idx, t));
      var x = candleX(idx);

      var wickTopFull = priceY(c.high), wickBotFull = priceY(c.low);
      var wickCenter = (wickTopFull + wickBotFull) / 2;
      var wickHalf = (wickBotFull - wickTopFull) / 2 * grow;
      ctx.strokeStyle = '#9a93a6';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(x, wickCenter - wickHalf);
      ctx.lineTo(x, wickCenter + wickHalf);
      ctx.stroke();

      var bodyTopFull = priceY(Math.max(c.open, c.close));
      var bodyBotFull = priceY(Math.min(c.open, c.close));
      var bodyCenter = (bodyTopFull + bodyBotFull) / 2;
      var bodyHalf = Math.max(1, (bodyBotFull - bodyTopFull) / 2 * grow);
      var up = c.close >= c.open;
      ctx.fillStyle = up ? '#3ddc97' : '#ff7a5e';
      ctx.fillRect(x - bw / 2, bodyCenter - bodyHalf, bw, bodyHalf * 2);

값이 그리기 결과를 바꾸는 곳 — 리본 굵기·원 반지름·꽃잎 각도

01은 SANKEY_DRAW_DURATION_MS 하나로 리본이 왼쪽에서 오른쪽까지 번지는 시간을 정하고, RIBBON_STAGGER_MS 값만큼 리본마다 시작 시각을 밀어 겹치지 않게 순서를 준다. 02는 BAR_GROW_MS·BAR_STAGGER_MS 로 막대가 자라는 속도와 순서를 나누고, 값이 음수면 COLOR_DOWN 색으로 갈라 오르내림을 색으로도 구분한다. 04는 BUBBLE_PACK_COLLISION_ITERATIONS 값이 클수록 원끼리 겹침을 더 오래 풀어내고, 07은 COXCOMB_PETAL_GAP_DEG 값이 꽃잎 사이 틈을 벌려 부채꼴 하나하나를 갈라놓는다. 값 하나를 바꾸면 그리기 결과 전체가 다시 계산된다.

잰 값 아홉 항목의 실제 변동 — 24장 캡처 기준

프레임 24장을 찍어 그 사이 델타 23개를 비교한 값이다 — 화면이 실제로 얼마나 바뀌는지는 감이 아니라 이 표로 잰다.

항목 누적면적 평균 강도 움직인 프레임
01 산키 리본 31.6% 88.9 19/23
02 워터폴 브리지 10.0% 171.7 22/23
03 퍼널 차트 29.8% 40.7 21/23
04 버블팩 25.8% 117.6 18/23
05 박스플롯 12.1% 128.2 20/23
06 평행 좌표 46.9% 33.6 21/23
07 콕스콤 19.5% 137.9 17/23
08 범프 차트 42.8% 75.8 20/23
09 캔들스틱 13.6% 121.0 23/23

평행 좌표(06)가 누적면적 46.9% 로 가장 넓게 바뀌고, 캔들스틱(09)이 23/23 으로 프레임마다 빠짐없이 움직인다.

어디서 깨지는가 — 동작 줄임 설정·좁은 화면·값이 0이거나 음수일 때

아홉 항목 모두 prefers-reduced-motion 을 matchMedia 로 감지해, 접근성 설정을 켠 브라우저에서는 애니메이션 클래스를 떼고 완성된 모양 하나만 정적으로 그린다. 좁은 화면에서는 버블팩(04)·박스플롯(05)·콕스콤(07)이 CALC_W 기준 캔버스를 sx·sy 배율로 다시 맞추는데, 폭이 아주 좁아지면 라벨과 도형 사이 간격이 촘촘해질 수 있다. 값이 0이거나 음수인 자리도 그냥 사라지지 않도록 짜여 있다 — 워터폴(02)은 증감이 0이어도 막대 높이를 최소 1px 로 그려 막대가 화면에서 없어지지 않고, 캔들스틱(09)도 시가와 종가가 같아지는 구간에서 몸통 높이가 0이 아니라 최소 1px 로 유지된다. 퍼널(03)은 마지막 단계 다음 값이 없을 때만 이전 단계 값의 0.86배로 대신 채워, 마지막 사다리꼴 폭이 완전히 0으로 사라지지 않게 막는다. 아홉 항목 전체 코드는 이 zip 안에 들어 있다.

변형 세 가지와 접근성

값 하나만 바꿔도 그리기 결과가 완전히 달라지는 자리 세 곳을 표로 남긴다.

변형 값 느낌
산키 리본 그리기 속도 조정 01 SANKEY_DRAW_DURATION_MS 900 → 1800 값을 줄이면 리본이 빠르게 한 번에 그어지고, 값을 늘리면 리본이 천천히 갈라지며 흐름을 따라가듯 그려진다.
버블팩 충돌 반복 횟수 조정 04 BUBBLE_PACK_COLLISION_ITERATIONS 30 → 80 값을 줄이면 원들이 서로 겹친 채로 빨리 자리를 잡고, 값을 늘리면 겹침이 완전히 풀릴 때까지 자리를 조금씩 더 오래 다시 잡는다.
콕스콤 꽃잎 사이 각도 조정 07 COXCOMB_PETAL_GAP_DEG 2 → 10 값을 줄이면 꽃잎이 서로 거의 맞닿아 하나의 원반처럼 보이고, 값을 늘리면 꽃잎 사이가 벌어져 각 조각이 또렷하게 구분된다.

접근성은 표 밖에서도 챙긴다. 아홉 항목 전부 reduced-motion 설정을 감지해 최종 모양으로 멈추고, 평행 좌표(06)는 포인터 없이도 화살표 위·아래 키로 브러시 구간을 옮길 수 있으며, 캔들스틱(09)은 화살표 좌·우 키로 캔들 하나씩 탐색할 수 있다. 이 압축 파일을 열어 아홉 개 데모 코드를 전부 내려받으려면 비밀번호로 z7mev6vs 를 그대로 입력하면 된다.

데이터 시각화 차트 종류는 서로 뭐가 다른가?

데이터 시각화 차트 종류는 서로 다른 계산 하나씩을 값에 걸어 모양을 만든다는 점에서 갈린다. 산키·워터폴·범프는 좌표 하나를 시간이나 순위에 따라 이어 흐름을 보여주고, 버블팩·콕스콤은 값을 반지름으로 바꿔 원이나 부채꼴로 뭉치며, 박스플롯·평행 좌표는 여러 값을 한 축 위에 나란히 놓고 비교한다. 캔들스틱은 주식 차트 종류에서 흔히 보이지만, 이 데모는 시세를 판단하는 도구가 아니라 시가·고가·저가·종가 네 값을 몸통과 꼬리로 계산해 그리는 부분만 다룬다. 이 아홉 가지는 차트 종류 와 용도가 서로 달라 무엇을 강조하고 싶은지에 따라 골라 쓴다. 굵기로 흐름을 나타내는 값 계산은 css 그래프 만들기 SVG로 9종 — 라이브러리 없이에서 SVG path 로도 확인할 수 있고, 같은 계산을 requestAnimationFrame 루프에 얹는 방식은 canvas 그래프 그리기 — 라이브러리 없이 그리는 애니메이션 차트 9종에 있다. 값이 바뀔 때 도형이 아니라 상태가 바뀌는 쪽은 대시보드 차트 디자인 인터랙션 9종 — 호버부터 드릴다운까지에서 다룬다. 아홉 항목 모두 CanvasRenderingContext2D 하나로만 그려, 그리기 라이브러리 없이도 아홉 종류를 전부 만들 수 있다는 것을 보여준다.

그래프 그리기 아홉 가지에 대해 자주 나오는 질문 셋

아홉 개 차트를 그리는 데 별도 그리기 라이브러리가 필요한가요?

필요 없다. 아홉 항목 모두 CanvasRenderingContext2D 기본 API 와 requestAnimationFrame 루프만으로 좌표를 계산해 그리며, 외부 차트 라이브러리는 하나도 쓰지 않는다.

그래프 그리기 연습으로 아홉 종류를 전부 손으로 옮겨 봐도 되나요?

된다. 각 항목 코드는 6~25줄짜리 핵심 계산 구간만 옮겨 뒀고, 나머지 초기화·이벤트 코드까지 전부 압축 파일 안에 있어 하나씩 옮겨 적으며 계산 순서를 따라가 볼 수 있다.

캔들스틱처럼 값이 겹치는 차트는 접근성 설정에서 어떻게 보이나요?

reduced-motion 을 켜면 캔들 아홉 개가 순서대로 자라나는 과정 없이 처음부터 시가·고가·저가·종가가 전부 반영된 완성된 모양으로 바로 그려진다. 화살표 좌·우 키로 캔들을 하나씩 옮겨 다니며 몸통과 꼬리 값을 확인할 수도 있다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.