Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-09-07 08:21:55 +09:00
10 changed files with 293 additions and 49 deletions
+42 -3
View File
@@ -26,6 +26,8 @@ export interface StructureOptionField {
default: string | number | null;
/** 미확정 항목(기본값 없음) — 사용자가 값을 넣어야 저장된다. */
required?: boolean;
/** 거짓이면 폼에 **회색으로** 그려지고 못 고른다 — 칸은 남기되 잠그는 자리. */
enabled?: boolean;
/** 입력 시점 — B05는 유무·종류·위치만 받고 상세 치수(detail)는 B06/B07에서 받는다
* (2026-08-17 사용자 확정). detail이면 required여도 B05 폼에 그리지 않는다. */
phase?: "b05" | "detail";
@@ -129,18 +131,28 @@ async function requestJson<T>(path: string, init: RequestInit = {}): Promise<T>
/** 타입 레지스트리는 서버 배포 중에 바뀌지 않으므로 탭 수명 동안 한 번만 받는다. */
let typesCache: Promise<StructureType[]> | null = null;
export function fetchStructureTypes(): Promise<StructureType[]> {
/**
* 구조물 타입 목록.
*
* `includeDisabled` 를 주면 `enabled:false` 타입(B군 종단배수·F군 생태/녹화·G군 일부)도
* 함께 준다. 레지스트리 주석(2026-08-17)이 「B05 선택지에서 빼고 **B06 개별 횡단도
* 옵션으로 재사용**」이라 적어 둔 그 자리다 — 2026-09-07 사용자 지시 「A군뿐 아니라
* 구조물 전체를 넣을 수 있어야 함」으로 B06 이 그 목록을 쓴다. B05 는 종전대로 켜진 것만.
*/
export function fetchStructureTypes(includeDisabled = false): Promise<StructureType[]> {
if (!typesCache) {
typesCache = requestJson<StructureTypesResponse>("/projects/structure-types", {
method: "GET",
})
.then((payload) => payload.types.filter((type) => type.enabled))
.then((payload) => payload.types)
.catch((error) => {
typesCache = null; // 실패한 약속을 남겨 두면 다시 시도할 수 없다.
throw error;
});
}
return typesCache;
return typesCache.then((types) =>
includeDisabled ? types : types.filter((type) => type.enabled),
);
}
export async function fetchStructures(projectId: string): Promise<StructureListResponse> {
@@ -174,6 +186,33 @@ export async function migrateLegacyStations(
/** 종단도 마크 위치 = 기준점. 구간형도 chainage_m이 기준점이다(2026-08-17 사용자
* 확정: 기준점에 마킹 + 시작·종료 측점). 기준점이 없는 기존 저장분은 시점으로 본다. */
/** 관 지점을 **알약 레인용 가상 구조물**로 만든다 — 정본은 `pipe_points.json` 이라
* 저장하지 않는다. B05 종단과 B06 종단이 같은 표기를 쓰도록 한 벌만 둔다
* (2026-09-07 사용자 지시 4 「구조물 표시 통일」). */
export function pipesToStructureMarks(
pipes: ReadonlyArray<{
chainage_m: number;
facility: string;
options?: Record<string, unknown> | null;
}>,
): StructureInstance[] {
return pipes.map((pipe) => ({
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
type_id: pipe.facility,
placement: "point",
chainage_m: pipe.chainage_m,
start_m: null,
end_m: null,
options: (pipe.options ?? {}) as Record<string, string | number>,
memo: "",
placement_source: "automatic",
status: "draft",
revision: 0,
geometry: null,
})) as StructureInstance[];
}
export function structureAnchorM(structure: StructureInstance): number {
return structure.chainage_m ?? structure.start_m ?? 0;
}
@@ -639,6 +639,13 @@
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
"input": "select",
"choices": ["자동(성토 쪽)", "좌", "우"],
"default": "자동(성토 쪽)"
}
]
},
@@ -697,6 +704,13 @@
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
"input": "select",
"choices": ["자동(성토 쪽)", "좌", "우"],
"default": "자동(성토 쪽)"
}
]
},
@@ -755,6 +769,13 @@
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
"input": "select",
"choices": ["자동(성토 쪽)", "좌", "우"],
"default": "자동(성토 쪽)"
}
]
},
@@ -813,6 +834,13 @@
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
"input": "select",
"choices": ["자동(성토 쪽)", "좌", "우"],
"default": "자동(성토 쪽)"
}
]
},
@@ -871,6 +899,13 @@
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
"input": "select",
"choices": ["자동(성토 쪽)", "좌", "우"],
"default": "자동(성토 쪽)"
}
]
},
@@ -44,6 +44,10 @@ class StructureOptionField(BaseModel):
# (2026-08-17 사용자 확정). `detail`이면 required여도 B05 저장에서 강제하지 않는다
# — 필수 원칙은 유지되고 강제 시점만 B06/B07로 미뤄진다.
phase: Literal["b05", "detail"] = "b05"
# 폼에 칸은 두되 **지금은 못 고르게** 할 때 거짓으로 둔다 — 회색으로 그려지고 값은
# 기본값이 그대로 저장된다(2026-09-07 사용자: 「폼 선택은 가능하게 반영하고 나중에
# 선택 비활성화로 하자」). 칸 자체를 없애면 나중에 켤 자리를 다시 찾아야 한다.
enabled: bool = True
class StructureType(BaseModel):
@@ -322,11 +322,18 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
input.value = String(preset ?? "");
}
const label = option.unit ? `${option.label} (${option.unit})` : option.label;
// `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다.
const locked = option.enabled === false;
if (locked) {
input.disabled = true;
input.classList.add("is-locked");
input.title = "지금은 고를 수 없는 항목입니다.";
}
optionRow.append(field(label, input));
input.addEventListener("change", () => liveCommit());
optionInputs.push({
key: option.key,
required: !!option.required,
required: !locked && !!option.required,
input,
read: () => (option.input === "number" ? Number(input.value) || 0 : input.value),
isEmpty: () => input.value.trim() === "",