feat(B06): 구조물 상세 제원을 B06 폼에서 받게 — 수량이 품셈 갈래를 고를 수 있게 됨
데스크탑 창 보고(2026-09-08): 실무 프로젝트 구조물의 **상세 옵션이 통째로 비어 있음** —
`back_len_cm`(뒷길이) · `stone_cm`(돌규격) · `form`(옹벽 형식). 그래서 B08 수량이 품셈 갈래를
못 골라 **금액이 0 으로 남음**(13-4 돌쌓기 뒷길이 · 13-6 큰돌쌓기 직경 · 12-3 옹벽 형식).
**까닭 — 아무도 안 받고 있었음.** 레지스트리가 그 칸들을 `phase: "detail"` 로 두고
「B05 는 유무·종류·위치만, **상세 치수는 B06/B07 에서**」(2026-08-17 사용자 확정)로 갈라 놨는데,
**그 화면이 아직 안 그리고 있었음**. 폼은 한 벌인데 필터(`isB05Option`)가 늘 상세를 숨겼음.
고침 — 폼이 `includeDetail` 을 받아 **B06 에서만** 상세 칸까지 그림(B05 는 종전 그대로).
새 결정이 아니라 **2026-08-17 확정을 이제 구현한 것**임.
⚠ **기본값 없는 필수 항목은 빈 칸(「— 선택 —」)으로 연다** — 첫 항목을 슬쩍 고르면 근거 없는
값(뒷길이 25㎝ 같은)이 수량·단가로 흘러감. 설계자가 고르게 둠.
**큰돌쌓기에 「쌓기 방식」(메쌓기·찰쌓기) 옵션 신설** — 품셈이 13-6-1 메 / 13-6-2 찰로 갈라
두는데 구분 칸이 없었음(돌쌓기는 이미 타입이 갈려 있음). 기본값 없음·필수·상세.
실화면 확인(용화 5601e828 · B06) — C군 → 큰돌쌓기 고르면 **돌규격·쌓기 방식** 칸이 빈 채로 뜸.
돌규격 `60~80` · 쌓기 방식 `찰쌓기` 로 한 건 넣고 [저장] → 정본 `structures.json` 에
`{"stone_cm": "60~80", "bond": "찰쌓기", …}` 로 남는 것 확인. 시험분은 지우고 저장해
**원래 7건**으로 되돌림.
시험 5건 — B06 만 상세를 받는지 · B05 는 종전대로인지 · 기본값 없는 필수는 빈 칸인지 ·
큰돌쌓기 메/찰 구분 · **규격 축이 타입마다 다른지**(큰돌쌓기=직경, 돌쌓기=뒷길이 — 데스크탑이
섞여 있던 결함을 잡은 자리). 전체 501 passed · 17 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,67 +234,70 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */
|
||||
const pushMarks = (): void =>
|
||||
deps.onMarks?.([...structures, ...pipesToStructureMarks(pipeFacilities)], markTypes);
|
||||
const section = createStructuresSection({
|
||||
onChange: (next) => {
|
||||
structures = withLocalIds(next);
|
||||
section.setStructures(structures);
|
||||
pushMarks();
|
||||
if (deps.projectId) writePendingStructures(deps.projectId, structures);
|
||||
deps.onStructuresChanged?.();
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
if (structure) deps.focusChainage(structureAnchorM(structure));
|
||||
},
|
||||
getInterval: deps.stationInterval,
|
||||
onReveal: () => deps.reveal?.(),
|
||||
onPipeAdd: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
|
||||
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
|
||||
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
||||
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
|
||||
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
|
||||
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
|
||||
const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M;
|
||||
if (moved) {
|
||||
if (deps.movePipe) {
|
||||
deps.movePipe(fromChainageM, toChainageM);
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) hit.chainage_m = toChainageM;
|
||||
currentChainageM = toChainageM;
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
showToast(PIPE_MOVE_NOTICE, "success");
|
||||
} else {
|
||||
showToast(PIPE_MOVE_GUIDE, "error");
|
||||
const section = createStructuresSection(
|
||||
{
|
||||
onChange: (next) => {
|
||||
structures = withLocalIds(next);
|
||||
section.setStructures(structures);
|
||||
pushMarks();
|
||||
if (deps.projectId) writePendingStructures(deps.projectId, structures);
|
||||
deps.onStructuresChanged?.();
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
if (structure) deps.focusChainage(structureAnchorM(structure));
|
||||
},
|
||||
getInterval: deps.stationInterval,
|
||||
onReveal: () => deps.reveal?.(),
|
||||
onPipeAdd: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
|
||||
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
|
||||
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
||||
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
|
||||
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
|
||||
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
|
||||
const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M;
|
||||
if (moved) {
|
||||
if (deps.movePipe) {
|
||||
deps.movePipe(fromChainageM, toChainageM);
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) hit.chainage_m = toChainageM;
|
||||
currentChainageM = toChainageM;
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
showToast(PIPE_MOVE_NOTICE, "success");
|
||||
} else {
|
||||
showToast(PIPE_MOVE_GUIDE, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
const patch = attributes.options;
|
||||
if (!patch || !Object.keys(patch).length) return;
|
||||
if (!deps.queuePipeOptions) {
|
||||
showToast(PIPE_ADD_GUIDE, "error");
|
||||
return;
|
||||
}
|
||||
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
|
||||
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
|
||||
// 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다.
|
||||
deps.queuePipeOptions(fromChainageM, patch as Record<string, number | string>);
|
||||
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
|
||||
// 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
const patch = attributes.options;
|
||||
if (!patch || !Object.keys(patch).length) return;
|
||||
if (!deps.queuePipeOptions) {
|
||||
showToast(PIPE_ADD_GUIDE, "error");
|
||||
return;
|
||||
}
|
||||
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
|
||||
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
|
||||
// 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다.
|
||||
deps.queuePipeOptions(fromChainageM, patch as Record<string, number | string>);
|
||||
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
|
||||
// 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
},
|
||||
onPipeRemove: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
onPipeSelect: (chainageM) => {
|
||||
// 목록에서 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다.
|
||||
writeStructurePick(deps.projectId, chainageM);
|
||||
if (chainageM !== null) deps.focusChainage(chainageM);
|
||||
},
|
||||
},
|
||||
onPipeRemove: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
onPipeSelect: (chainageM) => {
|
||||
// 목록에서 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다.
|
||||
writeStructurePick(deps.projectId, chainageM);
|
||||
if (chainageM !== null) deps.focusChainage(chainageM);
|
||||
},
|
||||
});
|
||||
{ includeDetail: true },
|
||||
);
|
||||
|
||||
// 고른 것 없이 좌측 폼을 만지면 값이 아무 데도 가지 않는다(패널 실시간 반영이
|
||||
// 선택된 항목에만 걸린다) — 조용히 무시되던 자리라 이유를 알린다(2026-08-30 사용자:
|
||||
|
||||
Reference in New Issue
Block a user