qcast-front/docs/SK_MOVEMENT_HANDOVER.md

19 KiB

SK 라인 이동 인수인계서

최초 작성일: 2026-04-22 최근 갱신일: 2026-05-06 (옵션 B 분리 — SK 라인 전용으로 축소) 대상: 후임 AI 엔지니어 또는 개발 관리자 작업 브랜치: hotfix/dev_yscha

이 문서의 범위: Straight Skeleton(SK) 라인 이동·계산 트랙만 다룹니다. SK 외 작업은 다음 별도 문서를 보세요:

  • UI_MODAL_HANDOVER.md — 모달/다이얼로그/계산기 토글
  • API_STABILITY_HANDOVER.md — API 안정화 / 인증 / 세션
  • MODULE_CIRCUIT_HANDOVER.md — 모듈/배치/회로/처마/티켓 시리즈
  • HANDOVER_INDEX.md — 진입점 인덱스

0. 이 문서가 뭔가요?

이 프로젝트(Qcast Front)에서 가장 까다롭고 위험한 부분인 "지붕 모양을 바꾸면 내부 선이 깨지는 문제" 를 어떻게 풀어왔는지 정리한 인수인계 문서입니다.

이 문서를 읽고 나면:

  1. 지붕 도면이 어떤 부품으로 구성돼 있는지 알 수 있고,
  2. 어떤 버그가 있었고 어떻게 풀었는지 따라갈 수 있고,
  3. 다음에 손대면 안 되는 코드와, 손대도 되는 코드를 구분할 수 있게 됩니다.

같이 봐야 하는 자료

  • 메모리 인덱스: ~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-front/memory/MEMORY.md
    • 모든 정책 결정·임계값·"왜 이렇게 했는지" 가 항목별로 들어 있습니다.
    • 작업을 시작하기 전에 반드시 한 번 통독하세요.

1. 도메인 1분 설명 — 지붕 도면은 어떻게 구성되나

이 앱은 건물 지붕을 그리는 CAD 같은 도구입니다. 사용자가 건물 외곽선을 그리면, 시스템이 자동으로 지붕(처마)과 그 안쪽 선(용마루 등)을 계산해 그려줍니다.

1.1 화면에 나오는 선 3종류

선 이름 색깔 누가 만드나? 어디에 저장되나?
wallLine (외벽선) 초록색 사용자가 직접 그리고 움직임 wall.lines, wall.baseLines
roofLine (처마선) 검정색 외벽선에서 일정 거리(offset)만큼 바깥쪽으로 자동 계산됨 roof.points
innerLine (내부선) 빨강 등 자동 계산 + 사용자가 추가한 용마루/추녀 roof.innerLines, canvas.skeletonLines

핵심: 사용자가 만지는 건 외벽선 뿐이고, 나머지는 자동 계산입니다. 그래서 외벽선이 움직이면 처마와 내부선이 다 같이 다시 계산됩니다.

1.2 "Straight Skeleton" (줄여서 SK) 가 뭔가요?

지붕 안쪽의 용마루·추녀 같은 선을 자동으로 그어주는 수학 알고리즘입니다. 다각형(외벽선)을 입력으로 주면, 안쪽으로 똑같은 속도로 줄어드는 가상의 선들이 만나는 자취가 나옵니다. 이게 곧 용마루 라인이 됩니다.

이 프로젝트의 가장 큰 어려움은 이 SK 알고리즘이 입력 값에 매우 예민해서, 외벽선을 조금만 잘못 움직여도 결과가 무너진다는 점입니다.

1.3 외벽선 이동의 4가지 방향

사용자가 외벽선을 잡고 끌면, 시스템은 그걸 다음 4가지로 분류합니다:

  • 위치: top (위) / bottom (아래) / left (왼쪽) / right (오른쪽)
  • 방향: in (안쪽으로) / out (바깥쪽으로)

이 8개 조합을 다 처리해야 합니다.

1.4 "오버 이동" 이란?

외벽선을 너무 많이 바깥쪽으로 끌면, 안쪽 도형이 뒤집혀 버립니다(예: ㄴ자가 ㄱ자가 됨). 이걸 "경계를 넘었다(crossed)" 고 부릅니다.

이 프로젝트의 핵심 골칫거리가 바로 이겁니다:

  • 사용자가 실수로/일부러 너무 많이 끌었을 때
  • 그동안 사용자가 힘들게 추가한 보조선들이 사라지면 안 됨
  • 동시에 SK 자동 계산은 정상적으로 다시 동작해야 함

1.5 4가지 판정 상태

이동이 들어오면 시스템이 다음 중 하나로 판정합니다:

판정 의미
ok 안전한 이동, 정상 처리
on-boundary 평행한 위치까지 딱 맞게 도달, 정상 처리
crossed 경계를 넘었음(뒤집힘), 특별 경로로 처리
unknown 판정 불가, 일단 기존 흐름 진행

판정 함수: verifyMoveBoundary() (src/util/skeleton-utils.js:510)


2. 어떤 문제가 있었나 (수정 전 증상)

  1. 외벽선을 우측 상단으로 강하게 끌면, 화면에 있지도 않은 가짜 수직선이 그려졌습니다.
  2. 외벽선을 두 번 연속 이동하면, 첫 번째 이동 결과가 사라지고 도형이 무너졌습니다.
  3. 경계를 넘는 이동을 하면 사용자가 그동안 추가한 보조선들이 다 사라졌습니다. — 이게 사용자가 가장 싫어하던 문제입니다.

3. 왜 이런 문제가 났나 (근본 원인)

3.1 좌표 기록이 짧게 잘려서 저장됐음

이동 후에 도형의 좌표를 lastPoints 라는 변수에 저장하는데, 이 저장 과정에서 점 일부가 빠졌습니다. 그래서 그다음 이동이 들어왔을 때 원본(roof.points) 과 점 개수가 안 맞아서 매칭이 깨졌습니다.

3.2 보정 함수가 항상 "원본"만 기준으로 비교했음

오버 이동을 감지하는 함수(calcOverCorrectedPoints) 가 1차 이동 결과를 무시하고 항상 원래 도형하고만 비교했습니다. 그래서 2차 이동에서 오버가 감지되면 1차 이동도 같이 되돌려버리는 부작용이 있었습니다.

3.3 화면을 먼저 지운 뒤 다시 그렸음

drawHelpLine 함수가 SK 재계산을 하기 전에 캔버스를 먼저 다 지웁니다. 그래서 SK 재계산 도중에 문제가 생겨도 화면은 이미 텅 빈 상태가 됩니다.

(2026-05-06 현재) 위 분석은 옛 정책 기준입니다. 지금은 crossed 일 때 drawSkeletonRidgeRoofFromBaseLines 라는 오버 전용 그리기 함수로 갈아타서, 화면이 비어 있어도 곧바로 다시 채워줍니다. 또 §6.1 의 클램프 정책 덕분에 애초에 crossed 자체가 잘 발생하지 않습니다.


4. 어떻게 풀었나 (2026-04-22 시점 작업)

4.1 새로 추가한 함수들 (기존 함수는 절대 안 건드림)

함수 위치 한마디로
buildRawMovedPoints(roofId, canvas) skeleton-utils.js:385 점을 가공/축소 없이 원본 그대로 이동시킨 결과를 만들어 줌
verifyMoveBoundary(roofId, canvas) skeleton-utils.js:510 "이 이동이 안전한가?" 를 4단계 (ok/on-boundary/crossed/unknown)로 판정해 줌
calcOverCorrectedPointsSafe(...) skeleton-utils.js:3222 기존 보정 함수가 "이전 이동 결과"를 잊어버리는 문제를 우회. 이전 결과를 합쳐서 가짜 원본을 만들어 넘김

4.2 안전장치(가드) 3곳

옛 정책에서는 "위험 감지 → 즉시 멈춤" 이었습니다. 현재(2026-05-06)는 멈추는 대신 다른 경로로 가게 바뀌었습니다.

어디 무슨 일을 하나
skeleton-utils.js:1071 (skeletonBuilder 안) verifyMoveBoundary 결과 확인 → crossed경고만 찍고 그대로 진행
skeleton-utils.js:1331 (catch 블록) 에러가 나도 전체 상태를 리셋하지 않음. 해당 지붕만 false 처리
QPolygon.js:467 (drawHelpLine 진입부) 결과를 __verdict 변수에 기록만. 용마루 분기에서 crossed오버 전용 함수로 분기

4.3 좌표 저장을 풀 길이로 보존

skeletonBuilder 끝부분(skeleton-utils.js:1315)에서:

const rawMovedFull = buildRawMovedPoints(roofId, canvas)
if (rawMovedFull && rawMovedFull.length === (roof.points?.length ?? 0)) {
  canvas.skeleton.lastPoints = rawMovedFull   // 점 개수 보존
} else {
  canvas.skeleton.lastPoints = roofLineContactPoints   // 안 맞으면 기존 방식
}

4.4 처음 검증 결과 (2026-04-22)

  • 1차 이동 820 → 정상.
  • 2차 이동 1912 (경계 넘는 이동) → 옛 정책에서는 화면이 그대로 유지됐음.
  • 현재는 같은 입력에서 오버 전용 함수로 분기해서 SK 가 정상 재계산됩니다. 그리고 §6.1 의 클램프 정책이 들어와서 실서비스에서는 애초에 crossed 가 잘 안 납니다.

5. 아직 안 끝난 일 / 추적 중

5.1 사용자가 추가한 라인을 따로 보관하기 (Step 5, 미완료)

문제: SK 가 다시 계산될 때마다 roof.innerLines 를 통째로 덮어씁니다. 사용자가 손으로 추가한 용마루/추녀 같은 라인은 SK 결과에 안 들어가니까 사라질 위험이 있습니다.

제안:

  • roof.userAddedLines 라는 별도 보관함을 만들고
  • 라인 추가 UI 에서 거기에 저장한 다음
  • SK 결과 + 사용자 추가분을 합쳐서 innerLines 에 넣기
roof.innerLines = [
  ...createInnerLinesFromSkeleton(...),   // 자동 계산
  ...(roof.userAddedLines ?? []),         // 사용자 추가
]

5.2 용마루 이동(moveFlowLine) 경로 판정 (미완료)

지금 verifyMoveBoundary 는 외벽 이동만 판정합니다. 용마루를 직접 잡고 끄는 경우는 항상 unknown 이 나옵니다. 이 경로에서도 경계 판정이 필요해지면 buildRawMovedPoints 를 확장해야 합니다.

5.3 빨강 처마 보조선 잔존 (추적 중, 휴면)

오버 이동을 클램프하면 빨강 색의 처마 보조선(eaveHelpLine) 이 화면에 남는 별건 이슈가 있습니다. 2026-04-27 이후 재개 예정 (메모리: project_over_move_red_helper_deferred.md).

5.4 v2.0 설계 (휴면)

v2.0 으로 가는 새 설계 방향이 있었지만 2026-05-06 자로 휴면입니다. 사용자 명령 전까지는 v2 관련 메모리(project_v2_*) 를 참조하지 마세요.


6. 꼭 지켜야 할 9가지 원칙

이 프로젝트에서 이 원칙들을 어기면 거의 반드시 새로운 버그가 생깁니다.

# 원칙 한 줄 설명
1 잘 되는 코드는 안 건드리기 버그 수정은 최소 지점만. 기존 함수 수정 금지. 필요하면 새 함수를 만드세요.
2 고치기 전에 흐름부터 확인하기 한 파일만 보고 추측해서 고치지 마세요. 호출 체인 전체를 따라가서 어디를 고칠지 정한 다음 작업하세요.
3 수정한 곳에는 태그를 남기기 수정한 줄 위에 // [태그명 YYYY-MM-DD] 주석을 달고, 작업 끝나면 git commit 메시지를 사용자에게 제시하세요 (커밋은 사용자가 직접 함).
4 오버 이동은 평행 위치까지만 (clamp) 사용자가 너무 많이 끌면 평행한 위치까지만 잘라줍니다. 정책은 OVER_MOVE_POLICY 상수로 바꿀 수 있어요.
5 화면 표시값과 내부 좌표는 따로 라벨이 흔들려 보이면 표시 단계에서 평균을 내는 식으로 보정하세요. 내부 좌표를 함부로 round 하면 곳곳에서 깨집니다.
6 좌표 비교 허용오차는 0.5 UI/Big.js 계산 오차 때문에 0.1 로 비교하면 자주 매칭 실패합니다.
7 디버그 라벨은 별도 파일 만들지 말기 QPolygon.js 안에 인라인으로 두고, NEXT_PUBLIC_RUN_MODE === 'local' 일 때만 표시되도록 가드.
8 V1 한 트랙만 v2.0 은 휴면 중. 사용자가 직접 명령하기 전까지 v2 관련 코드/메모리 언급 금지.
9 moveLine + offset 이중 적용 방지 offset 변경 후 baseLines 복원 → drawHelpLine 호출 시 roof.moveFlowLine / roof.moveUpDown 을 미리 설정하면 SK 빌더가 또 한 번 이동시켜 이중 이동이 발생합니다. 순서: baseLines 복원 → drawHelpLine(moveLine 없이) → moveLine 속성 복원. (메모리: feedback_moveline_offset.md)

7. 전체 흐름 (사용자가 외벽선을 끌었을 때)

사용자가 마우스로 외벽선을 끌어다 놓음
  ↓
useMovementSetting.js  __onMouseDown 핸들러
  ↓
handleSave()  (line 514)
  ├─ roof.moveUpDown / moveSelectLine / moveDirect / movePosition 세팅
  │   = "어느 라인을 어느 방향으로 얼마나 옮기는가" 정보를 저장
  │
  ├─ ★ 안전장치: OVER_GUARD  (line 895~940)
  │   너무 많이 끌었으면 평행 직전까지 잘라줌 (clamp 정책)
  │   - 'clamp' (기본): 평행까지만 자르고 진행
  │   - 'reject'      : 아무것도 안 함
  │   - 'passthrough' : 옛 동작 (위험)
  │
  ├─ wall.baseLines 좌표를 실제로 변경
  │
  └─ roof.drawHelpLine() 호출
       ↓
       QPolygon.js  drawHelpLine  (line 467)
         ├─ ★ verifyMoveBoundary 로 판정 → __verdict 변수에 보관
         ├─ 캔버스에서 내부 그림 다 지움 + innerLines = []
         ├─ 지붕 종류별로 분기:
         │   - 용마루형(Eaves) + crossed → drawSkeletonRidgeRoofFromBaseLines (오버 전용)
         │   - 용마루형 그 외             → drawSkeletonRidgeRoof (일반)
         │   - 박공형(Gable) / 외쪽(Shed) / 패턴 → 각각 전용 함수
         ↓
         skeletonBuilder  (skeleton-utils.js)
           ├─ ★ 안전장치: verifyMoveBoundary  (line 1071)
           │   crossed 여도 멈추지 않고 경고만 찍음
           ├─ safeMovedPointsWithFallback → movingLineFromSkeleton
           ├─ wall.baseLines 직접 사용 (V1 정책)
           ├─ SkeletonBuilder.BuildFromGeoJSON  ← 진짜 SK 계산
           ├─ createInnerLinesFromSkeleton  (line 937 부근)
           │   끝에 가짜 ridge 제거(cull) 2단계 추가됨
           │   ⚠ 사용자 추가 라인 보존(Step 5) 미완료
           └─ canvas.skeleton.lastPoints 저장 (line 1315, 풀 길이 보장)

8. SK 라인 — 그동안 누적된 작업

모든 항목은 §6 의 원칙(특히 #1, #4)을 만족합니다. 기존 함수를 고치지 않고, 진입부에 안전장치를 추가하거나 임계값을 조정하거나 끝부분에서 정리하는 식으로 처리했습니다.

8.1 오버 이동 정책 — 평행 직전까지 자동 클램프

  • 장소: useMovementSetting.handleSaveOVER_GUARD 블록 (line 895~940)
  • 상수: 파일 맨 위 OVER_MOVE_POLICY = 'clamp' | 'reject' | 'passthrough' (기본 clamp)
  • 메모리: feedback_over_move_silent_skip.md

8.2 평행 이동 시 SK 가 무너지는 문제

  • 증상: 외벽선을 평행 이동했을 때 점 개수 매칭이 깨졌습니다.
  • 수정: 좌표 변경 경로 2곳을 인덱스 직접 매핑 방식으로 통일.
  • 메모리: project_parallel_collapse_fix.md

8.3 정렬 기준을 wallLines 에서 roofLines 로 변경

  • 이유: wallLines 기준으로 정렬하니까 후처리에서 자꾸 어긋났습니다.
  • 수정: 정렬 master 를 roofLines 로. 2026-05-06: ensureCounterClockwiseLines 의 그래프 키도 0.1 round 로 통일 (커밋 6f41e127).
  • 메모리: project_index_master_roofLines.md

8.4 SHOULDER_ABSORBED 임계값 통일

  • 4개 경로에서 같은 임계값(< 10) 을 쓰도록 정리.
  • 메모리: project_shoulder_absorbed_thresholds.md

8.5 가짜 ridge 제거 (Phantom ridge cull)

  • 증상: SK 계산 오차로 길이 0인 가짜 ridge 가 생겼습니다.
  • 수정: createInnerLinesFromSkeleton 끝부분에서 2단계로 청소 (zero-length → dead-end).
  • 메모리: project_phantom_ridge_cull.md

8.6 HIP 와 RIDGE 구분 임계값

  • 증상: 외벽 이동 오차로 ridge 가 hip 으로 잘못 분류됐습니다.
  • 수정: addRawLine 의 대각 판정 임계 0.1 → 5.0mm.
  • 메모리: project_hip_ridge_threshold.md

8.7 Hip pitch 기본값

  • 증상: hip 의 평면도/실측 토글이 동작 안 했습니다.
  • 수정: processEavesEdge:2915 에서 outerLine.pitch ?? roof.pitch ?? 0 폴백.
  • 메모리: project_hip_pitch_fallback.md

8.8 보조선(auxiliaryLine) 통합 단계 제외

  • 이유: 보조선이 통합에 끼면 코너 끝점이 사라져서 split 이 실패.
  • 수정: integrateExtensionLines 에서 보조선 제외.
  • 메모리: project_aux_line_integrate_exclusion.md

8.9 마루(ridge) 이동 보강

  • 2차 이동에서 ridge 클릭 안 됨useMovementSetting[type] effect 에 ridge.selectable = true fallback.
  • 반대측 wall 오선택 → 거리 필터 전에 x/y 겹침 사전필터 추가.
  • 메모리: project_ridge_2nd_move_fix.md, project_marudo_xoverlap_prefilter.md

8.10 회귀 검증 5 시나리오

  • 어떤 SK 수정이든 끝나면 5개 시나리오로 동작 확인 필수.
  • 메모리: project_skeleton_regression_checklist.md

9. 메모리 인덱스 (SK 관련, 전부 ~/.claude/projects/-Users-cha-Works-Projects-qcast3-qcast-front/memory/)

원칙(feedback_*)

  • feedback_no_touch_working_code.md — 잘 되는 코드 안 건드리기
  • feedback_full_analysis_before_action.md — 흐름 먼저 확인
  • feedback_inline_change_tag.md — 수정 태그 + 커밋 메시지
  • feedback_over_move_silent_skip.md — 오버 이동 클램프 정책
  • feedback_moveline_offset.md — moveLine + offset 이중 적용 방지 ★
  • feedback_display_vs_internal_values.md — 표시값 vs 내부값 분리
  • feedback_same_point_tolerance.md — 좌표 tolerance 0.5
  • feedback_debug_labels_local_only.md — 디버그 라벨 인라인
  • feedback_v1_v2_dual_track.md — V1 단일 트랙

SK / 이동 (project_*)

  • project_parallel_collapse_fix.md
  • project_index_master_roofLines.md
  • project_shoulder_absorbed_thresholds.md
  • project_phantom_ridge_cull.md
  • project_hip_ridge_threshold.md
  • project_hip_pitch_fallback.md
  • project_aux_line_integrate_exclusion.md
  • project_ridge_2nd_move_fix.md
  • project_marudo_xoverlap_prefilter.md
  • project_skeleton_regression_checklist.md
  • project_over_move_red_helper_deferred.md (빨강 보조선 잔존, 추적 중)

휴면 (참조 금지)

  • project_v2_design_direction.md
  • project_v2_invariant_no_reset.md

10. 인수받은 직후 추천 순서

  1. 이 문서 §1~§3 읽기 — 도메인 감 잡기.
  2. §6 의 원칙 9개 머리에 박기.
  3. 메모리 MEMORY.md 인덱스 통독.
  4. §8.10 의 회귀 체크리스트 5 시나리오 직접 실행해서 현재 동작 확인.
  5. §5 의 미완료 항목 중 사용자 우선순위에 따라 착수.

마지막 한마디

이 프로젝트는 수학적으로 예민한 SK 계산 위에 사용자가 수많은 수동 작업을 쌓아 올린 구조입니다. 그래서 새 기능보다는 회귀 방지가 항상 우선입니다.

"잘 되는 코드는 건들지 말고", "오버 이동은 평행까지만 잘라주고", "사용자가 추가한 라인은 절대 사라지지 않게" — 이 세 줄만 머리에 두고 가시면 됩니다.

끝 (2026-05-06 갱신, SK 전용으로 분리).