Compare commits
12
Commits
0f32f8e7e7
..
Main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dd68e2c98 | ||
|
|
7622ed2793 | ||
|
|
a8f14e8c76 | ||
|
|
4bb2692ad0 | ||
|
|
7d184d2e97 | ||
|
|
a510f34f71 | ||
|
|
6790991471 | ||
|
|
aca94d175c | ||
|
|
fd49c71910 | ||
|
|
7477a803e9 | ||
|
|
0c052abfa7 | ||
|
|
62b25b045b |
No files matched your search
+1
-1
@@ -7,5 +7,5 @@ matching.db
|
|||||||
csv
|
csv
|
||||||
*.png
|
*.png
|
||||||
*.xlsx
|
*.xlsx
|
||||||
*.txt
|
gitea-api.txt
|
||||||
data
|
data
|
||||||
@@ -17,3 +17,9 @@ Thumbs.db
|
|||||||
|
|
||||||
# Local scratch
|
# Local scratch
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
# SQLite runtime files / local recovery backups
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
*.db.bak-*
|
||||||
|
db-backups/
|
||||||
+21
-6
@@ -2,18 +2,33 @@ FROM python:3.12-slim
|
|||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PORT=8090
|
PORT=8091 \
|
||||||
|
CHROME_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
fonts-noto-cjk \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY app.py index.html detail-view.html detail-view-project.html ./
|
RUN npm install --omit=dev playwright-core
|
||||||
|
|
||||||
# matching.db는 볼륨으로 마운트해서 데이터 보존
|
COPY mysql_preview_server.py \
|
||||||
VOLUME ["/app/data"]
|
index.html \
|
||||||
|
detail-view.html \
|
||||||
|
detail-view-project.html \
|
||||||
|
mysql-preview.html \
|
||||||
|
people-unified.html \
|
||||||
|
project-codes.html \
|
||||||
|
./
|
||||||
|
|
||||||
EXPOSE 8090
|
EXPOSE 8091 8092
|
||||||
|
|
||||||
CMD ["sh", "-c", "ln -sf /app/data/matching.db /app/matching.db 2>/dev/null || true; python3 app.py"]
|
CMD ["python3", "mysql_preview_server.py"]
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 장헌산업 업무 데이터 조회 시스템
|
||||||
|
|
||||||
|
이 저장소는 장헌산업 내부 업무 데이터를 조회하고, ERP 정보와 매칭해서 웹 화면으로 보여주는 사내용 도구입니다.
|
||||||
|
|
||||||
|
주요 목적은 아래 2가지입니다.
|
||||||
|
|
||||||
|
- `8091` 페이지: 사람별/프로젝트별 근무 데이터 조회
|
||||||
|
- `8092` 페이지: 시공 코드, 계약정보, 공사개요, 연계코드 조회
|
||||||
|
|
||||||
|
|
||||||
|
## 구성 개요
|
||||||
|
|
||||||
|
이 프로젝트는 Python 기반의 간단한 웹 서버와 SQLite DB(`matching.db`)를 사용합니다.
|
||||||
|
|
||||||
|
- 웹 서버: `mysql_preview_server.py`
|
||||||
|
- 공용 DB: `matching.db`
|
||||||
|
- 메인 대시보드 화면: `index.html`
|
||||||
|
- 사람별 근무 화면: `people-unified.html`
|
||||||
|
- 시공 코드 조회 화면: `project-codes.html`
|
||||||
|
|
||||||
|
같은 저장소 안에서 여러 HTML 화면을 제공하지만, 데이터는 공통으로 `matching.db`를 사용합니다.
|
||||||
|
|
||||||
|
|
||||||
|
## 페이지 설명
|
||||||
|
|
||||||
|
### `8091` 메인/근무 데이터 페이지
|
||||||
|
|
||||||
|
`8091` 포트는 근무 데이터와 집계 화면용입니다.
|
||||||
|
|
||||||
|
포함 내용:
|
||||||
|
|
||||||
|
- 프로젝트별 투입시간 집계
|
||||||
|
- 사람별 근무내역 조회
|
||||||
|
- 월별/기간별 프로젝트 투입 현황
|
||||||
|
- 사업관리 작업일보 기반 추가 공수 데이터 조회
|
||||||
|
|
||||||
|
관련 화면:
|
||||||
|
|
||||||
|
- `index.html`: 메인 대시보드
|
||||||
|
- `people-unified.html`: 사람별 / 프로젝트별 근무내역
|
||||||
|
- `detail-view.html`, `detail-view-project.html`: 상세 보기
|
||||||
|
|
||||||
|
|
||||||
|
### `8092` 시공 코드 조회 페이지
|
||||||
|
|
||||||
|
`8092` 포트는 ERP 시공 코드 전용 조회 화면입니다.
|
||||||
|
|
||||||
|
포함 내용:
|
||||||
|
|
||||||
|
- 시공코드 목록
|
||||||
|
- 계약정보
|
||||||
|
- 공사개요
|
||||||
|
- 교량 매칭 정보
|
||||||
|
- 공사시행계획서 기반 교량제원
|
||||||
|
- 연계코드 표시
|
||||||
|
|
||||||
|
연계코드는 아래 형식으로 표시됩니다.
|
||||||
|
|
||||||
|
- `영업/설계 약칭(코드번호)`가 아니라
|
||||||
|
- 현재는 `약칭(코드번호) · 약칭(코드번호)` 형식
|
||||||
|
|
||||||
|
예시:
|
||||||
|
|
||||||
|
- `새만금~전주간 고속도로 건설고사(제4공구) [8차](25-교영-09)`
|
||||||
|
- `새만금~전주간 고속도로 건설고사(제4공구) [8차](25-설계-05)`
|
||||||
|
|
||||||
|
관련 화면:
|
||||||
|
|
||||||
|
- `project-codes.html`
|
||||||
|
|
||||||
|
|
||||||
|
## 데이터 소스
|
||||||
|
|
||||||
|
이 시스템은 여러 데이터 소스를 함께 사용합니다.
|
||||||
|
|
||||||
|
### 1. SQLite 캐시 DB
|
||||||
|
|
||||||
|
- 파일: `matching.db`
|
||||||
|
- 저장소에 포함되어 있음
|
||||||
|
- 클론한 사람도 같은 시점의 데이터를 바로 볼 수 있음
|
||||||
|
|
||||||
|
주요 저장 내용:
|
||||||
|
|
||||||
|
- 직원 정보
|
||||||
|
- `dailyproject` 근무 데이터
|
||||||
|
- 프로젝트 약칭
|
||||||
|
- ERP 시공 코드 캐시
|
||||||
|
- ERP 계약정보 캐시
|
||||||
|
- ERP 교량 개요 캐시
|
||||||
|
- ERP 공사시행계획서 캐시
|
||||||
|
- 영업/설계/시공 연계코드 캐시
|
||||||
|
|
||||||
|
|
||||||
|
### 2. MySQL 원본 데이터
|
||||||
|
|
||||||
|
사내 MySQL에서 근무 데이터를 읽어와 SQLite로 적재합니다.
|
||||||
|
|
||||||
|
관련 환경값:
|
||||||
|
|
||||||
|
- `MYSQL_HOST`
|
||||||
|
- `MYSQL_PORT`
|
||||||
|
- `MYSQL_USER`
|
||||||
|
- `MYSQL_PASSWORD`
|
||||||
|
- `MYSQL_DB`
|
||||||
|
|
||||||
|
|
||||||
|
### 3. ERP 데이터
|
||||||
|
|
||||||
|
ERP에서 아래 정보를 가져옵니다.
|
||||||
|
|
||||||
|
- 프로젝트 약칭
|
||||||
|
- 시공 코드 목록
|
||||||
|
- 계약정보
|
||||||
|
- 공사관리 / 공사개요
|
||||||
|
- 공사시행계획서
|
||||||
|
- 영업/설계/시공 연계코드
|
||||||
|
|
||||||
|
관련 기본 URL:
|
||||||
|
|
||||||
|
- `http://erp.jangheon.co.kr/projt_mng`
|
||||||
|
|
||||||
|
|
||||||
|
## DB 관련 중요 사항
|
||||||
|
|
||||||
|
이 저장소는 **코드만이 아니라 `matching.db`도 함께 관리**합니다.
|
||||||
|
|
||||||
|
이유:
|
||||||
|
|
||||||
|
- 클론한 사람이 바로 같은 데이터를 볼 수 있어야 함
|
||||||
|
- 내부 페이지가 DB 캐시를 전제로 동작함
|
||||||
|
- ERP/MySQL 접속 없이도 기본 조회가 가능해야 함
|
||||||
|
|
||||||
|
즉, 이 저장소에서는 `matching.db`도 실제 배포/공유 자산입니다.
|
||||||
|
|
||||||
|
다만 아래 파일은 운영 중 자동 생성될 수 있습니다.
|
||||||
|
|
||||||
|
- `matching.db-wal`
|
||||||
|
- `matching.db-shm`
|
||||||
|
- `matching.db.bak-*`
|
||||||
|
|
||||||
|
이 파일들은 보조 파일/백업 파일이며, 기본 공유 대상은 `matching.db`입니다.
|
||||||
|
|
||||||
|
|
||||||
|
## 연계코드 저장 방식
|
||||||
|
|
||||||
|
시공코드 페이지의 연계코드는 DB에도 저장됩니다.
|
||||||
|
|
||||||
|
관련 테이블:
|
||||||
|
|
||||||
|
- `project_alias`
|
||||||
|
- `erp_project_alias_cache`
|
||||||
|
- `erp_linked_code_cache`
|
||||||
|
|
||||||
|
예를 들어 시공코드 하나에 대해 아래 정보를 저장합니다.
|
||||||
|
|
||||||
|
- 시공코드
|
||||||
|
- 사업코드
|
||||||
|
- 연관 영업코드
|
||||||
|
- 연관 영업 약칭
|
||||||
|
- 연관 설계코드
|
||||||
|
- 연관 설계 약칭
|
||||||
|
|
||||||
|
그래서 나중에 아래와 같은 질문에 바로 답할 수 있습니다.
|
||||||
|
|
||||||
|
- “이 시공코드의 연관 영업코드는 뭐야?”
|
||||||
|
- “이 설계코드 약칭이 뭐야?”
|
||||||
|
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
### 로컬 실행
|
||||||
|
|
||||||
|
Python 서버 실행:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 mysql_preview_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
기본 포트:
|
||||||
|
|
||||||
|
- `8091`: 메인/근무 데이터
|
||||||
|
- `8092`: 시공 코드 조회
|
||||||
|
|
||||||
|
|
||||||
|
### Docker 실행
|
||||||
|
|
||||||
|
이미지/컨테이너는 `docker-compose.yml`과 `Dockerfile`로 실행할 수 있습니다.
|
||||||
|
|
||||||
|
관련 문서:
|
||||||
|
|
||||||
|
- `DEPLOY_DOCKER.md`
|
||||||
|
|
||||||
|
|
||||||
|
## 주요 파일 설명
|
||||||
|
|
||||||
|
- `mysql_preview_server.py`: 현재 실제 기능 대부분이 들어있는 메인 서버
|
||||||
|
- `app.py`: 이전/보조 서버 코드
|
||||||
|
- `index.html`: 8091 메인 대시보드
|
||||||
|
- `people-unified.html`: 사람별/프로젝트별 근무 조회
|
||||||
|
- `project-codes.html`: 8092 시공 코드 전용 화면
|
||||||
|
- `matching.db`: 공용 SQLite 데이터베이스
|
||||||
|
- `docker-compose.yml`: Docker 배포 설정
|
||||||
|
- `DEPLOY_DOCKER.md`: Docker 배포 방법
|
||||||
|
|
||||||
|
|
||||||
|
## 사용 흐름
|
||||||
|
|
||||||
|
### 근무 데이터 흐름
|
||||||
|
|
||||||
|
1. MySQL 원본 데이터를 읽음
|
||||||
|
2. SQLite `matching.db`에 적재
|
||||||
|
3. `8091` 페이지에서 집계/상세 조회
|
||||||
|
|
||||||
|
|
||||||
|
### 시공 코드 데이터 흐름
|
||||||
|
|
||||||
|
1. ERP에서 시공코드 목록 조회
|
||||||
|
2. ERP 계약정보 / 공사개요 / 공사시행계획서 조회
|
||||||
|
3. 영업/설계/시공 연계코드와 약칭 매칭
|
||||||
|
4. SQLite `matching.db`에 캐시 저장
|
||||||
|
5. `8092` 페이지에서 빠르게 조회
|
||||||
|
|
||||||
|
|
||||||
|
## 운영 시 주의사항
|
||||||
|
|
||||||
|
- `matching.db`는 실제 운영 데이터가 들어 있으므로 함부로 초기화하면 안 됩니다.
|
||||||
|
- ERP 재조회 버튼을 누르면 캐시가 갱신될 수 있습니다.
|
||||||
|
- 운영 중에는 DB 파일이 변경될 수 있어 `matching.db-wal`, `matching.db-shm`가 생길 수 있습니다.
|
||||||
|
- 다른 사람이 같은 결과를 보려면 저장소의 `matching.db`도 함께 최신 상태여야 합니다.
|
||||||
|
|
||||||
|
|
||||||
|
## 권장 공유 방식
|
||||||
|
|
||||||
|
다른 사람이 이 저장소를 받아서 바로 확인하려면:
|
||||||
|
|
||||||
|
1. 저장소를 클론
|
||||||
|
2. `matching.db`가 포함되어 있는지 확인
|
||||||
|
3. 서버 실행
|
||||||
|
4. `8091`, `8092` 페이지 접속
|
||||||
|
|
||||||
|
이렇게 하면 ERP를 다시 긁지 않아도 커밋 시점 기준의 데이터를 바로 볼 수 있습니다.
|
||||||
+31
-7
@@ -1,13 +1,12 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
manhour-dashboard:
|
jh-people-8091:
|
||||||
build: .
|
build: .
|
||||||
container_name: manhour-dashboard
|
container_name: jh-people-8091
|
||||||
ports:
|
ports:
|
||||||
- "8090:8090"
|
- "8091:8091"
|
||||||
environment:
|
environment:
|
||||||
PORT: "8090"
|
PORT: "8091"
|
||||||
|
STARTUP_MAINTENANCE: "0"
|
||||||
MYSQL_HOST: "${MYSQL_HOST:-172.16.42.111}"
|
MYSQL_HOST: "${MYSQL_HOST:-172.16.42.111}"
|
||||||
MYSQL_PORT: "${MYSQL_PORT:-3306}"
|
MYSQL_PORT: "${MYSQL_PORT:-3306}"
|
||||||
MYSQL_USER: "${MYSQL_USER:-root}"
|
MYSQL_USER: "${MYSQL_USER:-root}"
|
||||||
@@ -20,5 +19,30 @@ services:
|
|||||||
INTRANET_LOGIN_ID: "${INTRANET_LOGIN_ID:-G25001}"
|
INTRANET_LOGIN_ID: "${INTRANET_LOGIN_ID:-G25001}"
|
||||||
INTRANET_LOGIN_PW: "${INTRANET_LOGIN_PW:-00000}"
|
INTRANET_LOGIN_PW: "${INTRANET_LOGIN_PW:-00000}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./matching.db:/app/matching.db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
jh-project-8092:
|
||||||
|
build: .
|
||||||
|
container_name: jh-project-8092
|
||||||
|
ports:
|
||||||
|
- "8092:8092"
|
||||||
|
environment:
|
||||||
|
PORT: "8092"
|
||||||
|
STARTUP_MAINTENANCE: "0"
|
||||||
|
MYSQL_HOST: "${MYSQL_HOST:-172.16.42.111}"
|
||||||
|
MYSQL_PORT: "${MYSQL_PORT:-3306}"
|
||||||
|
MYSQL_USER: "${MYSQL_USER:-root}"
|
||||||
|
MYSQL_PASSWORD: "${MYSQL_PASSWORD:-hanmacerp!}"
|
||||||
|
MYSQL_DB: "${MYSQL_DB:-jangheon_manhour}"
|
||||||
|
ERP_BASE_URL: "${ERP_BASE_URL:-http://erp.jangheon.co.kr/projt_mng}"
|
||||||
|
ERP_LOGIN_ID: "${ERP_LOGIN_ID:-g25001}"
|
||||||
|
ERP_LOGIN_PW: "${ERP_LOGIN_PW:-00000}"
|
||||||
|
INTRANET_BASE_URL: "${INTRANET_BASE_URL:-http://erp.jangheon.co.kr/intranet/}"
|
||||||
|
INTRANET_LOGIN_ID: "${INTRANET_LOGIN_ID:-G25001}"
|
||||||
|
INTRANET_LOGIN_PW: "${INTRANET_LOGIN_PW:-00000}"
|
||||||
|
volumes:
|
||||||
|
- ./matching.db:/app/matching.db
|
||||||
|
depends_on:
|
||||||
|
- jh-people-8091
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
BIN
Binary file not shown.
+732
-74
File diff suppressed because it is too large.
Load diff
+236
-21
@@ -352,6 +352,31 @@
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
.map-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 48px;
|
||||||
|
height: 28px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e9f8f5;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.map-button:hover {
|
||||||
|
filter: brightness(0.96);
|
||||||
|
}
|
||||||
|
.map-location {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -610,6 +635,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const LINKED_CODE_BY_BUSINESS = {"26-002":{"salesCode":"26-교영-02","designCode":"26-설계-02","constCode":""},"26-004":{"salesCode":"26-교영-04","designCode":"26-설계-03","constCode":"26-시공-03"},"25-003":{"salesCode":"25-교영-02","designCode":"25-설계-01","constCode":"25-시공-01"},"25-005":{"salesCode":"25-교영-04","designCode":"25-설계-02","constCode":"25-시공-37"},"25-008":{"salesCode":"25-교영-07","designCode":"25-설계-04","constCode":"25-시공-08"},"25-009":{"salesCode":"25-교영-09","designCode":"25-설계-05","constCode":"26-시공-02"},"25-004":{"salesCode":"25-교영-03","designCode":"25-설계-06","constCode":""},"25-032":{"salesCode":"25-교영-32","designCode":"26-설계-01","constCode":""},"25-028":{"salesCode":"25-교영-28","designCode":"26-설계-04","constCode":""},"24-035":{"salesCode":"24-교영-35","designCode":"24-설계-01","constCode":""},"24-034":{"salesCode":"24-교영-34","designCode":"24-설계-03","constCode":""},"24-026":{"salesCode":"24-교영-26","designCode":"24-설계-05","constCode":""},"24-039":{"salesCode":"24-교영-39","designCode":"24-설계-09","constCode":"25-시공-13"},"24-044":{"salesCode":"24-교영-44","designCode":"24-설계-14","constCode":"25-시공-30"},"24-056":{"salesCode":"24-교영-85","designCode":"24-설계-31","constCode":"24-시공-24"},"24-061":{"salesCode":"24-교영-63","designCode":"24-설계-20","constCode":"24-시공-25"},"24-068":{"salesCode":"24-교영-69","designCode":"24-설계-21","constCode":"24-시공-27"},"24-072":{"salesCode":"24-교영-75","designCode":"24-설계-23","constCode":""},"24-074":{"salesCode":"24-교영-77","designCode":"24-설계-24","constCode":""},"24-078":{"salesCode":"24-교영-81","designCode":"24-설계-25","constCode":"26-시공-01"},"24-079":{"salesCode":"24-교영-82","designCode":"24-설계-26","constCode":"24-시공-33"},"24-080":{"salesCode":"24-교영-83","designCode":"24-설계-27","constCode":""},"24-081":{"salesCode":"24-교영-86","designCode":"24-설계-30","constCode":""},"24-064":{"salesCode":"24-교영-65","designCode":"24-설계-32","constCode":""},"24-066":{"salesCode":"24-교영-67","designCode":"24-설계-33","constCode":""},"24-067":{"salesCode":"24-교영-68","designCode":"24-설계-34","constCode":""},"24-017":{"salesCode":"24-교영-17","designCode":"24-설계-35","constCode":""},"24-020":{"salesCode":"24-교영-20","designCode":"24-설계-36","constCode":""},"24-084":{"salesCode":"24-교영-89","designCode":"24-설계-37","constCode":""},"24-063":{"salesCode":"24-교영-64","designCode":"25-설계-03","constCode":""},"24-073":{"salesCode":"24-교영-76","designCode":"25-설계-07","constCode":"25-시공-15"},"24-024":{"salesCode":"24-교영-24","designCode":"25-설계-09","constCode":""},"24-019":{"salesCode":"24-교영-19","designCode":"25-설계-11","constCode":"25-시공-24"},"23-042":{"salesCode":"23-교영-38","designCode":"24-설계-28","constCode":""},"23-004":{"salesCode":"23-교영-04","designCode":"23-설계-07","constCode":""},"23-008":{"salesCode":"23-교영-08","designCode":"23-설계-08","constCode":"25-시공-32"},"23-003":{"salesCode":"23-교영-03","designCode":"23-설계-10","constCode":""},"23-019":{"salesCode":"23-교영-19","designCode":"23-설계-12","constCode":""},"23-020":{"salesCode":"23-교영-20","designCode":"23-설계-13","constCode":""},"23-035":{"salesCode":"23-교영-35","designCode":"23-설계-14","constCode":"24-시공-21"},"23-030":{"salesCode":"23-교영-30","designCode":"24-설계-02","constCode":""},"23-037":{"salesCode":"23-교영-37","designCode":"24-설계-18","constCode":""},"24-048":{"salesCode":"23-교영-37","designCode":"24-설계-18","constCode":""},"23-041":{"salesCode":"23-교영-39","designCode":"24-설계-29","constCode":""},"22-046":{"salesCode":"22-교영-46","designCode":"22-설계-01","constCode":""},"22-016":{"salesCode":"22-교영-16","designCode":"22-설계-02","constCode":"23-시공-27"},"22-045":{"salesCode":"22-교영-45","designCode":"22-설계-03","constCode":""},"22-017":{"salesCode":"22-교영-17","designCode":"22-설계-04","constCode":""},"22-014":{"salesCode":"22-교영-14","designCode":"22-설계-05","constCode":""},"22-044":{"salesCode":"22-교영-44","designCode":"22-설계-06","constCode":""},"22-043":{"salesCode":"22-교영-43","designCode":"22-설계-07","constCode":""},"22-011":{"salesCode":"22-교영-11","designCode":"22-설계-08","constCode":""},"22-041":{"salesCode":"22-교영-41","designCode":"22-설계-10","constCode":""},"22-033":{"salesCode":"22-교영-33","designCode":"23-설계-01","constCode":"25-시공-33"},"22-027":{"salesCode":"22-교영-27","designCode":"23-설계-02","constCode":""},"22-012":{"salesCode":"22-교영-12","designCode":"23-설계-03","constCode":""},"22-039":{"salesCode":"22-교영-39","designCode":"23-설계-04","constCode":"25-시공-31"},"22-038":{"salesCode":"22-교영-38","designCode":"23-설계-11","constCode":""},"22-036":{"salesCode":"22-교영-36"Line truncated
|
||||||
const state = {
|
const state = {
|
||||||
rows: [],
|
rows: [],
|
||||||
filteredRows: [],
|
filteredRows: [],
|
||||||
@@ -620,6 +646,7 @@
|
|||||||
sourceTable: '',
|
sourceTable: '',
|
||||||
sourceColumns: {},
|
sourceColumns: {},
|
||||||
};
|
};
|
||||||
|
const FACTORY_ADDRESS = '충청남도 당진시 고대면 성산로 464';
|
||||||
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
const contractTypeFilter = document.getElementById('contractTypeFilter');
|
const contractTypeFilter = document.getElementById('contractTypeFilter');
|
||||||
@@ -669,13 +696,56 @@
|
|||||||
.replace(/(?<=\d)-(?=\d)/g, '-\n');
|
.replace(/(?<=\d)-(?=\d)/g, '-\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractBusinessCodeKey(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
const match = text.match(/\d{2}-\d{3}/);
|
||||||
|
return match ? match[0] : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLinkedCodes(detail) {
|
||||||
|
const apiLinked = detail.linkedCodes || {};
|
||||||
|
if (apiLinked.salesCode || apiLinked.designCode) {
|
||||||
|
return apiLinked;
|
||||||
|
}
|
||||||
|
const key = extractBusinessCodeKey(detail.businessCode);
|
||||||
|
return LINKED_CODE_BY_BUSINESS[key] || { salesCode: '', designCode: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactLinkedCodeName(name, fallbackName) {
|
||||||
|
const cleanName = String(name || '').trim();
|
||||||
|
const cleanFallback = String(fallbackName || '').trim();
|
||||||
|
if (!cleanName) return cleanFallback;
|
||||||
|
if (cleanFallback && (cleanName.length > 24 || cleanName.includes('[') || cleanName.includes('민간투자사업'))) {
|
||||||
|
return cleanFallback;
|
||||||
|
}
|
||||||
|
return cleanName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLinkedCodeLabel(code, name, fallbackName = '') {
|
||||||
|
const cleanCode = String(code || '').trim();
|
||||||
|
const cleanName = compactLinkedCodeName(name, fallbackName);
|
||||||
|
if (!cleanCode) return '-';
|
||||||
|
if (!cleanName) return cleanCode;
|
||||||
|
return `${cleanName}(${cleanCode})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCrossbeamRows(crossbeam) {
|
||||||
|
if (Array.isArray(crossbeam)) {
|
||||||
|
return crossbeam.length ? crossbeam : [{ height: '', length: '', quantity: '', remarks: '' }];
|
||||||
|
}
|
||||||
|
if (crossbeam && typeof crossbeam === 'object') {
|
||||||
|
return [crossbeam];
|
||||||
|
}
|
||||||
|
return [{ height: '', length: '', quantity: '', remarks: '' }];
|
||||||
|
}
|
||||||
|
|
||||||
function renderBudgetPlanInlineTables(plan) {
|
function renderBudgetPlanInlineTables(plan) {
|
||||||
if (!plan) return '-';
|
if (!plan) return '-';
|
||||||
const inputDays = plan.inputDays || {};
|
const inputDays = plan.inputDays || {};
|
||||||
const inputRebar = plan.inputRebar || {};
|
const inputRebar = plan.inputRebar || {};
|
||||||
const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : [];
|
const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : [];
|
||||||
const predeck = plan.predeck || {};
|
const predeck = plan.predeck || {};
|
||||||
const crossbeam = plan.crossbeam || {};
|
const crossbeamRows = normalizeCrossbeamRows(plan.crossbeam);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="inline-plan-wrap">
|
<div class="inline-plan-wrap">
|
||||||
@@ -739,27 +809,33 @@
|
|||||||
</table>
|
</table>
|
||||||
<table class="plan-table plan-table-split">
|
<table class="plan-table plan-table-split">
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="2">프리덱</th>
|
<th colspan="4">프리덱</th>
|
||||||
|
<th colspan="4">가로보</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<th>일반부(㎡)</th>
|
<th>일반부(㎡)</th>
|
||||||
<th>중분대(㎡)</th>
|
<th>중분대(㎡)</th>
|
||||||
<th>방호벽부(㎡)</th>
|
<th>방호벽부(㎡)</th>
|
||||||
<th>비고</th>
|
<th>비고</th>
|
||||||
<th rowspan="2">가로보</th>
|
|
||||||
<th>형고(m)</th>
|
<th>형고(m)</th>
|
||||||
<th>길이(m)</th>
|
<th>길이(m)</th>
|
||||||
<th>수량(EA)</th>
|
<th>수량(EA)</th>
|
||||||
<th>비고</th>
|
<th>비고</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
${crossbeamRows.map((row, index) => `
|
||||||
<td>${escapeHtml(predeck.generalArea || '-')}</td>
|
<tr>
|
||||||
<td>${escapeHtml(predeck.medianArea || '-')}</td>
|
${index === 0 ? `
|
||||||
<td>${escapeHtml(predeck.barrierArea || '-')}</td>
|
<td rowspan="${crossbeamRows.length}">${escapeHtml(predeck.generalArea || '-')}</td>
|
||||||
<td class="plan-left">${escapeHtml(predeck.remarks || '-')}</td>
|
<td rowspan="${crossbeamRows.length}">${escapeHtml(predeck.medianArea || '-')}</td>
|
||||||
<td>${escapeHtml(crossbeam.height || '-')}</td>
|
<td rowspan="${crossbeamRows.length}">${escapeHtml(predeck.barrierArea || '-')}</td>
|
||||||
<td>${escapeHtml(crossbeam.length || '-')}</td>
|
<td rowspan="${crossbeamRows.length}" class="plan-left">${escapeHtml(predeck.remarks || '-')}</td>
|
||||||
<td>${escapeHtml(crossbeam.quantity || '-')}</td>
|
` : ''}
|
||||||
<td class="plan-left">${escapeHtml(crossbeam.remarks || '-')}</td>
|
<td>${escapeHtml(row.height || '-')}</td>
|
||||||
</tr>
|
<td>${escapeHtml(row.length || '-')}</td>
|
||||||
|
<td>${escapeHtml(row.quantity || '-')}</td>
|
||||||
|
<td class="plan-left">${escapeHtml(row.remarks || '-')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -772,6 +848,13 @@
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareProjectCodeDesc(a, b) {
|
||||||
|
return String(b.projectCode || '').localeCompare(String(a.projectCode || ''), 'ko', {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: 'base',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderRows(rows) {
|
function renderRows(rows) {
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
resultBody.innerHTML = '<tr><td colspan="2" class="empty">조건에 맞는 프로젝트가 없습니다.</td></tr>';
|
resultBody.innerHTML = '<tr><td colspan="2" class="empty">조건에 맞는 프로젝트가 없습니다.</td></tr>';
|
||||||
@@ -840,11 +923,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDetailRows(detail) {
|
function renderDetailRows(detail) {
|
||||||
|
const linkedCodes = resolveLinkedCodes(detail);
|
||||||
|
const linkedCodeText = [
|
||||||
|
formatLinkedCodeLabel(linkedCodes.salesCode, linkedCodes.salesName, detail.projectName),
|
||||||
|
formatLinkedCodeLabel(linkedCodes.designCode, linkedCodes.designName, detail.projectName),
|
||||||
|
].join(' · ');
|
||||||
const rows = [
|
const rows = [
|
||||||
['사업코드', detail.businessCode],
|
['사업코드', detail.businessCode],
|
||||||
['약칭', detail.projectName],
|
['약칭', detail.projectName],
|
||||||
['시공코드', detail.projectCode],
|
['시공코드', detail.projectCode],
|
||||||
['현장위치', detail.siteLocation],
|
['연계코드', linkedCodeText],
|
||||||
|
['현장위치', renderSiteLocationCell(detail.siteLocation, detail.projectName)],
|
||||||
['발주처', detail.clientName],
|
['발주처', detail.clientName],
|
||||||
['최종계약금액', detail.finalContractAmountText],
|
['최종계약금액', detail.finalContractAmountText],
|
||||||
['계약종류', detail.contractType],
|
['계약종류', detail.contractType],
|
||||||
@@ -859,9 +948,32 @@
|
|||||||
detailBody.innerHTML = rows.map(([label, value]) => `
|
detailBody.innerHTML = rows.map(([label, value]) => `
|
||||||
<tr>
|
<tr>
|
||||||
<th>${escapeHtml(label)}</th>
|
<th>${escapeHtml(label)}</th>
|
||||||
<td>${typeof value === 'string' && value.includes('<table') ? value : escapeHtml(value || '-')}</td>
|
<td>${typeof value === 'string' && (value.includes('<table') || value.includes('class="map-location"')) ? value : escapeHtml(value || '-')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
Array.from(detailBody.querySelectorAll('.map-button')).forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
openBridgeMap(button.getAttribute('data-bridge') || '', button.getAttribute('data-location') || '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSiteLocationCell(siteLocation, projectName) {
|
||||||
|
const location = normalizeValue(siteLocation);
|
||||||
|
const disabled = !location || location === '-';
|
||||||
|
return `
|
||||||
|
<div class="map-location">
|
||||||
|
<div>${toHtmlWithBreaks(location)}</div>
|
||||||
|
<button
|
||||||
|
class="map-button"
|
||||||
|
type="button"
|
||||||
|
data-bridge="${escapeAttr(projectName || '')}"
|
||||||
|
data-location="${escapeAttr(location)}"
|
||||||
|
${disabled ? 'disabled' : ''}
|
||||||
|
title="공장 위치와 현장 위치를 지도에서 보기"
|
||||||
|
>지도</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBridgeName(value) {
|
function normalizeBridgeName(value) {
|
||||||
@@ -902,6 +1014,86 @@
|
|||||||
return `${text.slice(0, splitIndex).trim()}\n${text.slice(splitIndex + 1).trim()}`;
|
return `${text.slice(0, splitIndex).trim()}\n${text.slice(splitIndex + 1).trim()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMapQuery(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.replace(/\n+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAddressForMap(value) {
|
||||||
|
const original = normalizeMapQuery(value);
|
||||||
|
const pointMatch = original.match(/\(시점\)\s*([^()]+?)(?=\s*\(종점\)|$)/);
|
||||||
|
if (pointMatch && pointMatch[1]) {
|
||||||
|
return normalizeMapQuery(pointMatch[1]);
|
||||||
|
}
|
||||||
|
const pointLabelMatch = original.match(/시점\s*[::]?\s*([^()]+?)(?=\s*종점\s*[::]?|$)/);
|
||||||
|
if (pointLabelMatch && pointLabelMatch[1]) {
|
||||||
|
return normalizeMapQuery(pointLabelMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = original
|
||||||
|
.replace(/^\(주\)\s*장헌\s*/, '')
|
||||||
|
.replace(/^\(주\)장헌\s*/, '')
|
||||||
|
.replace(/^\[.*?\]\s*/, '')
|
||||||
|
.replace(/\(.*?\)/g, ' ')
|
||||||
|
.replace(/\[.*?\]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
const addressStart = text.search(/(서울특별시|서울시|부산광역시|부산시|대구광역시|대구시|인천광역시|인천시|광주광역시|광주시|대전광역시|대전시|울산광역시|울산시|세종특별자치시|세종시|경기도|강원특별자치도|강원도|충청북도|충북|충청남도|충남|전북특별자치도|전라북도|전북|전라남도|전남|경상북도|경북|경상남도|경남|제주특별자치도|제주도)/);
|
||||||
|
if (addressStart > 0) {
|
||||||
|
text = text.slice(addressStart).trim();
|
||||||
|
}
|
||||||
|
text = text
|
||||||
|
.split(/\s*~\s*/)[0]
|
||||||
|
.split(/\s+종점\s+/)[0]
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openBridgeMap(bridgeName, bridgeLocation) {
|
||||||
|
const site = extractAddressForMap(bridgeLocation);
|
||||||
|
if (!site || site === '-') {
|
||||||
|
alert('교량 위치 정보가 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const popup = window.open(
|
||||||
|
'',
|
||||||
|
`jhBridgeRouteMap_${Date.now()}`,
|
||||||
|
'popup=yes,width=1280,height=820,left=80,top=60,resizable=yes,scrollbars=yes'
|
||||||
|
);
|
||||||
|
if (!popup) {
|
||||||
|
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 후 다시 눌러주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popup.document.write('<!doctype html><meta charset="utf-8"><title>길찾기 준비 중</title><body style="font-family:sans-serif;padding:24px;">네이버 자동차 길찾기를 준비하는 중입니다...</body>');
|
||||||
|
popup.focus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
origin: FACTORY_ADDRESS,
|
||||||
|
destination: site,
|
||||||
|
});
|
||||||
|
const response = await fetch(`/api/naver-route-url?${params.toString()}`, { cache: 'no-store' });
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok || !result.ok || !result.url) {
|
||||||
|
throw new Error(result.error || '네이버 길찾기 URL을 만들지 못했습니다.');
|
||||||
|
}
|
||||||
|
popup.location.href = result.url;
|
||||||
|
} catch (error) {
|
||||||
|
const fallbackUrl = `https://map.naver.com/p/search/${encodeURIComponent(site)}`;
|
||||||
|
popup.document.body.innerHTML = `
|
||||||
|
<div style="font-family:sans-serif;padding:24px;line-height:1.6;">
|
||||||
|
<h2 style="margin:0 0 12px;color:#b91c1c;">네이버 자동차 길찾기를 바로 열지 못했습니다.</h2>
|
||||||
|
<p>현장 주소: <strong>${escapeHtml(site)}</strong></p>
|
||||||
|
<p style="white-space:pre-wrap;color:#7f1d1d;">${escapeHtml(error.message || String(error))}</p>
|
||||||
|
<p><a href="${escapeAttr(fallbackUrl)}" style="color:#0f766e;font-weight:700;">네이버지도에서 현장 위치 먼저 열기</a></p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatStatus(scaleRow, overviewRow) {
|
function formatStatus(scaleRow, overviewRow) {
|
||||||
const status = overviewRow?.constructionStatus || scaleRow?.constructionStatus || '';
|
const status = overviewRow?.constructionStatus || scaleRow?.constructionStatus || '';
|
||||||
return status || '-';
|
return status || '-';
|
||||||
@@ -920,7 +1112,7 @@
|
|||||||
const inputRebar = plan.inputRebar || {};
|
const inputRebar = plan.inputRebar || {};
|
||||||
const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : [];
|
const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : [];
|
||||||
const predeck = plan.predeck || {};
|
const predeck = plan.predeck || {};
|
||||||
const crossbeam = plan.crossbeam || {};
|
const crossbeamRows = normalizeCrossbeamRows(plan.crossbeam);
|
||||||
|
|
||||||
planModalTitle.textContent = `${bridgeName || plan.projectName || '프로젝트'} 공사시행계획서`;
|
planModalTitle.textContent = `${bridgeName || plan.projectName || '프로젝트'} 공사시행계획서`;
|
||||||
planModalBody.innerHTML = `
|
planModalBody.innerHTML = `
|
||||||
@@ -948,10 +1140,25 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="plan-section-title">교량제원 - 프리덱 / 가로보</div>
|
<div class="plan-section-title">교량제원 - 프리덱 / 가로보</div>
|
||||||
<table class="plan-table">
|
<table class="plan-table">
|
||||||
<tr><th>프리덱 일반부</th><td>${escapeHtml(predeck.generalArea || '-')}</td><th>가로보 형고</th><td>${escapeHtml(crossbeam.height || '-')}</td></tr>
|
<tr><th colspan="2">프리덱</th><th>형고(m)</th><th>길이(m)</th><th>수량(EA)</th><th>비고</th></tr>
|
||||||
<tr><th>프리덱 중분대</th><td>${escapeHtml(predeck.medianArea || '-')}</td><th>가로보 길이</th><td>${escapeHtml(crossbeam.length || '-')}</td></tr>
|
${crossbeamRows.map((row, index) => `
|
||||||
<tr><th>프리덱 방호벽부</th><td>${escapeHtml(predeck.barrierArea || '-')}</td><th>가로보 수량</th><td>${escapeHtml(crossbeam.quantity || '-')}</td></tr>
|
<tr>
|
||||||
<tr><th>프리덱 비고</th><td>${escapeHtml(predeck.remarks || '-')}</td><th>가로보 비고</th><td>${escapeHtml(crossbeam.remarks || '-')}</td></tr>
|
${index === 0 ? `<th>일반부(㎡)</th><td>${escapeHtml(predeck.generalArea || '-')}</td>` : ''}
|
||||||
|
${index === 1 ? `<th>중분대(㎡)</th><td>${escapeHtml(predeck.medianArea || '-')}</td>` : ''}
|
||||||
|
${index === 2 ? `<th>방호벽부(㎡)</th><td>${escapeHtml(predeck.barrierArea || '-')}</td>` : ''}
|
||||||
|
${index >= 3 ? `<th>비고</th><td>${escapeHtml(predeck.remarks || '-')}</td>` : ''}
|
||||||
|
<td>${escapeHtml(row.height || '-')}</td>
|
||||||
|
<td>${escapeHtml(row.length || '-')}</td>
|
||||||
|
<td>${escapeHtml(row.quantity || '-')}</td>
|
||||||
|
<td>${escapeHtml(row.remarks || '-')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
${crossbeamRows.length < 4 ? `
|
||||||
|
${crossbeamRows.length <= 0 ? '' : ''}
|
||||||
|
${crossbeamRows.length < 2 ? `<tr><th>중분대(㎡)</th><td>${escapeHtml(predeck.medianArea || '-')}</td><td colspan="4"></td></tr>` : ''}
|
||||||
|
${crossbeamRows.length < 3 ? `<tr><th>방호벽부(㎡)</th><td>${escapeHtml(predeck.barrierArea || '-')}</td><td colspan="4"></td></tr>` : ''}
|
||||||
|
${crossbeamRows.length < 4 ? `<tr><th>비고</th><td>${escapeHtml(predeck.remarks || '-')}</td><td colspan="4"></td></tr>` : ''}
|
||||||
|
` : ''}
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1002,6 +1209,7 @@
|
|||||||
crossbeamCount: scaleRow.crossbeamCount || '-',
|
crossbeamCount: scaleRow.crossbeamCount || '-',
|
||||||
panelCount: scaleRow.panelCount || '-',
|
panelCount: scaleRow.panelCount || '-',
|
||||||
rebarCount: scaleRow.rebarCount || '-',
|
rebarCount: scaleRow.rebarCount || '-',
|
||||||
|
rawBridgeLocation: overviewRow.bridgeLocation || '',
|
||||||
bridgeLocation: formatBridgeLocation(overviewRow.bridgeLocation),
|
bridgeLocation: formatBridgeLocation(overviewRow.bridgeLocation),
|
||||||
remarks: overviewRow.remarks || '-',
|
remarks: overviewRow.remarks || '-',
|
||||||
};
|
};
|
||||||
@@ -1010,6 +1218,8 @@
|
|||||||
|
|
||||||
function renderMergedBridgeRows(scaleRows, overviews, budgetPlan) {
|
function renderMergedBridgeRows(scaleRows, overviews, budgetPlan) {
|
||||||
const mergedRows = mergeBridgeRows(scaleRows, overviews, budgetPlan);
|
const mergedRows = mergeBridgeRows(scaleRows, overviews, budgetPlan);
|
||||||
|
const siteLocation = normalizeValue(state.detail?.siteLocation || state.selectedRow?.siteLocation || '');
|
||||||
|
const hasSiteLocation = siteLocation && siteLocation !== '-';
|
||||||
bridgeMeta.textContent = mergedRows.length
|
bridgeMeta.textContent = mergedRows.length
|
||||||
? `공사규모 ${scaleRows.length}건 · 공사개요 ${overviews.length}건을 교량명 기준으로 매칭했습니다.`
|
? `공사규모 ${scaleRows.length}건 · 공사개요 ${overviews.length}건을 교량명 기준으로 매칭했습니다.`
|
||||||
: '연결된 공사규모나 공사개요 데이터가 없습니다.';
|
: '연결된 공사규모나 공사개요 데이터가 없습니다.';
|
||||||
@@ -1037,7 +1247,11 @@
|
|||||||
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.crossbeamCount))}</td>
|
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.crossbeamCount))}</td>
|
||||||
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.panelCount))}</td>
|
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.panelCount))}</td>
|
||||||
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.rebarCount))}</td>
|
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.rebarCount))}</td>
|
||||||
<td class="bridge-summary cell-wide">${toHtmlWithBreaks(row.bridgeLocation)}</td>
|
<td class="bridge-summary cell-wide">
|
||||||
|
<div class="map-location">
|
||||||
|
<div>${toHtmlWithBreaks(row.bridgeLocation)}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
class="remark-button"
|
class="remark-button"
|
||||||
@@ -1102,6 +1316,7 @@
|
|||||||
if (selectedApplicationType) {
|
if (selectedApplicationType) {
|
||||||
state.filteredRows = state.filteredRows.filter((row) => splitApplicationTypes(row.applicationType).includes(selectedApplicationType));
|
state.filteredRows = state.filteredRows.filter((row) => splitApplicationTypes(row.applicationType).includes(selectedApplicationType));
|
||||||
}
|
}
|
||||||
|
state.filteredRows.sort(compareProjectCodeDesc);
|
||||||
|
|
||||||
countChip.textContent = `총 ${state.rows.length.toLocaleString()}건 / 표시 ${state.filteredRows.length.toLocaleString()}건`;
|
countChip.textContent = `총 ${state.rows.length.toLocaleString()}건 / 표시 ${state.filteredRows.length.toLocaleString()}건`;
|
||||||
statusText.textContent = keyword
|
statusText.textContent = keyword
|
||||||
|
|||||||
Reference in new issue
Block a user