feat(D01·D04): 예정노선 측점 표기 No.+거리(61+14.5 꼴) — D01 입력 옆 표기 · D04 표 · xlsx 측점 칸
- D01 시작 · 종료 칸 이름 측점 말로 · 입력 옆 즉시 `= 61+14.5` (저장값 m 그대로) - D04 표 측점 열 · xlsx 시점 · 종점 측점 칸 덧(m 칸 · shp 숫자 칸 유지) - 파이썬 station_label ↔ stationLabel 거울 시험 · 도움말 D01 · D04 글 · 그림 열여섯 장 다시 찍음 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@@ -17,7 +17,7 @@
|
|||||||
| 2 | 사업 지역 | 사업이 있는 지역 이름을 적음. 꼭 넣어야 하는 칸 |
|
| 2 | 사업 지역 | 사업이 있는 지역 이름을 적음. 꼭 넣어야 하는 칸 |
|
||||||
| 3 | 임도 종류 · 설계속도 | 임도 종류를 고르면 옆 설계속도 목록이 따라 바뀜 |
|
| 3 | 임도 종류 · 설계속도 | 임도 종류를 고르면 옆 설계속도 목록이 따라 바뀜 |
|
||||||
| 4 | 프로젝트명 | 위 칸을 채우면 이름이 저절로 만들어짐. 직접 고쳐도 됨 |
|
| 4 | 프로젝트명 | 위 칸을 채우면 이름이 저절로 만들어짐. 직접 고쳐도 됨 |
|
||||||
| 5 | 노선 누가거리 | 노선 시작 · 종료 누가거리(m). 비워 두어도 됨 |
|
| 5 | 노선 시작 · 종료 측점 | 거리(누가거리 m)로 넣으면 옆에 측점(`61+14.5`)이 보임. 비워 두어도 됨 |
|
||||||
| 6 | 단추 줄 | [?] 도움말 · [취소] 대시보드로 돌아감 · [등록] 프로젝트를 만듦 |
|
| 6 | 단추 줄 | [?] 도움말 · [취소] 대시보드로 돌아감 · [등록] 프로젝트를 만듦 |
|
||||||
|
|
||||||
## 작업 순서
|
## 작업 순서
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
2. 사업 연도 시작 · 끝과 사업 지역을 넣음. [사업 연도 · 지역](D01/02_사업연도_지역) 참고.
|
2. 사업 연도 시작 · 끝과 사업 지역을 넣음. [사업 연도 · 지역](D01/02_사업연도_지역) 참고.
|
||||||
3. 임도 종류와 설계속도를 고름. [임도 종류 · 설계속도](D01/03_임도종류_설계속도) 참고.
|
3. 임도 종류와 설계속도를 고름. [임도 종류 · 설계속도](D01/03_임도종류_설계속도) 참고.
|
||||||
4. 프로젝트명을 확인함. [프로젝트명](D01/04_프로젝트명) 참고.
|
4. 프로젝트명을 확인함. [프로젝트명](D01/04_프로젝트명) 참고.
|
||||||
5. 필요하면 노선 시작 · 종료 누가거리를 넣음. [노선 누가거리](D01/05_누가거리) 참고.
|
5. 필요하면 노선 시작 · 종료 측점을 넣음. [노선 시작 · 종료 측점](D01/05_누가거리) 참고.
|
||||||
6. [등록]을 누름. 문제가 있는 칸은 아래에 빨간 안내가 나옴. [입력 오류와 등록](D01/06_입력오류와_등록) 참고.
|
6. [등록]을 누름. 문제가 있는 칸은 아래에 빨간 안내가 나옴. [입력 오류와 등록](D01/06_입력오류와_등록) 참고.
|
||||||
7. 등록되면 바로 다음 화면인 [기준 지정](D02/00_설명서)이 열림.
|
7. 등록되면 바로 다음 화면인 [기준 지정](D02/00_설명서)이 열림.
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
# 노선 누가거리
|
# 노선 시작 · 종료 측점
|
||||||
|
|
||||||
노선 시작과 끝의 측점 값(누가거리, m)을 미리 정해 두는 칸. 비워 두어도 등록됨.
|
노선 시작과 끝의 측점을 미리 정해 두는 칸. 거리(누가거리, m)로 넣으면 칸 옆에 측점(No.+거리, 예 `61+14.5`)이 바로 보임. 비워 두어도 등록됨.
|
||||||
|
|
||||||
## 여는 길
|
## 여는 길
|
||||||
|
|
||||||
- 예정노선 등록 화면 맨 아래 「노선 시작 누가거리 (m)」 · 「노선 종료 누가거리 (m)」.
|
- 예정노선 등록 화면 맨 아래 「노선 시작 측점 (누가거리 m 입력)」 · 「노선 종료 측점 (누가거리 m 입력)」.
|
||||||
|
|
||||||
## 단계
|
## 단계
|
||||||
|
|
||||||
| 단계 | 할 일 |
|
| 단계 | 할 일 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 1 | 노선 첫 지점의 측점 값을 「노선 시작 누가거리」에 넣음. 0 이상의 수. |
|
| 1 | 노선 첫 지점의 거리(m)를 「노선 시작 측점」 칸에 넣음. 0 이상의 수. 칸 옆에 `= 측점` 이 바로 나옴. |
|
||||||
| 2 | 노선 끝에서 목표로 하는 거리를 「노선 종료 누가거리」에 넣음. 시작 값보다 커야 함. |
|
| 2 | 노선 끝에서 목표로 하는 거리(m)를 「노선 종료 측점」 칸에 넣음. 시작 값보다 커야 함. |
|
||||||
| 3 | 모르면 두 칸 모두 비워 둠. |
|
| 3 | 모르면 두 칸 모두 비워 둠. |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
그림 번호 — 1 노선 시작 누가거리 · 2 노선 종료 누가거리
|
그림 번호 — 1 노선 시작 측점 · 2 노선 종료 측점
|
||||||
|
|
||||||
## 보이는 것
|
## 보이는 것
|
||||||
|
|
||||||
- 시작 값은 [노선 분석](D03/00_설명서) 화면의 측점 번호가 어디서부터 세는지를 정함. 비우면 0 부터.
|
- 시작 값은 [노선 분석](D03/00_설명서) 화면의 측점 번호가 어디서부터 세는지를 정함. 비우면 0 부터.
|
||||||
- 종료 값은 분석을 마친 뒤 실제 이은 끝 누가와 비교해 「목표 · 차이」로 보임.
|
- 종료 값은 분석을 마친 뒤 실제 이은 끝 누가와 비교해 「목표 · 차이」로 보임.
|
||||||
- 종료 값이 시작 값 이하이면 [등록] 때 「종료 누가거리는 시작보다 커야 합니다.」 안내가 나옴.
|
- 종료 값이 시작 값 이하이면 [등록] 때 「종료 측점은 시작보다 커야 합니다.」 안내가 나옴.
|
||||||
|
|
||||||
> 참고: 누가거리 = 노선을 따라 잰 거리.
|
> 참고: 누가거리 = 노선을 따라 잰 거리(m). 측점 = 20 m 마다 번호(No.)를 매기고 남는 거리를 더한 표기(1234.5 m = `61+14.5`). 저장되는 값은 m 그대로.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
그림 번호 — 1 끝 연도가 시작 연도보다 앞 · 2 사업 지역 비어 있음 · 3 종료 누가거리가 시작 이하
|
그림 번호 — 1 끝 연도가 시작 연도보다 앞 · 2 사업 지역 비어 있음 · 3 종료 측점이 시작 이하
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
| 숫자 범위 안내 | 시작 연도가 2000 ~ 2100 밖 |
|
| 숫자 범위 안내 | 시작 연도가 2000 ~ 2100 밖 |
|
||||||
| 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 | 끝 연도가 시작 연도보다 앞 |
|
| 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 | 끝 연도가 시작 연도보다 앞 |
|
||||||
| 「필수 항목을 입력하세요.」 | 사업 지역 또는 프로젝트명이 빔 |
|
| 「필수 항목을 입력하세요.」 | 사업 지역 또는 프로젝트명이 빔 |
|
||||||
| 「종료 누가거리는 시작보다 커야 합니다.」 | 종료 값이 시작 값 이하 |
|
| 「종료 측점은 시작보다 커야 합니다.」 | 종료 값이 시작 값 이하 |
|
||||||
|
|
||||||
- 산림청이 아닌 계정이거나 회사에 연결되지 않은 계정이면 서버가 「예정노선은 산림청만 만들 수 있습니다.」로 막고, 이 화면에 머묾.
|
- 산림청이 아닌 계정이거나 회사에 연결되지 않은 계정이면 서버가 「예정노선은 산림청만 만들 수 있습니다.」로 막고, 이 화면에 머묾.
|
||||||
- 등록에 실패하면 서버가 알려 준 까닭이 알림으로 뜸.
|
- 등록에 실패하면 서버가 알려 준 까닭이 알림으로 뜸.
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
| 임도 종류 | 간선임도 · 산불진화임도 · 작업임도 중 하나 | 간선임도(목록 맨 위) | 설계속도 선택지와 이름 조합에 쓰임 |
|
| 임도 종류 | 간선임도 · 산불진화임도 · 작업임도 중 하나 | 간선임도(목록 맨 위) | 설계속도 선택지와 이름 조합에 쓰임 |
|
||||||
| 설계속도 | 노선을 설계하는 기준 속도 | 20 km/h | 아래 표 참조 |
|
| 설계속도 | 노선을 설계하는 기준 속도 | 20 km/h | 아래 표 참조 |
|
||||||
| 프로젝트명 | 대시보드에 보이는 이름 | 자동 조합 | 필수 · 글자 255 자까지 |
|
| 프로젝트명 | 대시보드에 보이는 이름 | 자동 조합 | 필수 · 글자 255 자까지 |
|
||||||
| 노선 시작 누가거리 (m) | 노선 첫 지점의 측점 값 | 비어 있음 | 0 이상 · 선택 |
|
| 노선 시작 측점 (누가거리 m 입력) | 노선 첫 지점의 거리(m) · 옆에 측점 표기 | 비어 있음 | 0 이상 · 선택 |
|
||||||
| 노선 종료 누가거리 (m) | 노선 끝 지점의 계획 목표 거리 | 비어 있음 | 시작 값보다 커야 함 · 선택 |
|
| 노선 종료 측점 (누가거리 m 입력) | 노선 끝 지점의 계획 목표 거리(m) · 옆에 측점 표기 | 비어 있음 | 시작 값보다 커야 함 · 선택 |
|
||||||
|
|
||||||
## 설계속도 선택지
|
## 설계속도 선택지
|
||||||
| 임도 종류 | 고를 수 있는 값 |
|
| 임도 종류 | 고를 수 있는 값 |
|
||||||
@@ -36,5 +36,5 @@
|
|||||||
- 시작 연도가 2000 ~ 2100 밖이면 숫자 범위 안내가 나옴.
|
- 시작 연도가 2000 ~ 2100 밖이면 숫자 범위 안내가 나옴.
|
||||||
- 끝 연도가 시작보다 앞이면 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 가 나옴.
|
- 끝 연도가 시작보다 앞이면 「끝 연도는 시작 연도와 같거나 뒤여야 합니다.」 가 나옴.
|
||||||
- 지역이나 이름이 비면 필수 입력 안내가 나옴.
|
- 지역이나 이름이 비면 필수 입력 안내가 나옴.
|
||||||
- 종료 누가거리가 시작 이하이면 「종료 누가거리는 시작보다 커야 합니다.」 가 나옴.
|
- 종료 측점이 시작 이하이면 「종료 측점은 시작보다 커야 합니다.」 가 나옴.
|
||||||
- 산림청이 아닌 계정으로 등록하면 서버가 「예정노선은 산림청만 만들 수 있습니다.」 로 막음. 회사에 연결되지 않은 계정도 막힘.
|
- 산림청이 아닌 계정으로 등록하면 서버가 「예정노선은 산림청만 만들 수 있습니다.」 로 막음. 회사에 연결되지 않은 계정도 막힘.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -14,13 +14,13 @@
|
|||||||
|
|
||||||
| 번호 | 구역 | 하는 일 |
|
| 번호 | 구역 | 하는 일 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 | 연도 표 | 연도마다 연장 · 시점 · 종점 · 누가 · 확정 시각 · 지정 업체를 한 줄로 보여 줌. 업체를 여기서 추가 · 삭제함 |
|
| 1 | 연도 표 | 연도마다 연장 · 시점 · 종점 · 측점 · 확정 시각 · 지정 업체를 한 줄로 보여 줌. 업체를 여기서 추가 · 삭제함 |
|
||||||
| 2 | 「확정할 연도」 카드 | 확정할 연도를 고름. 분석 [저장]이 된 연도만 나옴 |
|
| 2 | 「확정할 연도」 카드 | 확정할 연도를 고름. 분석 [저장]이 된 연도만 나옴 |
|
||||||
| 3 | 단추 줄 | [?] · [<] · [>] · [내보내기] · [확정] · [저장] |
|
| 3 | 단추 줄 | [?] · [<] · [>] · [내보내기] · [확정] · [저장] |
|
||||||
|
|
||||||
## 작업 순서
|
## 작업 순서
|
||||||
|
|
||||||
1. 연도 표에서 연도마다 연장과 누가를 확인함. [연도 표](D04/01_연도_표) 참고.
|
1. 연도 표에서 연도마다 연장과 측점을 확인함. [연도 표](D04/01_연도_표) 참고.
|
||||||
2. 지정 업체 칸에서 업체를 추가함. 등록 회사는 이름을 찾아 고르고, 아니면 업체명과 메일을 직접 적음. [지정 업체](D04/02_지정_업체) · [업체 직접 추가](D04/03_업체_직접_추가) 참고.
|
2. 지정 업체 칸에서 업체를 추가함. 등록 회사는 이름을 찾아 고르고, 아니면 업체명과 메일을 직접 적음. [지정 업체](D04/02_지정_업체) · [업체 직접 추가](D04/03_업체_직접_추가) 참고.
|
||||||
3. [저장]을 눌러 지정 업체를 서버에 저장함. [단추와 확정할 연도](D04/04_단추와_확정할_연도) 참고.
|
3. [저장]을 눌러 지정 업체를 서버에 저장함. [단추와 확정할 연도](D04/04_단추와_확정할_연도) 참고.
|
||||||
4. 「확정할 연도」에서 확정할 연도를 고르고 [확정]을 누름. 확인 창에서 진행을 누르면 확정과 메일 발송이 이뤄짐. [확정과 메일](D04/05_확정과_메일) 참고.
|
4. 「확정할 연도」에서 확정할 연도를 고르고 [확정]을 누름. 확인 창에서 진행을 누르면 확정과 메일 발송이 이뤄짐. [확정과 메일](D04/05_확정과_메일) 참고.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
그림 번호 — 1 연장(m) · 2 시점(위도, 경도) · 3 누가(m) · 4 확정 시각 · 5 지정 업체
|
그림 번호 — 1 연장(m) · 2 시점(위도, 경도) · 3 측점 · 4 확정 시각 · 5 지정 업체
|
||||||
|
|
||||||
## 단계
|
## 단계
|
||||||
|
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 1 | 연도 열과 연장(m)으로 연도별 노선 길이를 확인함. |
|
| 1 | 연도 열과 연장(m)으로 연도별 노선 길이를 확인함. |
|
||||||
| 2 | 시점 · 종점(위도, 경도)으로 노선 양 끝 위치를 확인함. |
|
| 2 | 시점 · 종점(위도, 경도)으로 노선 양 끝 위치를 확인함. |
|
||||||
| 3 | 누가(m) 열로 그 연도 노선이 전체 노선의 어디부터 어디까지인지 확인함. |
|
| 3 | 측점 열(`61+14.5` 꼴)로 그 연도 노선이 전체 노선의 어디부터 어디까지인지 확인함. |
|
||||||
| 4 | 확정 열이 비어 있으면 아직 확정 전. 날짜와 시각이 있으면 확정이 끝난 연도. |
|
| 4 | 확정 열이 비어 있으면 아직 확정 전. 날짜와 시각이 있으면 확정이 끝난 연도. |
|
||||||
| 5 | 지정 업체 열은 [지정 업체](D04/02_지정_업체) 참고. |
|
| 5 | 지정 업체 열은 [지정 업체](D04/02_지정_업체) 참고. |
|
||||||
|
|
||||||
## 보이는 것
|
## 보이는 것
|
||||||
|
|
||||||
- 분석을 저장하지 않은 연도는 누가 칸에 「노선 분석 [저장] 전」이 나옴. 「확정할 연도」 목록에도 나타나지 않음.
|
- 분석을 저장하지 않은 연도는 측점 칸에 「노선 분석 [저장] 전」이 나옴. 「확정할 연도」 목록에도 나타나지 않음.
|
||||||
- 확정 열 = 확정한 날짜와 시각(한국 시간). 비어 있으면 아직 확정 전.
|
- 확정 열 = 확정한 날짜와 시각(한국 시간). 비어 있으면 아직 확정 전.
|
||||||
- 시점 · 종점은 위도 · 경도 순서로 소수 6 자리까지 보임.
|
- 시점 · 종점은 위도 · 경도 순서로 소수 6 자리까지 보임.
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
| 대상지 | 「사업지역.연장km」 글자(예: 지역.2.2). 20바이트(한글 10자 안팎)를 넘으면 뒤가 잘림 | 글자 20 |
|
| 대상지 | 「사업지역.연장km」 글자(예: 지역.2.2). 20바이트(한글 10자 안팎)를 넘으면 뒤가 잘림 | 글자 20 |
|
||||||
| 길이 | 노선 연장, 단위 km(소수 셋째 자리) | 숫자 11.3 |
|
| 길이 | 노선 연장, 단위 km(소수 셋째 자리) | 숫자 11.3 |
|
||||||
| 연도 | 사업 연도. 전체 노선은 비어 있음 | 정수 4 |
|
| 연도 | 사업 연도. 전체 노선은 비어 있음 | 정수 4 |
|
||||||
| 시점누가 | 그 선이 시작하는 측점 값(m) | 숫자 11.2 |
|
| 시점누가 | 그 선이 시작하는 누가거리(m) · 기계용 숫자 칸 | 숫자 11.2 |
|
||||||
| 종점누가 | 그 선이 끝나는 측점 값(m) | 숫자 11.2 |
|
| 종점누가 | 그 선이 끝나는 누가거리(m) · 기계용 숫자 칸 | 숫자 11.2 |
|
||||||
|
|
||||||
- 대상지 · 길이 두 칸은 용화 양식과 같고, 연도 · 시점누가 · 종점누가 는 이 프로그램이 덧붙인 칸임.
|
- 대상지 · 길이 두 칸은 용화 양식과 같고, 연도 · 시점누가 · 종점누가 는 이 프로그램이 덧붙인 칸임.
|
||||||
- 누가 값은 2단계에서 이어 셈한 값 그대로임. 앞 연도 종점 누가 = 다음 연도 시점 누가.
|
- 누가 값은 2단계에서 이어 셈한 값 그대로임. 앞 연도 종점 누가 = 다음 연도 시점 누가.
|
||||||
@@ -50,7 +50,8 @@
|
|||||||
| 종점 X · 종점 Y | 끝점 UTM-K 좌표(m) |
|
| 종점 X · 종점 Y | 끝점 UTM-K 좌표(m) |
|
||||||
| 시점 경도 · 시점 위도 | 시작점 위경도(도) |
|
| 시점 경도 · 시점 위도 | 시작점 위경도(도) |
|
||||||
| 종점 경도 · 종점 위도 | 끝점 위경도(도) |
|
| 종점 경도 · 종점 위도 | 끝점 위경도(도) |
|
||||||
| 시점누가(m) · 종점누가(m) | 이어 셈한 측점 값 |
|
| 시점 측점 · 종점 측점 | 측점(No.+거리 · 예 `61+14.5` · 20 m 마다 번호) |
|
||||||
|
| 시점누가(m) · 종점누가(m) | 이어 셈한 누가거리(m) |
|
||||||
| 연장(m) | 그 연도 노선 길이 |
|
| 연장(m) | 그 연도 노선 길이 |
|
||||||
|
|
||||||
## 보는 사람에 따른 차이
|
## 보는 사람에 따른 차이
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -18,6 +18,7 @@ import {
|
|||||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||||
import { createHelpButton } from "@ui/ui_template_help";
|
import { createHelpButton } from "@ui/ui_template_help";
|
||||||
import { t } from "@ui/ui_template_locale";
|
import { t } from "@ui/ui_template_locale";
|
||||||
|
import { stationLabel } from "../common_util/common_util_station";
|
||||||
import { createGroupProject } from "../A00_Common/group_workflow";
|
import { createGroupProject } from "../A00_Common/group_workflow";
|
||||||
import { navigateTo } from "../A00_Common/router";
|
import { navigateTo } from "../A00_Common/router";
|
||||||
import { createNameComposer } from "../B02_ProjRegister/B02_ProjRegister_UI_Name";
|
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 와 같음) */
|
/** B02 임도 종류 → 설계속도 기준 등급(B05 grade_class_of 와 같음) */
|
||||||
const GRADE_CLASS: Record<string, string> = { main: "trunk", fire: "fire", work: "work" };
|
const GRADE_CLASS: Record<string, string> = { main: "trunk", fire: "fire", work: "work" };
|
||||||
|
|
||||||
|
/** 측점 간격 — D03 `PLAN_STATION_INTERVAL_M` 와 같음 */
|
||||||
|
const STATION_INTERVAL_M = 20;
|
||||||
|
|
||||||
const numberOrNull = (text: string): number | null =>
|
const numberOrNull = (text: string): number | null =>
|
||||||
text.trim() === "" ? null : Number.parseFloat(text);
|
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 name = createInputField({ label: t("DE_Reg_Name"), required: true });
|
||||||
const startM = createInputField({ label: t("DE_Reg_StartM"), type: "number", min: 0 });
|
const startM = createInputField({ label: t("DE_Reg_StartM"), type: "number", min: 0 });
|
||||||
const endM = createInputField({ label: t("DE_Reg_EndM"), 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)
|
// 설계속도 = 종류별 고를 수 있는 값 · 기본 = 가장 낮은 값(20)
|
||||||
const syncSpeeds = (): void => {
|
const syncSpeeds = (): void => {
|
||||||
|
|||||||
@@ -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
|
from __future__ import annotations
|
||||||
@@ -49,6 +52,20 @@ _SCHEMA = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
_BAD_FILENAME = re.compile(r'[\\/:*?"<>|\r\n\t]')
|
_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
|
@dataclass
|
||||||
@@ -130,7 +147,7 @@ def _write_shapefile(
|
|||||||
|
|
||||||
|
|
||||||
def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes:
|
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)
|
to_ll = Transformer.from_crs(EXPORT_EPSG, 4326, always_xy=True)
|
||||||
book = Workbook()
|
book = Workbook()
|
||||||
sheet = book.active
|
sheet = book.active
|
||||||
@@ -147,6 +164,8 @@ def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes:
|
|||||||
"시점 위도",
|
"시점 위도",
|
||||||
"종점 경도",
|
"종점 경도",
|
||||||
"종점 위도",
|
"종점 위도",
|
||||||
|
"시점 측점",
|
||||||
|
"종점 측점",
|
||||||
"시점누가(m)",
|
"시점누가(m)",
|
||||||
"종점누가(m)",
|
"종점누가(m)",
|
||||||
"연장(m)",
|
"연장(m)",
|
||||||
@@ -166,6 +185,8 @@ def _coordinate_xlsx(project_name: str, rows: list[dict]) -> bytes:
|
|||||||
round(slat, 7),
|
round(slat, 7),
|
||||||
round(elon, 7),
|
round(elon, 7),
|
||||||
round(elat, 7),
|
round(elat, 7),
|
||||||
|
station_label(row["start_station"]),
|
||||||
|
station_label(row["end_station"]),
|
||||||
round(row["start_station"], 2),
|
round(row["start_station"], 2),
|
||||||
round(row["end_station"], 2),
|
round(row["end_station"], 2),
|
||||||
round(row["length_m"], 2),
|
round(row["length_m"], 2),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* D04_PlanConfirm_UI_Page.ts
|
* D04_PlanConfirm_UI_Page.ts
|
||||||
* D04 확정 · 업체 · 내보내기(요구서 345 3-4 · PLAN 531-3) — B05~B09 틀(group_workflow).
|
* D04 확정 · 업체 · 내보내기(요구서 345 3-4 · PLAN 531-3) — B05~B09 틀(group_workflow).
|
||||||
* 본문 = 연도 표(연장 · 시점 · 종점 · 누가 · 확정 · 지정 업체) · 연도마다 업체 여럿
|
* 본문 = 연도 표(연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 연도마다 업체 여럿
|
||||||
* (등록 회사 찾기 = 가입 화면 회사 검색 · 아니면 업체명 + 메일 수기).
|
* (등록 회사 찾기 = 가입 화면 회사 검색 · 아니면 업체명 + 메일 수기).
|
||||||
* 좌측 = 확정할 연도 고르기 + [내보내기] [확정] [저장].
|
* 좌측 = 확정할 연도 고르기 + [내보내기] [확정] [저장].
|
||||||
* 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤) → 캐시 비움 ·
|
* 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤) → 캐시 비움 ·
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "@ui/ui_template_elements";
|
} from "@ui/ui_template_elements";
|
||||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||||
import { t } from "@ui/ui_template_locale";
|
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 { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||||
@@ -77,6 +78,10 @@ async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|||||||
|
|
||||||
const fmt = (value: number | undefined, digits = 1): string =>
|
const fmt = (value: number | undefined, digits = 1): string =>
|
||||||
value === undefined || value === null ? "—" : value.toFixed(digits);
|
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)}` : "—");
|
const where = (p?: EndPoint): string => (p ? `${p.lat.toFixed(6)}, ${p.lon.toFixed(6)}` : "—");
|
||||||
/** 같은 업체 — 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */
|
/** 같은 업체 — 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */
|
||||||
const sameFirm = (a: Assignee, b: Assignee): boolean =>
|
const sameFirm = (a: Assignee, b: Assignee): boolean =>
|
||||||
@@ -221,7 +226,7 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
|||||||
const rows = summary.years.map((row) => {
|
const rows = summary.years.map((row) => {
|
||||||
const firms = list.filter((item) => item.year === row.year).map(firmChip);
|
const firms = list.filter((item) => item.year === row.year).map(firmChip);
|
||||||
const chain = row.saved
|
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");
|
: t("D04_NotSaved");
|
||||||
return el("tr", {
|
return el("tr", {
|
||||||
attrs: { "data-year": String(row.year) },
|
attrs: { "data-year": String(row.year) },
|
||||||
|
|||||||
@@ -160,6 +160,13 @@ def test_zip_and_coordinate_table():
|
|||||||
sheet = load_workbook(io.BytesIO(bundle.all["좌표표_시험 프로젝트.xlsx"])).active
|
sheet = load_workbook(io.BytesIO(bundle.all["좌표표_시험 프로젝트.xlsx"])).active
|
||||||
rows = list(sheet.iter_rows(min_row=3, values_only=True))
|
rows = list(sheet.iter_rows(min_row=3, values_only=True))
|
||||||
assert [r[0] for r in rows] == [2026, 2027]
|
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
|
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]
|
assert [r[0] for r in one.iter_rows(min_row=3, values_only=True)] == [2026]
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -26,7 +26,7 @@ export const ui_locales_de = {
|
|||||||
D04_Col_Length: ["연장 (m)", "Length (m)"],
|
D04_Col_Length: ["연장 (m)", "Length (m)"],
|
||||||
D04_Col_Start: ["시점 (위도, 경도)", "Start (lat, lon)"],
|
D04_Col_Start: ["시점 (위도, 경도)", "Start (lat, lon)"],
|
||||||
D04_Col_End: ["종점 (위도, 경도)", "End (lat, lon)"],
|
D04_Col_End: ["종점 (위도, 경도)", "End (lat, lon)"],
|
||||||
D04_Col_Chainage: ["누가 (m)", "Chainage (m)"],
|
D04_Col_Chainage: ["측점", "Station"],
|
||||||
D04_Col_Confirmed: ["확정", "Confirmed"],
|
D04_Col_Confirmed: ["확정", "Confirmed"],
|
||||||
D04_Col_Assignees: ["지정 업체", "Assigned firms"],
|
D04_Col_Assignees: ["지정 업체", "Assigned firms"],
|
||||||
D04_NotSaved: ["노선 분석 [저장] 전", "Route not saved"],
|
D04_NotSaved: ["노선 분석 [저장] 전", "Route not saved"],
|
||||||
@@ -91,9 +91,9 @@ export const ui_locales_de = {
|
|||||||
DE_Reg_KindPlan: ["노선 계획", "Route plan"],
|
DE_Reg_KindPlan: ["노선 계획", "Route plan"],
|
||||||
DE_Reg_DesignSpeed: ["설계속도", "Design speed"],
|
DE_Reg_DesignSpeed: ["설계속도", "Design speed"],
|
||||||
DE_Reg_Name: ["프로젝트명", "Project name"],
|
DE_Reg_Name: ["프로젝트명", "Project name"],
|
||||||
DE_Reg_StartM: ["노선 시작 누가거리 (m)", "Route start chainage (m)"],
|
DE_Reg_StartM: ["노선 시작 측점 (누가거리 m 입력)", "Route start station (enter chainage m)"],
|
||||||
DE_Reg_EndM: ["노선 종료 누가거리 (m)", "Route end chainage (m)"],
|
DE_Reg_EndM: ["노선 종료 측점 (누가거리 m 입력)", "Route end station (enter chainage m)"],
|
||||||
DE_Reg_ChainOrder: ["종료 누가거리는 시작보다 커야 합니다.", "End chainage must exceed start."],
|
DE_Reg_ChainOrder: ["종료 측점은 시작보다 커야 합니다.", "End station must exceed start."],
|
||||||
DE_Reg_Source: ["대상 임도 프로젝트", "Source forest road project"],
|
DE_Reg_Source: ["대상 임도 프로젝트", "Source forest road project"],
|
||||||
DE_Reg_SourceEmpty: [
|
DE_Reg_SourceEmpty: [
|
||||||
"횡단 설계(B06)까지 끝난 임도 프로젝트가 없습니다.",
|
"횡단 설계(B06)까지 끝난 임도 프로젝트가 없습니다.",
|
||||||
|
|||||||