/** * N-Back "숫자 기차" (수정사항-021-2, 021-3, 021-4) * * 가로형 컨베이어: * - 맨 왼쪽 [ ? ] 박스는 고정 (수정사항-021-4 — 슬라이드 시 움직이지 않음) * - [ ? ] 오른쪽의 나머지 박스들만 우→좌로 한 칸 슬라이드 (0.5s) * - 셀 구성 (레벨별 셀 수): * 1-Back: [?][현재] (고정 '?' + 슬라이딩 1칸) * 2-Back: [?][ ][현재] (고정 '?' + 슬라이딩 2칸) * 3-Back: [?][ ][ ][현재] (고정 '?' + 슬라이딩 3칸) * - 중간 빈 칸: 물음표도 아니고 숫자도 표시하지 않는 구간 (수정사항-021-3) * * 슬라이드 원리 (뷰포트=슬라이딩 영역 nback칸, 파이프라인=nback+1칸, nback=2 예시): * - idle: [?] | [ ][현재] translateX=0 * - 슬라이드: [?] | [현재][다음] translateX=-1칸 (0.5s) * - 리셋: [?] | [ ][다음] translateX=0 즉시 복귀 → 무감지 * → 현재 숫자가 좌측(고정 '?' 방향)으로 슬라이드되어 사라지고, 다음 숫자가 우측에서 등장 */ import styled, { css } from 'styled-components'; interface NBackTrainProps { nback: number; // 1 | 2 | 3 value: number | null; // 현재 숫자 animating: boolean; nextValue?: number | null; // 우측에서 들어올 다음 숫자 } const Wrap = styled.div` --cell: clamp(88px, 24vmin, 150px); --gap: 14px; position: relative; display: flex; align-items: center; gap: var(--gap); `; /** 맨 왼쪽 [ ? ] 박스 — 고정 (애니메이션 시 움직이지 않음) */ const QuestionCell = styled.div` width: var(--cell); height: var(--cell); border-radius: 18px; display: flex; align-items: center; justify-content: center; font-weight: 800; box-sizing: border-box; flex: 0 0 auto; background: #6688c3; border: 3px solid #2c3d5a; color: #fff; font-size: calc(var(--cell) * 0.42); `; /** 슬라이딩 영역: '?' 제외 박스만 우→좌로 이동 (클리핑: nback칸만 보임) */ const Viewport = styled.div<{ visibleCells: number }>` overflow: hidden; width: ${(p) => `calc(var(--cell) * ${p.visibleCells} + var(--gap) * ${p.visibleCells - 1})`}; `; /** 파이프라인: visibleCells + 1 (entering next), 슬라이드 시 좌측1칸 클리핑 */ const Row = styled.div<{ animating: boolean; visibleCells: number }>` display: flex; gap: var(--gap); width: ${(p) => `calc(var(--cell) * ${p.visibleCells + 1} + var(--gap) * ${p.visibleCells})`}; transform: translateX( ${(p) => p.animating ? 'calc(-1 * (var(--cell) + var(--gap)))' : '0px'} ); transition: ${(p) => p.animating ? 'transform 0.5s ease-in-out' : 'none'}; `; const Cell = styled.div<{ kind: 'value' | 'next' | 'empty' }>` width: var(--cell); height: var(--cell); border-radius: 18px; display: flex; align-items: center; justify-content: center; font-weight: 800; box-sizing: border-box; flex: 0 0 auto; ${(p) => (p.kind === 'value' || p.kind === 'next') && css` background: #f0f0f8; border: 3px solid #2c3d5a; color: #002361; font-size: calc(var(--cell) * 0.52); `} ${(p) => p.kind === 'empty' && css` background: #eef1f6; border: 2px dashed #c2cadd; color: transparent; font-size: calc(var(--cell) * 0.52); `} `; export default function NBackTrain({ nback, value, animating, nextValue, }: NBackTrainProps) { const visibleCells = nback; // '?' 제외, 슬라이딩 영역에 표시되는 셀 수 // 슬라이딩 파이프라인: [empty(1)..empty(nback-1), current, next(entering)] const cells: Array<{ kind: 'value' | 'next' | 'empty'; text: string }> = []; // 중간 빈 칸 (nback-1개) — 물음표 없이 숫자 미표시 구간 (수정사항-021-3) for (let i = 1; i < nback; i++) { cells.push({ kind: 'empty', text: '' }); } // 현재 숫자 (슬라이딩 영역 우측) cells.push({ kind: 'value', text: value != null ? String(value) : '', }); // 진입 중 다음 숫자 (우측 클리핑 영역) cells.push( nextValue != null ? { kind: 'next', text: String(nextValue) } : { kind: 'empty', text: '' }, ); return ( ? {cells.map((c, i) => ( {c.text} ))} ); }