Skip to content

feat: 이동·동작 블록 카탈로그 + 시뮬레이션 결과 화면 — main 반영 (#34, #36) - #38

Merged
wook10 merged 5 commits into
mainfrom
feature/34-block-catalog
Sep 16, 2026
Merged

wook10 merged 5 commits into
mainfrom
feature/34-block-catalog

Conversation

@wook10

@wook10 wook10 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

배경

#35·#37이 스택 PR 구조상 각각 feature/32-block-delete·feature/34-block-catalog로 머지돼(원래 계획대로 #33 머지 후 base를 main으로 옮겼어야 했는데, 그 전에 세 PR이 순서대로 머지됨), 실제로는 main에 조각1(#33)만 반영되고 조각2(#34 이동·동작 블록 카탈로그)·조각3(#36 시뮬레이션 결과 화면)은 아직 main에 없습니다. 이 PR로 그 둘을 main에 반영합니다.

Closes #34
Closes #36

포함 내용

각 조각의 상세 내용·기명서 대조표는 #35, #37 설명을 참고해 주세요. 새 코드는 없고, 이미 리뷰·테스트를 마친 커밋들을 main 에 올리는 PR입니다.

검증

  • 전체 122개 테스트 통과, typecheck/lint/build/format:check 클린 (feature/34-block-catalog 브랜치 기준 재확인 완료)

🤖 Generated with Claude Code

wook10 and others added 5 commits September 16, 2026 22:33
Figma Slide 5(카탈로그 레퍼런스 시트)에 정의된 이동 5종(뒤로·앞으로·오른쪽
회전·왼쪽 회전·정지)·동작 7종(앉기·일어서기·인사하기·하트·춤추기·구르기·
공격) 블록을 추가하고, '흐름'으로 고정돼 있던 팔레트 카테고리 전환을
복원했다(Figma 카탈로그가 근거가 되어 894697f에서 제거했던 걸 다시 도입).

- BlockKind/BlockNode/SerializedBlockNode 에 새 종류 추가
- 회전 각도 허용 범위(1~90도)는 기명서·Figma 모두 값이 없어 사용자 확인
- 뒤로/앞으로 이동은 같은 총 이동 거리(안전 구역) 계산에 포함, 회전은 거리가
  아니라 포함하지 않음
- BlockPalette: selectedCategory state 로 4개 카테고리 전환, 카테고리별
  블록 목록 렌더

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
inryeok-bot MEDIUM·CORRECTNESS: 시작 카테고리에서 newBlock('start')·
newBlock('end') 를 매번 새로 만들 수 있었다. 시작은 프로그램에 정확히
하나, 종료는 많아야 하나여야 하는데, 런타임 검증(validateBlockProgram·
evaluateProgram)은 이를 검사하지 않아 중복 상태로도 시뮬레이션·실행까지
통과한다. 반면 자동 저장 스냅샷 검증(isBlockProgramSnapshot)은 중복을
거부해서, 다음 방문 때 초안 전체가 INITIAL_PROGRAM 으로 조용히 사라진다.

- 시작은 팔레트에서 그림만 보여주고 더 이상 꺼낼 수 없다 (고정 진입점)
- 종료는 프로그램 어디에도(스택·자유 블록) 아직 없을 때만 꺼낼 수 있다
  (hasBlockKind 로 확인) — 쓰레기통으로 지우면 다시 꺼낼 수 있다
- BlockPalette 가 program 을 받아 이 조건을 판단하도록 prop 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Simulation "시뮬레이션 실행"이 페이지 필요:YES 로 바뀌어, 워크스페이스 안
텍스트 힌트 대신 전용 결과 화면(Figma Slide 6 실패·Slide 7 성공)을 새로
분리했다.

- SimulationResult.failedAtIndex 추가: repeat 를 실제 실행 횟수만큼 펼친
  순서 기준으로 안전 구역을 벗어난 지점(1-based) — evaluateProgram 이 계산
- views/experience/model: unrollExecutionSteps — 같은 펼치기 규칙을 클라이
  언트에서 재사용해 "실행순서" 단계 목록을 만든다
- SimulationResultView: 결과 카드(성공/실패) + 실행순서(완료/문제 발생/
  대기) + 실행 블록(BlockStack 재사용) + 로봇 미리보기(배지·진행 바)
- simulationResultStorage: ExperienceView ↔ 결과 화면 간 program·result
  스냅샷을 sessionStorage 로 전달 (라우트 이동 시 리액트 state 소실 보완)
- ExperienceView: 시뮬레이션 성공/실패 모두 결과 화면으로 이동, 결과
  화면에서 돌아왔을 때(같은 program 이면) 통과 상태를 이어받아
  RunReadyWorkspace 를 그대로 보여준다
- '블록 고치기'·'로봇 실행하기' 버튼은 모두 /experience 로 돌아간다 —
  실제 실행 요청은 기존 RunReadyWorkspace 흐름 그대로 유지

테스트 인프라: next/navigation useRouter 를 테스트 전역에서 mock (기존
"invariant expected app router to be mounted" 회피) — shared/testing/routerMock.ts

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
inryeok-bot MEDIUM 2건:

1. ERROR_HANDLING — writeSimulationResult() 가 sessionStorage 저장 실패
   (용량 초과·접근 거부 등)를 내부에서 삼키는데도 호출자는 항상 결과
   화면으로 이동했다. 그 화면은 읽을 게 없어 곧장 /experience 로 돌아가
   방금 끝난 시뮬레이션 결과가 안내 없이 사라졌다.
   → writeSimulationResult 가 성공 여부를 반환하고, 실패하면 이동하지
     않는다(이 화면의 기존 인라인 안내로 계속 보여준다).

2. DATA_INTEGRITY — readSimulationResult() 가 JSON.parse 결과를 구조
   검증 없이 StoredSimulationResult 로 단언했다. 필드가 빠지거나 타입이
   어긋난 저장값도 그대로 신뢰해, 결과 화면이 program.stack 등에 접근
   하다 예외로 죽을 수 있었다.
   → program 은 기존 isBlockProgramSnapshot 재사용, result 는 새
     isSimulationResultShape 로 필드·타입을 확인하고, 하나라도 어긋나면
     null 로 처리한다(localDraft 복원과 같은 방어 패턴).

테스트: simulationResultStorage.test.ts 신규(왕복·저장 실패·손상값 4종),
ExperienceView.test.tsx 에 저장 실패 시 이동하지 않고 인라인으로 계속
보여주는 통합 테스트 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
feat: 시뮬레이션 결과 화면 분리 — 새 라우트 /experience/simulation (#36)
@wook10
wook10 merged commit 0880a7b into main Sep 16, 2026
1 check passed

@inryeok-bot inryeok-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 결과

변경된 22개 파일을 검토했으며, 수정이 필요한 문제 2개를 발견했습니다.

심각도 개수
Critical 0
High 1
Medium 1
Low 0

주요 내용

  • HIGH · PERFORMANCE — 안전 구역 판정 전에 전체 반복 트리를 펼쳐 메모리가 고갈될 수 있습니다
  • MEDIUM · CORRECTNESS — 렌더 중 sessionStorage를 읽어 새로고침 시 hydration 결과가 달라집니다

검토한 head: b3b00bc6314e

*/
function safeZoneBreachIndex(nodes: SerializedBlockNode[]): number | undefined {
let cumulative = 0;
const steps = unroll(nodes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH · PERFORMANCE

안전 구역 판정 전에 전체 반복 트리를 펼쳐 메모리가 고갈될 수 있습니다

safeZoneBreachIndex는 위반 지점을 순회하기 전에 unroll(nodes)로 모든 실행 단계를 배열에 생성합니다. 반복 횟수는 각 노드에서 유효하더라도 중첩 깊이에 따라 실행 단계 수가 지수적으로 증가할 수 있습니다.

안전 구역을 초과하는 깊은 중첩 프로그램을 평가하면 실제 위반은 초반에 발생하더라도 전체 배열을 먼저 만들기 때문에 API 처리 스레드가 장시간 멈추거나 OOM으로 종료될 수 있습니다. 반복 트리를 재귀적으로 순회하면서 누적 거리가 한도를 넘는 즉시 중단하고, 실행 인덱스만 누적하도록 변경하는 것이 안전합니다.


export function SimulationResultView() {
const router = useRouter();
const stored = useMemo(() => readSimulationResult(), []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MEDIUM · CORRECTNESS

렌더 중 sessionStorage를 읽어 새로고침 시 hydration 결과가 달라집니다

이 Client Component도 초기 요청에서는 서버에서 렌더되므로 서버의 readSimulationResult()는 항상 null을 반환합니다. 같은 탭에서 결과가 저장된 상태로 페이지를 새로고침하면 클라이언트 최초 렌더에서는 값이 존재해 전체 결과 화면을 만들기 때문에, 서버의 빈 마크업과 일치하지 않습니다.

저장값은 마운트 후 useEffect에서 읽어 상태에 넣거나, 서버 스냅샷을 명시할 수 있는 외부 저장소 패턴을 사용해야 합니다. 로딩 전용 마크업을 서버와 클라이언트 최초 렌더에서 동일하게 유지하는 것이 필요합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 시뮬레이션 결과 화면 분리 (새 라우트) feat: 이동·동작 전체 블록 카탈로그 + 카테고리 전환

1 participant