diff --git a/A05_EduDetail/guide/D01/00_설명서.md b/A05_EduDetail/guide/D01/00_설명서.md index 773eb7182..aeaea5a69 100644 --- a/A05_EduDetail/guide/D01/00_설명서.md +++ b/A05_EduDetail/guide/D01/00_설명서.md @@ -17,7 +17,7 @@ | 2 | 사업 지역 | 사업이 있는 지역 이름을 적음. 꼭 넣어야 하는 칸 | | 3 | 임도 종류 · 설계속도 | 임도 종류를 고르면 옆 설계속도 목록이 따라 바뀜 | | 4 | 프로젝트명 | 위 칸을 채우면 이름이 저절로 만들어짐. 직접 고쳐도 됨 | -| 5 | 노선 누가거리 | 노선 시작 · 종료 누가거리(m). 비워 두어도 됨 | +| 5 | 노선 시작 · 종료 측점 | 거리(누가거리 m)로 넣으면 옆에 측점(`61+14.5`)이 보임. 비워 두어도 됨 | | 6 | 단추 줄 | [?] 도움말 · [취소] 대시보드로 돌아감 · [등록] 프로젝트를 만듦 | ## 작업 순서 @@ -26,7 +26,7 @@ 2. 사업 연도 시작 · 끝과 사업 지역을 넣음. [사업 연도 · 지역](D01/02_사업연도_지역) 참고. 3. 임도 종류와 설계속도를 고름. [임도 종류 · 설계속도](D01/03_임도종류_설계속도) 참고. 4. 프로젝트명을 확인함. [프로젝트명](D01/04_프로젝트명) 참고. -5. 필요하면 노선 시작 · 종료 누가거리를 넣음. [노선 누가거리](D01/05_누가거리) 참고. +5. 필요하면 노선 시작 · 종료 측점을 넣음. [노선 시작 · 종료 측점](D01/05_누가거리) 참고. 6. [등록]을 누름. 문제가 있는 칸은 아래에 빨간 안내가 나옴. [입력 오류와 등록](D01/06_입력오류와_등록) 참고. 7. 등록되면 바로 다음 화면인 [기준 지정](D02/00_설명서)이 열림. diff --git a/A05_EduDetail/guide/D01/05_누가거리.md b/A05_EduDetail/guide/D01/05_누가거리.md index e43506999..8b0622744 100644 --- a/A05_EduDetail/guide/D01/05_누가거리.md +++ b/A05_EduDetail/guide/D01/05_누가거리.md @@ -1,27 +1,27 @@ -# 노선 누가거리 +# 노선 시작 · 종료 측점 -노선 시작과 끝의 측점 값(누가거리, m)을 미리 정해 두는 칸. 비워 두어도 등록됨. +노선 시작과 끝의 측점을 미리 정해 두는 칸. 거리(누가거리, m)로 넣으면 칸 옆에 측점(No.+거리, 예 `61+14.5`)이 바로 보임. 비워 두어도 등록됨. ## 여는 길 -- 예정노선 등록 화면 맨 아래 「노선 시작 누가거리 (m)」 · 「노선 종료 누가거리 (m)」. +- 예정노선 등록 화면 맨 아래 「노선 시작 측점 (누가거리 m 입력)」 · 「노선 종료 측점 (누가거리 m 입력)」. ## 단계 | 단계 | 할 일 | | --- | --- | -| 1 | 노선 첫 지점의 측점 값을 「노선 시작 누가거리」에 넣음. 0 이상의 수. | -| 2 | 노선 끝에서 목표로 하는 거리를 「노선 종료 누가거리」에 넣음. 시작 값보다 커야 함. | +| 1 | 노선 첫 지점의 거리(m)를 「노선 시작 측점」 칸에 넣음. 0 이상의 수. 칸 옆에 `= 측점` 이 바로 나옴. | +| 2 | 노선 끝에서 목표로 하는 거리(m)를 「노선 종료 측점」 칸에 넣음. 시작 값보다 커야 함. | | 3 | 모르면 두 칸 모두 비워 둠. | ![누가거리 입력](D01_05_chainage.webp) -그림 번호 — 1 노선 시작 누가거리 · 2 노선 종료 누가거리 +그림 번호 — 1 노선 시작 측점 · 2 노선 종료 측점 ## 보이는 것 - 시작 값은 [노선 분석](D03/00_설명서) 화면의 측점 번호가 어디서부터 세는지를 정함. 비우면 0 부터. - 종료 값은 분석을 마친 뒤 실제 이은 끝 누가와 비교해 「목표 · 차이」로 보임. -- 종료 값이 시작 값 이하이면 [등록] 때 「종료 누가거리는 시작보다 커야 합니다.」 안내가 나옴. +- 종료 값이 시작 값 이하이면 [등록] 때 「종료 측점은 시작보다 커야 합니다.」 안내가 나옴. -> 참고: 누가거리 = 노선을 따라 잰 거리. +> 참고: 누가거리 = 노선을 따라 잰 거리(m). 측점 = 20 m 마다 번호(No.)를 매기고 남는 거리를 더한 표기(1234.5 m = `61+14.5`). 저장되는 값은 m 그대로. diff --git a/A05_EduDetail/guide/D01/06_입력오류와_등록.md b/A05_EduDetail/guide/D01/06_입력오류와_등록.md index f4555b654..68395dbf4 100644 --- a/A05_EduDetail/guide/D01/06_입력오류와_등록.md +++ b/A05_EduDetail/guide/D01/06_입력오류와_등록.md @@ -17,7 +17,7 @@ ![입력 오류 안내](D01_06_errors.webp) -그림 번호 — 1 끝 연도가 시작 연도보다 앞 · 2 사업 지역 비어 있음 · 3 종료 누가거리가 시작 이하 +그림 번호 — 1 끝 연도가 시작 연도보다 앞 · 2 사업 지역 비어 있음 · 3 종료 측점이 시작 이하 ![단추 줄](D01_07_buttons.webp) @@ -30,7 +30,7 @@ | 숫자 범위 안내 | 시작 연도가 2000 ~ 2100 밖 | | 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 | 끝 연도가 시작 연도보다 앞 | | 「필수 항목을 입력하세요.」 | 사업 지역 또는 프로젝트명이 빔 | -| 「종료 누가거리는 시작보다 커야 합니다.」 | 종료 값이 시작 값 이하 | +| 「종료 측점은 시작보다 커야 합니다.」 | 종료 값이 시작 값 이하 | - 산림청이 아닌 계정이거나 회사에 연결되지 않은 계정이면 서버가 「예정노선은 산림청만 만들 수 있습니다.」로 막고, 이 화면에 머묾. - 등록에 실패하면 서버가 알려 준 까닭이 알림으로 뜸. diff --git a/A05_EduDetail/guide/D01/10_칸_뜻.md b/A05_EduDetail/guide/D01/10_칸_뜻.md index d02b3e035..bc58a4a28 100644 --- a/A05_EduDetail/guide/D01/10_칸_뜻.md +++ b/A05_EduDetail/guide/D01/10_칸_뜻.md @@ -13,8 +13,8 @@ | 임도 종류 | 간선임도 · 산불진화임도 · 작업임도 중 하나 | 간선임도(목록 맨 위) | 설계속도 선택지와 이름 조합에 쓰임 | | 설계속도 | 노선을 설계하는 기준 속도 | 20 km/h | 아래 표 참조 | | 프로젝트명 | 대시보드에 보이는 이름 | 자동 조합 | 필수 · 글자 255 자까지 | -| 노선 시작 누가거리 (m) | 노선 첫 지점의 측점 값 | 비어 있음 | 0 이상 · 선택 | -| 노선 종료 누가거리 (m) | 노선 끝 지점의 계획 목표 거리 | 비어 있음 | 시작 값보다 커야 함 · 선택 | +| 노선 시작 측점 (누가거리 m 입력) | 노선 첫 지점의 거리(m) · 옆에 측점 표기 | 비어 있음 | 0 이상 · 선택 | +| 노선 종료 측점 (누가거리 m 입력) | 노선 끝 지점의 계획 목표 거리(m) · 옆에 측점 표기 | 비어 있음 | 시작 값보다 커야 함 · 선택 | ## 설계속도 선택지 | 임도 종류 | 고를 수 있는 값 | @@ -36,5 +36,5 @@ - 시작 연도가 2000 ~ 2100 밖이면 숫자 범위 안내가 나옴. - 끝 연도가 시작보다 앞이면 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 가 나옴. - 지역이나 이름이 비면 필수 입력 안내가 나옴. -- 종료 누가거리가 시작 이하이면 「종료 누가거리는 시작보다 커야 합니다.」 가 나옴. +- 종료 측점이 시작 이하이면 「종료 측점은 시작보다 커야 합니다.」 가 나옴. - 산림청이 아닌 계정으로 등록하면 서버가 「예정노선은 산림청만 만들 수 있습니다.」 로 막음. 회사에 연결되지 않은 계정도 막힘. diff --git a/A05_EduDetail/guide/D01/D01_00_overview.webp b/A05_EduDetail/guide/D01/D01_00_overview.webp index 615329ebf..44fd854b8 100644 Binary files a/A05_EduDetail/guide/D01/D01_00_overview.webp and b/A05_EduDetail/guide/D01/D01_00_overview.webp differ diff --git a/A05_EduDetail/guide/D01/D01_02_year_region.webp b/A05_EduDetail/guide/D01/D01_02_year_region.webp index 46953ef23..47445555e 100644 Binary files a/A05_EduDetail/guide/D01/D01_02_year_region.webp and b/A05_EduDetail/guide/D01/D01_02_year_region.webp differ diff --git a/A05_EduDetail/guide/D01/D01_03_type_speed.webp b/A05_EduDetail/guide/D01/D01_03_type_speed.webp index 3a3d33e10..6dc60fd53 100644 Binary files a/A05_EduDetail/guide/D01/D01_03_type_speed.webp and b/A05_EduDetail/guide/D01/D01_03_type_speed.webp differ diff --git a/A05_EduDetail/guide/D01/D01_04_name.webp b/A05_EduDetail/guide/D01/D01_04_name.webp index fddea6e71..e963b54b6 100644 Binary files a/A05_EduDetail/guide/D01/D01_04_name.webp and b/A05_EduDetail/guide/D01/D01_04_name.webp differ diff --git a/A05_EduDetail/guide/D01/D01_05_chainage.webp b/A05_EduDetail/guide/D01/D01_05_chainage.webp index 86c1473c3..7174d2ecf 100644 Binary files a/A05_EduDetail/guide/D01/D01_05_chainage.webp and b/A05_EduDetail/guide/D01/D01_05_chainage.webp differ diff --git a/A05_EduDetail/guide/D01/D01_06_errors.webp b/A05_EduDetail/guide/D01/D01_06_errors.webp index a0e2b8659..46fabacec 100644 Binary files a/A05_EduDetail/guide/D01/D01_06_errors.webp and b/A05_EduDetail/guide/D01/D01_06_errors.webp differ diff --git a/A05_EduDetail/guide/D01/D01_07_buttons.webp b/A05_EduDetail/guide/D01/D01_07_buttons.webp index f6925946a..7bcd31fec 100644 Binary files a/A05_EduDetail/guide/D01/D01_07_buttons.webp and b/A05_EduDetail/guide/D01/D01_07_buttons.webp differ diff --git a/A05_EduDetail/guide/D04/00_설명서.md b/A05_EduDetail/guide/D04/00_설명서.md index 9b9d5fe30..f3aa27375 100644 --- a/A05_EduDetail/guide/D04/00_설명서.md +++ b/A05_EduDetail/guide/D04/00_설명서.md @@ -14,13 +14,13 @@ | 번호 | 구역 | 하는 일 | | --- | --- | --- | -| 1 | 연도 표 | 연도마다 연장 · 시점 · 종점 · 누가 · 확정 시각 · 지정 업체를 한 줄로 보여 줌. 업체를 여기서 추가 · 삭제함 | +| 1 | 연도 표 | 연도마다 연장 · 시점 · 종점 · 측점 · 확정 시각 · 지정 업체를 한 줄로 보여 줌. 업체를 여기서 추가 · 삭제함 | | 2 | 「확정할 연도」 카드 | 확정할 연도를 고름. 분석 [저장]이 된 연도만 나옴 | | 3 | 단추 줄 | [?] · [<] · [>] · [내보내기] · [확정] · [저장] | ## 작업 순서 -1. 연도 표에서 연도마다 연장과 누가를 확인함. [연도 표](D04/01_연도_표) 참고. +1. 연도 표에서 연도마다 연장과 측점을 확인함. [연도 표](D04/01_연도_표) 참고. 2. 지정 업체 칸에서 업체를 추가함. 등록 회사는 이름을 찾아 고르고, 아니면 업체명과 메일을 직접 적음. [지정 업체](D04/02_지정_업체) · [업체 직접 추가](D04/03_업체_직접_추가) 참고. 3. [저장]을 눌러 지정 업체를 서버에 저장함. [단추와 확정할 연도](D04/04_단추와_확정할_연도) 참고. 4. 「확정할 연도」에서 확정할 연도를 고르고 [확정]을 누름. 확인 창에서 진행을 누르면 확정과 메일 발송이 이뤄짐. [확정과 메일](D04/05_확정과_메일) 참고. diff --git a/A05_EduDetail/guide/D04/01_연도_표.md b/A05_EduDetail/guide/D04/01_연도_표.md index d58975f1d..73f953ab7 100644 --- a/A05_EduDetail/guide/D04/01_연도_표.md +++ b/A05_EduDetail/guide/D04/01_연도_표.md @@ -8,7 +8,7 @@ ![연도 표](D04_01_year_table.webp) -그림 번호 — 1 연장(m) · 2 시점(위도, 경도) · 3 누가(m) · 4 확정 시각 · 5 지정 업체 +그림 번호 — 1 연장(m) · 2 시점(위도, 경도) · 3 측점 · 4 확정 시각 · 5 지정 업체 ## 단계 @@ -16,13 +16,13 @@ | --- | --- | | 1 | 연도 열과 연장(m)으로 연도별 노선 길이를 확인함. | | 2 | 시점 · 종점(위도, 경도)으로 노선 양 끝 위치를 확인함. | -| 3 | 누가(m) 열로 그 연도 노선이 전체 노선의 어디부터 어디까지인지 확인함. | +| 3 | 측점 열(`61+14.5` 꼴)로 그 연도 노선이 전체 노선의 어디부터 어디까지인지 확인함. | | 4 | 확정 열이 비어 있으면 아직 확정 전. 날짜와 시각이 있으면 확정이 끝난 연도. | | 5 | 지정 업체 열은 [지정 업체](D04/02_지정_업체) 참고. | ## 보이는 것 -- 분석을 저장하지 않은 연도는 누가 칸에 「노선 분석 [저장] 전」이 나옴. 「확정할 연도」 목록에도 나타나지 않음. +- 분석을 저장하지 않은 연도는 측점 칸에 「노선 분석 [저장] 전」이 나옴. 「확정할 연도」 목록에도 나타나지 않음. - 확정 열 = 확정한 날짜와 시각(한국 시간). 비어 있으면 아직 확정 전. - 시점 · 종점은 위도 · 경도 순서로 소수 6 자리까지 보임. diff --git a/A05_EduDetail/guide/D04/10_내보내기_파일_구성.md b/A05_EduDetail/guide/D04/10_내보내기_파일_구성.md index e4f4a968d..e45d52c6f 100644 --- a/A05_EduDetail/guide/D04/10_내보내기_파일_구성.md +++ b/A05_EduDetail/guide/D04/10_내보내기_파일_구성.md @@ -31,8 +31,8 @@ | 대상지 | 「사업지역.연장km」 글자(예: 지역.2.2). 20바이트(한글 10자 안팎)를 넘으면 뒤가 잘림 | 글자 20 | | 길이 | 노선 연장, 단위 km(소수 셋째 자리) | 숫자 11.3 | | 연도 | 사업 연도. 전체 노선은 비어 있음 | 정수 4 | -| 시점누가 | 그 선이 시작하는 측점 값(m) | 숫자 11.2 | -| 종점누가 | 그 선이 끝나는 측점 값(m) | 숫자 11.2 | +| 시점누가 | 그 선이 시작하는 누가거리(m) · 기계용 숫자 칸 | 숫자 11.2 | +| 종점누가 | 그 선이 끝나는 누가거리(m) · 기계용 숫자 칸 | 숫자 11.2 | - 대상지 · 길이 두 칸은 용화 양식과 같고, 연도 · 시점누가 · 종점누가 는 이 프로그램이 덧붙인 칸임. - 누가 값은 2단계에서 이어 셈한 값 그대로임. 앞 연도 종점 누가 = 다음 연도 시점 누가. @@ -50,7 +50,8 @@ | 종점 X · 종점 Y | 끝점 UTM-K 좌표(m) | | 시점 경도 · 시점 위도 | 시작점 위경도(도) | | 종점 경도 · 종점 위도 | 끝점 위경도(도) | -| 시점누가(m) · 종점누가(m) | 이어 셈한 측점 값 | +| 시점 측점 · 종점 측점 | 측점(No.+거리 · 예 `61+14.5` · 20 m 마다 번호) | +| 시점누가(m) · 종점누가(m) | 이어 셈한 누가거리(m) | | 연장(m) | 그 연도 노선 길이 | ## 보는 사람에 따른 차이 diff --git a/A05_EduDetail/guide/D04/D04_00_overview.webp b/A05_EduDetail/guide/D04/D04_00_overview.webp index 244fdb9b7..4ff4fec6e 100644 Binary files a/A05_EduDetail/guide/D04/D04_00_overview.webp and b/A05_EduDetail/guide/D04/D04_00_overview.webp differ diff --git a/A05_EduDetail/guide/D04/D04_01_year_table.webp b/A05_EduDetail/guide/D04/D04_01_year_table.webp index daea15aea..a3778add0 100644 Binary files a/A05_EduDetail/guide/D04/D04_01_year_table.webp and b/A05_EduDetail/guide/D04/D04_01_year_table.webp differ diff --git a/A05_EduDetail/guide/D04/D04_02_assigned_firms.webp b/A05_EduDetail/guide/D04/D04_02_assigned_firms.webp index 9290679d0..149327197 100644 Binary files a/A05_EduDetail/guide/D04/D04_02_assigned_firms.webp and b/A05_EduDetail/guide/D04/D04_02_assigned_firms.webp differ diff --git a/A05_EduDetail/guide/D04/D04_03_add_manual.webp b/A05_EduDetail/guide/D04/D04_03_add_manual.webp index 822449792..592068815 100644 Binary files a/A05_EduDetail/guide/D04/D04_03_add_manual.webp and b/A05_EduDetail/guide/D04/D04_03_add_manual.webp differ diff --git a/A05_EduDetail/guide/D04/D04_04_unsaved_note.webp b/A05_EduDetail/guide/D04/D04_04_unsaved_note.webp index aef7924b8..b9fc471d1 100644 Binary files a/A05_EduDetail/guide/D04/D04_04_unsaved_note.webp and b/A05_EduDetail/guide/D04/D04_04_unsaved_note.webp differ diff --git a/A05_EduDetail/guide/D04/D04_07_finalize_ask.webp b/A05_EduDetail/guide/D04/D04_07_finalize_ask.webp index 0591a25af..981fb170c 100644 Binary files a/A05_EduDetail/guide/D04/D04_07_finalize_ask.webp and b/A05_EduDetail/guide/D04/D04_07_finalize_ask.webp differ diff --git a/D01_PlanRegister/D01_PlanRegister_UI_Page.ts b/D01_PlanRegister/D01_PlanRegister_UI_Page.ts index 53d026d3a..49c819408 100644 --- a/D01_PlanRegister/D01_PlanRegister_UI_Page.ts +++ b/D01_PlanRegister/D01_PlanRegister_UI_Page.ts @@ -18,6 +18,7 @@ import { import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { createHelpButton } from "@ui/ui_template_help"; import { t } from "@ui/ui_template_locale"; +import { stationLabel } from "../common_util/common_util_station"; import { createGroupProject } from "../A00_Common/group_workflow"; import { navigateTo } from "../A00_Common/router"; import { createNameComposer } from "../B02_ProjRegister/B02_ProjRegister_UI_Name"; @@ -26,6 +27,9 @@ import { SPEED_CHOICES } from "../B05_Profile/logic/B05_Profile_Grade_Criteria"; /** B02 임도 종류 → 설계속도 기준 등급(B05 grade_class_of 와 같음) */ const GRADE_CLASS: Record = { main: "trunk", fire: "fire", work: "work" }; +/** 측점 간격 — D03 `PLAN_STATION_INTERVAL_M` 와 같음 */ +const STATION_INTERVAL_M = 20; + const numberOrNull = (text: string): number | null => text.trim() === "" ? null : Number.parseFloat(text); @@ -64,6 +68,20 @@ export function renderD01PlanRegister(root: HTMLElement): void { const name = createInputField({ label: t("DE_Reg_Name"), required: true }); const startM = createInputField({ label: t("DE_Reg_StartM"), type: "number", min: 0 }); const endM = createInputField({ label: t("DE_Reg_EndM"), type: "number", min: 0 }); + // 누가거리 칸 옆 즉시 측점 표기(저장값 · 서버 검사는 m 그대로) + for (const field of [startM, endM]) { + const shown = document.createElement("span"); + shown.style.cssText = "font-size:12px;color:var(--color-text-muted)"; + shown.dataset.station = ""; + field.root.insertBefore(shown, field.root.lastElementChild); + field.input.addEventListener("input", () => { + const value = numberOrNull(field.input.value); + shown.textContent = + value !== null && Number.isFinite(value) && value >= 0 + ? `= ${stationLabel(value, STATION_INTERVAL_M, 1)}` + : ""; + }); + } // 설계속도 = 종류별 고를 수 있는 값 · 기본 = 가장 낮은 값(20) const syncSpeeds = (): void => { diff --git a/D04_PlanConfirm/D04_PlanConfirm_Export.py b/D04_PlanConfirm/D04_PlanConfirm_Export.py index 8bbb9cf78..633d98630 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_Export.py +++ b/D04_PlanConfirm/D04_PlanConfirm_Export.py @@ -7,6 +7,9 @@ 계산 자리 ③ 서버 단독 — 화면이 만지지 않는다. 순수 함수(값 → 바이트)만 두고 파일은 임시 폴더에서만 쓴다. + +짝: `station_label` ↔ `common_util/common_util_station.ts` `stationLabel(…, 1)` +(거울 시험 `test_545_1_station_label_mirror.py`). """ from __future__ import annotations @@ -49,6 +52,20 @@ _SCHEMA = { }, } _BAD_FILENAME = re.compile(r'[\\/:*?"<>|\r\n\t]') +STATION_INTERVAL_M = 20.0 # D03 `PLAN_STATION_INTERVAL_M` 와 같음 + + +def station_label(chainage_m: float, interval_m: float = STATION_INTERVAL_M) -> str: + """측점 표기 `61+14.5` — TS `stationLabel(chainage, interval, 1)` 짝(소수 한 자리 · 반올림 위로).""" + safe = interval_m if interval_m > 0 else 1.0 + number = math.floor((chainage_m + 1e-6) / safe) + rest = chainage_m - number * safe + if abs(rest) < 0.05: + rest = 0.0 + if rest >= safe - 0.05: + number, rest = number + 1, 0.0 + tenth = math.floor(rest * 10 + 0.5) / 10 + return f"{number}+{int(tenth) if tenth == int(tenth) else tenth}" @dataclass @@ -130,7 +147,7 @@ def _write_shapefile( def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes: - """사람이 읽는 좌표표 — 연도별 시점 · 종점(UTM-K · 위경도) · 누가 · 연장.""" + """사람이 읽는 좌표표 — 연도별 시점 · 종점(UTM-K · 위경도) · 측점 · 누가 · 연장.""" to_ll = Transformer.from_crs(EXPORT_EPSG, 4326, always_xy=True) book = Workbook() sheet = book.active @@ -147,6 +164,8 @@ def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes: "시점 위도", "종점 경도", "종점 위도", + "시점 측점", + "종점 측점", "시점누가(m)", "종점누가(m)", "연장(m)", @@ -166,6 +185,8 @@ def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes: round(slat, 7), round(elon, 7), round(elat, 7), + station_label(row["start_station"]), + station_label(row["end_station"]), round(row["start_station"], 2), round(row["end_station"], 2), round(row["length_m"], 2), diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts index c67dd5a28..6a3ee8416 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts @@ -1,7 +1,7 @@ /* ============================================================================= * D04_PlanConfirm_UI_Page.ts * D04 확정 · 업체 · 내보내기(요구서 345 3-4 · PLAN 531-3) — B05~B09 틀(group_workflow). - * 본문 = 연도 표(연장 · 시점 · 종점 · 누가 · 확정 · 지정 업체) · 연도마다 업체 여럿 + * 본문 = 연도 표(연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 연도마다 업체 여럿 * (등록 회사 찾기 = 가입 화면 회사 검색 · 아니면 업체명 + 메일 수기). * 좌측 = 확정할 연도 고르기 + [내보내기] [확정] [저장]. * 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤) → 캐시 비움 · @@ -23,6 +23,7 @@ import { } from "@ui/ui_template_elements"; import { createPanelCard } from "@ui/ui_template_collapsible"; import { t } from "@ui/ui_template_locale"; +import { stationLabel } from "../common_util/common_util_station"; import { clearState, readState, writeState } from "../A00_Common/b_page_state"; import { mountGroupStep } from "../A00_Common/group_workflow"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; @@ -77,6 +78,10 @@ async function api(path: string, init: RequestInit = {}): Promise { const fmt = (value: number | undefined, digits = 1): string => value === undefined || value === null ? "—" : value.toFixed(digits); +/** 측점 간격 — D03 `PLAN_STATION_INTERVAL_M` 와 같음 */ +const STATION_INTERVAL_M = 20; +const station = (m: number | undefined): string => + m === undefined || m === null ? "—" : stationLabel(m, STATION_INTERVAL_M, 1); const where = (p?: EndPoint): string => (p ? `${p.lat.toFixed(6)}, ${p.lon.toFixed(6)}` : "—"); /** 같은 업체 — 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */ const sameFirm = (a: Assignee, b: Assignee): boolean => @@ -221,7 +226,7 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { const rows = summary.years.map((row) => { const firms = list.filter((item) => item.year === row.year).map(firmChip); const chain = row.saved - ? `${fmt(row.start_chainage_m)} ~ ${fmt(row.end_chainage_m)}` + ? `${station(row.start_chainage_m)} ~ ${station(row.end_chainage_m)}` : t("D04_NotSaved"); return el("tr", { attrs: { "data-year": String(row.year) }, diff --git a/resources/tester/test_531_d04_export_finalize.py b/resources/tester/test_531_d04_export_finalize.py index ed191ff82..cd4142df8 100644 --- a/resources/tester/test_531_d04_export_finalize.py +++ b/resources/tester/test_531_d04_export_finalize.py @@ -160,6 +160,13 @@ def test_zip_and_coordinate_table(): sheet = load_workbook(io.BytesIO(bundle.all["좌표표_시험 프로젝트.xlsx"])).active rows = list(sheet.iter_rows(min_row=3, values_only=True)) assert [r[0] for r in rows] == [2026, 2027] + head = [c.value for c in next(sheet.iter_rows(min_row=2, max_row=2))] + assert head[9:11] == ["시점 측점", "종점 측점"] and head[11:13] == [ + "시점누가(m)", + "종점누가(m)", + ] + # 100 m → 5+0 · m 칸은 그대로 + assert rows[0][9] == "5+0" and rows[0][11] == pytest.approx(100.0) # 업체용 좌표표는 그 연도만 one = load_workbook(io.BytesIO(bundle.by_year[2026]["좌표표_시험 프로젝트.xlsx"])).active assert [r[0] for r in one.iter_rows(min_row=3, values_only=True)] == [2026] diff --git a/resources/tester/test_545_1_station_label_mirror.py b/resources/tester/test_545_1_station_label_mirror.py new file mode 100644 index 000000000..b17f30706 --- /dev/null +++ b/resources/tester/test_545_1_station_label_mirror.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""545-1 측점 표기 거울 — D04 `station_label`(py) ↔ `common_util_station.ts` `stationLabel(…, 1)`.""" + +import json +import subprocess +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from D04_PlanConfirm.D04_PlanConfirm_Export import station_label # noqa: E402 + +TS = PROJECT_ROOT / "common_util" / "common_util_station.ts" +TYPESCRIPT = PROJECT_ROOT / "config" / "node_modules" / "typescript" +VALUES = [ + 0, + 19.96, + 20, + 39.99, + 100, + 1234.5, + 1234.56, + 1234.44, + 1234.05, + 1234.95, + 61 * 20 + 14.5, + 812.345, +] + +SCRIPT = r""" +const ts = require(process.argv[1]); +const src = require("fs").readFileSync(process.argv[2], "utf8"); +const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText; +const box = { exports: {} }; +new Function("exports", "module", js)(box.exports, box); +process.stdout.write(JSON.stringify(JSON.parse(process.argv[3]).map((v) => box.exports.stationLabel(v, 20, 1)))); +""" + + +def test_파이썬과_TS_측점_표기_같음(): + proc = subprocess.run( + ["node", "-e", SCRIPT, str(TYPESCRIPT), str(TS), json.dumps(VALUES)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + assert json.loads(proc.stdout) == [station_label(v) for v in VALUES] + + +def test_표기_꼴(): + assert station_label(1234.5) == "61+14.5" + assert station_label(0) == "0+0" + assert station_label(39.99) == "2+0" diff --git a/ui_template/ui_template_locale_de.ts b/ui_template/ui_template_locale_de.ts index a26bd2b8a..7e510a954 100644 --- a/ui_template/ui_template_locale_de.ts +++ b/ui_template/ui_template_locale_de.ts @@ -26,7 +26,7 @@ export const ui_locales_de = { D04_Col_Length: ["연장 (m)", "Length (m)"], D04_Col_Start: ["시점 (위도, 경도)", "Start (lat, lon)"], D04_Col_End: ["종점 (위도, 경도)", "End (lat, lon)"], - D04_Col_Chainage: ["누가 (m)", "Chainage (m)"], + D04_Col_Chainage: ["측점", "Station"], D04_Col_Confirmed: ["확정", "Confirmed"], D04_Col_Assignees: ["지정 업체", "Assigned firms"], D04_NotSaved: ["노선 분석 [저장] 전", "Route not saved"], @@ -91,9 +91,9 @@ export const ui_locales_de = { DE_Reg_KindPlan: ["노선 계획", "Route plan"], DE_Reg_DesignSpeed: ["설계속도", "Design speed"], DE_Reg_Name: ["프로젝트명", "Project name"], - DE_Reg_StartM: ["노선 시작 누가거리 (m)", "Route start chainage (m)"], - DE_Reg_EndM: ["노선 종료 누가거리 (m)", "Route end chainage (m)"], - DE_Reg_ChainOrder: ["종료 누가거리는 시작보다 커야 합니다.", "End chainage must exceed start."], + DE_Reg_StartM: ["노선 시작 측점 (누가거리 m 입력)", "Route start station (enter chainage m)"], + DE_Reg_EndM: ["노선 종료 측점 (누가거리 m 입력)", "Route end station (enter chainage m)"], + DE_Reg_ChainOrder: ["종료 측점은 시작보다 커야 합니다.", "End station must exceed start."], DE_Reg_Source: ["대상 임도 프로젝트", "Source forest road project"], DE_Reg_SourceEmpty: [ "횡단 설계(B06)까지 끝난 임도 프로젝트가 없습니다.",