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)에서 가장 까다롭고 위험한 부분인 "지붕 모양을 바꾸면 내부 선이 깨지는 문제" 를 어떻게 풀어왔는지 정리한 인수인계 문서입니다.
이 문서를 읽고 나면:
- 지붕 도면이 어떤 부품으로 구성돼 있는지 알 수 있고,
- 어떤 버그가 있었고 어떻게 풀었는지 따라갈 수 있고,
- 다음에 손대면 안 되는 코드와, 손대도 되는 코드를 구분할 수 있게 됩니다.
같이 봐야 하는 자료
- 메모리 인덱스:
~/.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. 어떤 문제가 있었나 (수정 전 증상)
- 외벽선을 우측 상단으로 강하게 끌면, 화면에 있지도 않은 가짜 수직선이 그려졌습니다.
- 외벽선을 두 번 연속 이동하면, 첫 번째 이동 결과가 사라지고 도형이 무너졌습니다.
- 경계를 넘는 이동을 하면 사용자가 그동안 추가한 보조선들이 다 사라졌습니다. — 이게 사용자가 가장 싫어하던 문제입니다.
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.handleSave의OVER_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 = truefallback. - 반대측 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.5feedback_debug_labels_local_only.md— 디버그 라벨 인라인feedback_v1_v2_dual_track.md— V1 단일 트랙
SK / 이동 (project_*)
project_parallel_collapse_fix.mdproject_index_master_roofLines.mdproject_shoulder_absorbed_thresholds.mdproject_phantom_ridge_cull.mdproject_hip_ridge_threshold.mdproject_hip_pitch_fallback.mdproject_aux_line_integrate_exclusion.mdproject_ridge_2nd_move_fix.mdproject_marudo_xoverlap_prefilter.mdproject_skeleton_regression_checklist.mdproject_over_move_red_helper_deferred.md(빨강 보조선 잔존, 추적 중)
휴면 (참조 금지)
project_v2_design_direction.mdproject_v2_invariant_no_reset.md
10. 인수받은 직후 추천 순서
- 이 문서 §1~§3 읽기 — 도메인 감 잡기.
- §6 의 원칙 9개 머리에 박기.
- 메모리
MEMORY.md인덱스 통독. - §8.10 의 회귀 체크리스트 5 시나리오 직접 실행해서 현재 동작 확인.
- §5 의 미완료 항목 중 사용자 우선순위에 따라 착수.
마지막 한마디
이 프로젝트는 수학적으로 예민한 SK 계산 위에 사용자가 수많은 수동 작업을 쌓아 올린 구조입니다. 그래서 새 기능보다는 회귀 방지가 항상 우선입니다.
"잘 되는 코드는 건들지 말고", "오버 이동은 평행까지만 잘라주고", "사용자가 추가한 라인은 절대 사라지지 않게" — 이 세 줄만 머리에 두고 가시면 됩니다.
끝 (2026-05-06 갱신, SK 전용으로 분리).