Add JH work data page and database
BIN
exports/abnormal_work_cases_report.docx
Normal file
237
exports/abnormal_work_cases_report.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# 인트라넷 / 사업관리 이상케이스 보고서
|
||||
|
||||
- 작성일: 2026-06-04 17:47:53
|
||||
- 기준 데이터: 현재 로컬 `matching.db`에 수집된 인트라넷 SQL, 사업관리 작업일보, userstate
|
||||
- 판단 단위: 같은 사람 + 같은 날짜
|
||||
- 시간 비교 기준: 인트라넷 `정규시간` vs 사업관리 `공수 * 8시간`
|
||||
- 업무 판단 기준: 같은 날짜에 인트라넷과 사업관리 기록이 함께 있으면 `사업관리 기록 우선`
|
||||
|
||||
## 정상 / 이상 기준
|
||||
|
||||
정상으로 보는 경우는 아래 2가지입니다.
|
||||
|
||||
1. 사업관리만 있고 사업관리 기록이 1개인 경우
|
||||
2. 인트라넷과 사업관리가 모두 있고, 사업관리 기록이 1개이며, 프로젝트와 시간이 모두 같은 경우
|
||||
|
||||
추가로 `연차인데 근무기록이 있는 경우`는 이상케이스로 봅니다.
|
||||
|
||||
## 이상케이스 요약
|
||||
|
||||
| 구분 | 케이스 | 건수 | 인원수 |
|
||||
|---|---:|---:|---:|
|
||||
| A | 사업관리만 있는데 사업관리 기록이 2개 이상인 경우 | 18 | 3 |
|
||||
| B | 인트라넷과 사업관리 시간이 같지만 프로젝트가 다른 경우 | 88 | 8 |
|
||||
| C | 인트라넷과 사업관리 프로젝트는 같지만 시간이 다른 경우 | 124 | 3 |
|
||||
| D | 인트라넷과 사업관리 프로젝트도 다르고 시간도 다른 경우 | 32 | 5 |
|
||||
| E | 인트라넷도 있고 사업관리 기록이 2개 이상인 경우 | 5 | 1 |
|
||||
| F | 연차인데 근무기록이 있는 경우 | 66 | 25 |
|
||||
| 합계 | | 333 | 30 |
|
||||
|
||||
## A. 사업관리만 있는데 사업관리 기록이 2개 이상인 경우
|
||||
|
||||
인트라넷에는 해당 날짜 기록이 없지만, 사업관리에는 같은 사람/같은 날짜에 현장 기록이 2개 이상 존재합니다.
|
||||
|
||||
- 건수: 18건
|
||||
- 인원수: 3명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 이호범 (J23301) | 건설본부 | 사원 | 9 |
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 5 |
|
||||
| 김성환 (J25301) | 건설본부 | 부장 | 4 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-01-31 | 김성환 (J25301) | 없음 | 25-시공-07 (신평교), 24-시공-03 (은화삼지구(지하화구간)) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 인장/가설 현장관리 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2026-01-30 | 김성환 (J25301) | 없음 | 24-시공-03 (은화삼지구(지하화구간)), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 인장/가설 관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2026-01-29 | 김성환 (J25301) | 없음 | 24-시공-03 (은화삼지구(지하화구간)), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 인장/가설 관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2026-01-28 | 김성환 (J25301) | 없음 | 24-시공-03 (은화삼지구(지하화구간)), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 인장/가설 관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-13 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 1.5공수 = 12h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-12 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-11 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-10 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-09 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
| 2025-12-08 | 이호범 (J23301) | 없음 | 25-시공-01 (신촌교), 22-시공-24 (전동교) / 1.5공수 = 12h / 사업관리 행 2개 / 작업내용: 철근조립,관리자 | 인트라넷 기록 없음, 사업관리 현장 기록 2개 이상 |
|
||||
|
||||
## B. 인트라넷과 사업관리 시간이 같지만 프로젝트가 다른 경우
|
||||
|
||||
양쪽 모두 같은 시간으로 잡혔지만 프로젝트 코드가 다릅니다. 최종 판단은 사업관리 프로젝트를 우선합니다.
|
||||
|
||||
- 건수: 88건
|
||||
- 인원수: 8명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 최영준 (J24201) | 기술영업본부 | 전무이사 | 55 |
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 14 |
|
||||
| 최태순 (J10301) | 건설본부 | 상무이사 | 9 |
|
||||
| 김인수 (J18203) | 건설본부 | 상무이사 | 3 |
|
||||
| 차성대 (J10308) | 건설본부 | 상무이사 | 2 |
|
||||
| 윤선일 (J22201) | 건설본부 | 차장 | 2 |
|
||||
| 윤두현 (J22204) | 건설본부 | 부장 | 2 |
|
||||
| 주상구 (J23101) | 건설본부 | 상무이사 | 1 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-01-14 | 김인수 (J18203) | 24-시공-16 (시화MTV) / 정규 8h / OT 0h | 22-시공-24 (전동교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2026-01-13 | 김인수 (J18203) | 24-시공-16 (시화MTV) / 정규 8h / OT 0h | 22-시공-24 (전동교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2026-01-12 | 김인수 (J18203) | 24-시공-16 (시화MTV) / 정규 8h / OT 0h | 22-시공-24 (전동교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-10-01 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-30 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-29 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 3h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-26 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-25 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-23 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 3h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
| 2025-09-19 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 시간은 같지만 프로젝트가 다름 |
|
||||
|
||||
## C. 인트라넷과 사업관리 프로젝트는 같지만 시간이 다른 경우
|
||||
|
||||
프로젝트는 동일하지만 인트라넷 정규시간과 사업관리 공수 환산 시간이 다릅니다.
|
||||
|
||||
- 건수: 124건
|
||||
- 인원수: 3명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 122 |
|
||||
| 이인갑 (J25510) | 건설본부 | 이사 | 1 |
|
||||
| 이호범 (J23301) | 건설본부 | 사원 | 1 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-01-06 | 이인갑 (J25510) | 25-시공-07 (신평교) / 정규 3h / OT 0h | 25-시공-07 (신평교) / 1공수 = 8h / 사업관리 행 1개 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-12-31 | 김인열 (M05510) | 21-시공-33 (지산교) / 정규 5h / OT 0h | 21-시공-33 (지산교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-09-09 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 7h / OT 0h | 22-시공-41 (영산강교(물품)) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-08-08 | 김인열 (M05510) | 24-시공-17 (오산교) / 정규 7h / OT 0h | 24-시공-17 (오산교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-06-19 | 김인열 (M05510) | 23-시공-33 (중리교) / 정규 7h / OT 0h | 23-시공-33 (중리교) / 1공수 = 8h / 사업관리 행 1개 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-06-12 | 김인열 (M05510) | 23-시공-33 (중리교) / 정규 2h / OT 0h | 23-시공-33 (중리교) / 1공수 = 8h / 사업관리 행 1개 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-05-22 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 3h / OT 0h | 22-시공-41 (영산강교(물품)) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-05-15 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 5h / OT 0h | 22-시공-41 (영산강교(물품)) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-05-07 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 1h / OT 0h | 22-시공-41 (영산강교(물품)) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
| 2025-04-25 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 3h / OT 0h | 22-시공-41 (영산강교(물품)) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 관리자 | 프로젝트는 같지만 시간이 다름 |
|
||||
|
||||
## D. 인트라넷과 사업관리 프로젝트도 다르고 시간도 다른 경우
|
||||
|
||||
프로젝트와 시간 모두 일치하지 않습니다.
|
||||
|
||||
- 건수: 32건
|
||||
- 인원수: 5명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 최영준 (J24201) | 기술영업본부 | 전무이사 | 19 |
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 10 |
|
||||
| 이의환 (J13308) | 건설본부 | 부장 | 1 |
|
||||
| 윤선일 (J22201) | 건설본부 | 차장 | 1 |
|
||||
| 최태순 (J10301) | 건설본부 | 상무이사 | 1 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2025-12-24 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 5h / OT 0h | 21-시공-33 (지산교) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 인장관리자 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-10-02 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 7h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,반출 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-24 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 0.5공수 = 4h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-22 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 6h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-16 | 최영준 (J24201) | 25-영업-01 / 정규 5h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-15 | 김인열 (M05510) | 22-시공-41 (영산강교(물품)) / 정규 7h / OT 0h | 21-시공-23 (청룡2교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 인장/가설 관리자 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-11 | 최영준 (J24201) | 25-영업-01 / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1.5공수 = 12h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-05 | 최영준 (J24201) | 25-교영-01 (신규과업) / 정규 8h / OT 0h | 24-시공-30 (공릉천교 외) / 1.5공수 = 12h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-09-02 | 최영준 (J24201) | 24-교영-01 (2024년 신규과업) / 정규 6h / OT 0h | 24-시공-30 (공릉천교 외) / 1공수 = 8h / 사업관리 행 1개 / 작업내용: 거푸집해체,설치 | 프로젝트와 시간이 모두 다름 |
|
||||
| 2025-08-29 | 최영준 (J24201) | 24-교영-01 (2024년 신규과업) / 정규 8h / OT 2.5h | 24-시공-30 (공릉천교 외) / 0.5공수 = 4h / 사업관리 행 1개 / 작업내용: 거푸집해체 | 프로젝트와 시간이 모두 다름 |
|
||||
|
||||
## E. 인트라넷도 있고 사업관리 기록이 2개 이상인 경우
|
||||
|
||||
인트라넷에도 같은 날짜 기록이 있고, 사업관리에는 같은 날짜에 2개 이상 현장 기록이 있습니다.
|
||||
|
||||
- 건수: 5건
|
||||
- 인원수: 1명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 5 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2021-11-23 | 김인열 (M05510) | 21-시공-16 (양천2교) / 정규 8h / OT 3h | 21-시공-16 (양천2교), 21-시공-13 (안막교,안막IC교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 현장관리 | 인트라넷도 있고 사업관리 현장 기록 2개 이상 |
|
||||
| 2021-11-18 | 김인열 (M05510) | 21-시공-16 (양천2교) / 정규 8h / OT 3h | 21-시공-16 (양천2교), 21-시공-13 (안막교,안막IC교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 현장관리 | 인트라넷도 있고 사업관리 현장 기록 2개 이상 |
|
||||
| 2021-11-17 | 김인열 (M05510) | 21-시공-16 (양천2교) / 정규 8h / OT 3h | 21-시공-16 (양천2교), 21-시공-13 (안막교,안막IC교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 현장관리 | 인트라넷도 있고 사업관리 현장 기록 2개 이상 |
|
||||
| 2020-04-02 | 김인열 (M05510) | 20-시공-01 (검단천교) / 정규 5h / OT 0h | 20-시공-01 (검단천교), 18-시공-03 (왕림교,정남1육교) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 제작현장관리,제작 현장관리 | 인트라넷도 있고 사업관리 현장 기록 2개 이상 |
|
||||
| 2020-03-16 | 김인열 (M05510) | 18-시공-03 (왕림교,정남1육교) / 정규 8h / OT 0h | 18-시공-03 (왕림교,정남1육교), 17-시공-21 (북수원1교 외) / 2공수 = 16h / 사업관리 행 2개 / 작업내용: 인장/가설 인장관리,인장/가설 용접관리 | 인트라넷도 있고 사업관리 현장 기록 2개 이상 |
|
||||
|
||||
## F. 연차인데 근무기록이 있는 경우
|
||||
|
||||
userstate에는 연차로 등록되어 있지만 같은 날짜에 인트라넷 또는 사업관리 근무기록이 존재합니다.
|
||||
|
||||
- 건수: 66건
|
||||
- 인원수: 25명
|
||||
|
||||
### 사람별 건수
|
||||
|
||||
| 사람 | 팀 | 직급 | 건수 |
|
||||
|---|---|---|---:|
|
||||
| 이호범 (J23301) | 건설본부 | 사원 | 9 |
|
||||
| 이세민 (P13302) | 생산본부 | 부장 | 8 |
|
||||
| 김인수 (J18203) | 건설본부 | 상무이사 | 8 |
|
||||
| 김인열 (M05510) | 건설본부 | 전무이사 | 7 |
|
||||
| 최영준 (J24201) | 기술영업본부 | 전무이사 | 6 |
|
||||
| 최성용 (J23303) | 건설본부 | 대리 | 4 |
|
||||
| 남궁전 (J14101) | 기술영업본부 | 전무이사 | 2 |
|
||||
| 김승국 (M02302) | 생산본부 | 상무이사 | 2 |
|
||||
| 김범석 (J22202) | 건설본부 | 부장 | 2 |
|
||||
| 이중경 (J16202) | 기술영업본부 | 이사 | 2 |
|
||||
| 김진 (J10314) | 기술영업본부 | 이사 | 2 |
|
||||
| 최동찬 (J22301) | 생산본부 | 대리 | 1 |
|
||||
| 허남덕 (J10315) | 건설본부 | 전무이사 | 1 |
|
||||
| 윤두현 (J22204) | 건설본부 | 부장 | 1 |
|
||||
| 이가연 (J24302) | 기술영업본부 | 사원 | 1 |
|
||||
| 김재호 (J08201) | 기술영업본부 | 상무이사 | 1 |
|
||||
| 김상철 (J22302) | 건설본부 | 이사 | 1 |
|
||||
| 김량균 (J17207) | 기술영업본부 | 부사장 | 1 |
|
||||
| 김인범 (J23308) | 생산본부 | 사원 | 1 |
|
||||
| 최만규 (J22305) | 생산본부 | 과장 | 1 |
|
||||
| 차성대 (J10308) | 건설본부 | 상무이사 | 1 |
|
||||
| 임재상 (J25201) | 건설본부 | 대리 | 1 |
|
||||
| 윤선일 (J22201) | 건설본부 | 차장 | 1 |
|
||||
| 김갑성 (J24307) | 건설본부 | 전무이사 | 1 |
|
||||
| 정우성 (J23307) | 기술영업본부 | 상무이사 | 1 |
|
||||
|
||||
### 상세 예시
|
||||
|
||||
| 날짜 | 사람 | 인트라넷 | 사업관리 | 확인 포인트 |
|
||||
|---|---|---|---|---|
|
||||
| 2024-01-12 | 김인열 (M05510) | 없음 | 22-시공-39 (용두교) / 0.5공수 = 4h | 연차 등록: 연차(개인사유 ) |
|
||||
| 2024-01-30 | 최동찬 (J22301) | 24-제조-02 (생산) / 정규 8h / OT 3h | 없음 | 연차 등록: 연차(개인사유로 연차를 신청합니다.) |
|
||||
| 2020-01-03 | 남궁전 (J14101) | 16-시공-20 (산외교) / 정규 8h / OT 0h | 없음 | 연차 등록: 연차(개인사유) |
|
||||
| 2020-02-10 | 허남덕 (J10315) | 17-설계-05 (충청내륙(2-1) 구안천교) / 정규 8h / OT 0h | 없음 | 연차 등록: 연차(개인사유(감기몸살)) |
|
||||
| 2020-03-20 | 남궁전 (J14101) | 19-시공-22 (산외교-슬라브) / 정규 8h / OT 0h | 없음 | 연차 등록: 병원진료(안과 검진) |
|
||||
| 2021-02-04 | 김인열 (M05510) | 없음 | 20-시공-39 (가곡교, 평능2교) / 1공수 = 8h | 연차 등록: 연차(병원 허리 검사 진료) |
|
||||
| 2021-04-05 | 김승국 (M02302) | 20-기술-01 (스마트건설(10과제)) / 정규 3h / OT 0h | 없음 | 연차 등록: 연차(개인사유(코로나 검사 결과 대기)) |
|
||||
| 2022-02-03 | 김인열 (M05510) | 21-시공-14 (상관교 외) / 정규 8h / OT 3h | 없음 | 연차 등록: 연차(병원진료) |
|
||||
| 2022-07-29 | 김범석 (J22202) | 없음 | 20-시공-36 (도하4교) / 1공수 = 8h | 연차 등록: 연차(연차휴가사용계획서) |
|
||||
| 2022-08-12 | 김범석 (J22202) | 없음 | 20-시공-36 (도하4교) / 1공수 = 8h | 연차 등록: 연차(연차휴가사용계획서) |
|
||||
|
||||
## 해석 메모
|
||||
|
||||
- 사업관리 기록이 2개 이상인 경우는 하루에 여러 현장/프로젝트가 잡힌 것으로 보입니다.
|
||||
- 인트라넷과 사업관리 프로젝트가 다른 경우는 최종 프로젝트 판단 시 사업관리 프로젝트를 우선하는 것이 현재 합의된 기준입니다.
|
||||
- 연차인데 근무기록이 있는 경우는 userstate와 근무기록 간 충돌이므로 별도 확인이 필요합니다.
|
||||
- 단, 이 보고서는 현재 수집된 데이터 기준입니다. 사업관리 추가 수집 후 건수는 바뀔 수 있습니다.
|
||||
BIN
exports/abnormal_work_cases_report_portrait.docx
Normal file
BIN
exports/abnormal_work_cases_report_print.docx
Normal file
62
exports/create_erd_clean_image.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
OUT=Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
PNG=OUT/'work_data_erd_clean.png'
|
||||
W,H=1700,1150
|
||||
img=Image.new('RGB',(W,H),'#151718'); d=ImageDraw.Draw(img)
|
||||
font=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',17)
|
||||
font_b=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',20)
|
||||
font_t=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',30)
|
||||
font_s=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',14)
|
||||
BG='#151718'; CARD='#1f2426'; HEADER='#252b2e'; BORDER='#59636a'; TEXT='#edf2f4'; MUTED='#a9b2b8'; PK='#f5d76e'; FK='#87c8ff'
|
||||
BLUE='#60d7f2'; GREEN='#80e27e'; PURPLE='#b99bff'; GRAY='#9ca7ad'
|
||||
entities={
|
||||
'member':(70,150,300,['PK MemberNo','korName','teamName','rankName','groupCode','rankCode','isRetired']),
|
||||
'dallyproject':(70,490,330,['PK id','FK MemberNo','WorkDate','EntryPCode','EntryTime / LeaveTime','OverTime','TotalHours','RegularHours','OvertimeHours']),
|
||||
'project_alias':(70,875,330,['PK projectCode','shortName','bridge/project name']),
|
||||
'site_worksheet_worker_cache':(590,120,390,['PK projectCode + workDate','PK korName + jobType','workText','note','personCount','syncedAt']),
|
||||
'site_worksheet_record':(590,455,390,['PK projectCode + workDate','PK memberNo + korName','FK memberNo','jobType','workText','note','personCount']),
|
||||
'site_worksheet_day_sync':(590,760,390,['PK projectCode + workDate','syncedAt']),
|
||||
'site_worksheet_menu_sync':(590,940,390,['PK projectCode + workDate','PK selMenu','2 = normal worksheet','3 = tension/temp works','syncedAt']),
|
||||
'work_calendar_detail':(1190,255,370,['PK id','source: sql / site','FK memberNo','workDate','projectCode','projectName','workText','jobType','hours','regularHours','overtimeHours','personCount']),
|
||||
'work_calendar_day':(1190,745,370,['PK memberNo + workDate','korName / teamName / rankName','sqlHours','sqlProjectCodes','siteCount','siteProjectCodes','siteWorkTexts','hasSql / hasSite'])}
|
||||
heights={k:52+len(v[3])*25+18 for k,v in entities.items()}
|
||||
def card(n):
|
||||
x,y,w,fs=entities[n]; h=heights[n]
|
||||
d.rounded_rectangle((x,y,x+w,y+h),radius=8,fill=CARD,outline=BORDER,width=2)
|
||||
d.rectangle((x,y,x+w,y+42),fill=HEADER); d.line((x,y+42,x+w,y+42),fill=BORDER,width=1)
|
||||
d.text((x+16,y+11),n,font=font_b,fill=TEXT); yy=y+58
|
||||
for f in fs:
|
||||
c=PK if f.startswith('PK') else FK if f.startswith('FK') else TEXT
|
||||
d.text((x+18,yy),f,font=font_s,fill=c); yy+=25
|
||||
def a(n,s,off=0):
|
||||
x,y,w,_=entities[n]; h=heights[n]
|
||||
return {'r':(x+w,y+h//2+off),'l':(x,y+h//2+off),'t':(x+w//2+off,y),'b':(x+w//2+off,y+h)}[s]
|
||||
def arrow(points,label,color):
|
||||
d.line(points,fill=color,width=4,joint='curve')
|
||||
x,y=points[-1]; d.polygon([(x,y),(x-10,y-6),(x-10,y+6)],fill=color)
|
||||
mx,my=points[len(points)//2]
|
||||
tw=d.textlength(label,font=font_s)
|
||||
d.rounded_rectangle((mx-tw/2-8,my-14,mx+tw/2+8,my+14),radius=5,fill=BG,outline='#4b5358')
|
||||
d.text((mx-tw/2,my-9),label,font=font_s,fill=MUTED)
|
||||
# columns
|
||||
for x1,x2,title in [(40,430,'SOURCE'),(540,1030,'ERP CACHE / MATCH'),(1140,1620,'CALENDAR')]:
|
||||
d.rounded_rectangle((x1,105,x2,1080),radius=12,fill='#181b1c',outline='#2c3235')
|
||||
d.text((x1+22,126),title,font=font_b,fill='#7f8a90')
|
||||
d.text((60,38),'Work Data ERD',font=font_t,fill=TEXT)
|
||||
d.text((60,75),'Clean column layout with primary data-flow lines',font=font_s,fill=MUTED)
|
||||
# main flow only
|
||||
arrow([a('member','r',0),(500,a('member','r')[1]),(500,a('site_worksheet_record','l')[1]),a('site_worksheet_record','l')],'employee match',BLUE)
|
||||
arrow([a('dallyproject','r',0),(1100,a('dallyproject','r')[1]),(1100,a('work_calendar_detail','l',-55)[1]),a('work_calendar_detail','l',-55)],'SQL work -> detail',BLUE)
|
||||
arrow([a('project_alias','r',0),(520,a('project_alias','r')[1]),(520,a('site_worksheet_worker_cache','l',-20)[1]),a('site_worksheet_worker_cache','l',-20)],'project code/name',GREEN)
|
||||
arrow([a('site_worksheet_worker_cache','b'),(785,420),a('site_worksheet_record','t')],'raw rows -> matched rows',PURPLE)
|
||||
arrow([a('site_worksheet_day_sync','t'),(980,700),(980,300),a('site_worksheet_worker_cache','r',35)],'date fetched',GRAY)
|
||||
arrow([a('site_worksheet_menu_sync','t'),(1030,880),(1030,320),a('site_worksheet_worker_cache','r',75)],'menu 2/3 fetched',GRAY)
|
||||
arrow([a('site_worksheet_record','r'),(1100,a('site_worksheet_record','r')[1]),(1100,a('work_calendar_detail','l',45)[1]),a('work_calendar_detail','l',45)],'site work -> detail',PURPLE)
|
||||
arrow([a('project_alias','r',40),(1135,a('project_alias','r')[1]+40),(1135,a('work_calendar_detail','l',95)[1]),a('work_calendar_detail','l',95)],'projectName',GREEN)
|
||||
arrow([a('work_calendar_detail','b'),(1375,710),a('work_calendar_day','t')],'daily summary',GRAY)
|
||||
for n in entities: card(n)
|
||||
# legend
|
||||
d.rounded_rectangle((60,1090,1600,1138),radius=8,fill='#1d2123',outline='#3a4247')
|
||||
d.text((80,1103),'Blue employee/SQL Green project name Purple ERP worksheet match Gray sync/calendar flow',font=font_s,fill=MUTED)
|
||||
img.save(PNG); print(PNG)
|
||||
130
exports/create_erd_columns_image.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
PNG = OUT / 'work_data_erd_columns.png'
|
||||
W, H = 1800, 1250
|
||||
img = Image.new('RGB', (W, H), '#151718')
|
||||
d = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 17)
|
||||
font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 19)
|
||||
font_t = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 30)
|
||||
font_s = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14)
|
||||
|
||||
BG = '#151718'
|
||||
CARD = '#1f2426'
|
||||
HEADER = '#242a2d'
|
||||
BORDER = '#4c555a'
|
||||
LINE = '#8b969d'
|
||||
TEXT = '#e7ecef'
|
||||
MUTED = '#a6b0b6'
|
||||
PK = '#f6d365'
|
||||
FK = '#8bc7ff'
|
||||
BLUE = '#67d8ef'
|
||||
PURPLE = '#c4a7ff'
|
||||
GREEN = '#8be28b'
|
||||
|
||||
entities = {
|
||||
# col 1 source
|
||||
'member': (80, 170, 310, ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']),
|
||||
'dallyproject': (80, 500, 330, ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime', 'LeaveTime', 'OverTime', 'TotalHours', 'RegularHours', 'OvertimeHours']),
|
||||
'project_alias': (80, 900, 330, ['PK projectCode', 'shortName', 'bridge/project name']),
|
||||
# col 2 erp collection
|
||||
'site_worksheet_worker_cache': (610, 140, 400, ['PK projectCode + workDate', 'PK korName + jobType', 'workText', 'note', 'personCount', 'syncedAt']),
|
||||
'site_worksheet_record': (610, 500, 400, ['PK projectCode + workDate', 'PK memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'note', 'personCount']),
|
||||
'site_worksheet_day_sync': (610, 850, 400, ['PK projectCode + workDate', 'syncedAt']),
|
||||
'site_worksheet_menu_sync': (610, 1035, 400, ['PK projectCode + workDate', 'PK selMenu', '2 = normal worksheet', '3 = tension/temp works', 'syncedAt']),
|
||||
# col 3 integrated calendar
|
||||
'work_calendar_detail': (1240, 310, 390, ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'jobType', 'hours', 'regularHours', 'overtimeHours', 'personCount']),
|
||||
'work_calendar_day': (1240, 790, 390, ['PK memberNo + workDate', 'korName', 'teamName', 'rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql', 'hasSite']),
|
||||
}
|
||||
heights = {}
|
||||
for k,(x,y,w,fields) in entities.items():
|
||||
heights[k] = 52 + len(fields)*25 + 18
|
||||
|
||||
def draw_card(name):
|
||||
x,y,w,fields = entities[name]
|
||||
h = heights[name]
|
||||
d.rounded_rectangle((x,y,x+w,y+h), radius=8, fill=CARD, outline=BORDER, width=2)
|
||||
d.rectangle((x,y,x+w,y+42), fill=HEADER)
|
||||
d.line((x,y+42,x+w,y+42), fill=BORDER, width=1)
|
||||
d.text((x+16,y+11), name, font=font_b, fill=TEXT)
|
||||
yy = y+58
|
||||
for f in fields:
|
||||
color = TEXT
|
||||
if f.startswith('PK'): color = PK
|
||||
elif f.startswith('FK'): color = FK
|
||||
d.text((x+18,yy), f, font=font_s, fill=color)
|
||||
yy += 25
|
||||
|
||||
def anchor(name, side, offset=0):
|
||||
x,y,w,_ = entities[name]
|
||||
h = heights[name]
|
||||
if side == 'r': return (x+w, y+h//2+offset)
|
||||
if side == 'l': return (x, y+h//2+offset)
|
||||
if side == 't': return (x+w//2+offset, y)
|
||||
if side == 'b': return (x+w//2+offset, y+h)
|
||||
|
||||
def orth(a, b, label='', color=LINE, via_x=None, via_y=None):
|
||||
x1,y1 = a; x2,y2 = b
|
||||
pts = [(x1,y1)]
|
||||
if via_x is not None:
|
||||
pts += [(via_x,y1), (via_x,y2)]
|
||||
elif via_y is not None:
|
||||
pts += [(x1,via_y), (x2,via_y)]
|
||||
else:
|
||||
mx = (x1+x2)//2
|
||||
pts += [(mx,y1), (mx,y2)]
|
||||
pts += [(x2,y2)]
|
||||
d.line(pts, fill=color, width=3)
|
||||
for p in (pts[0], pts[-1]):
|
||||
d.ellipse((p[0]-5,p[1]-5,p[0]+5,p[1]+5), fill=color)
|
||||
if label:
|
||||
# place label around middle point
|
||||
mid = pts[len(pts)//2]
|
||||
tw = d.textlength(label, font=font_s)
|
||||
x,y = mid
|
||||
d.rounded_rectangle((x-tw/2-8,y-13,x+tw/2+8,y+13), radius=5, fill=BG, outline='#4a5054')
|
||||
d.text((x-tw/2,y-9), label, font=font_s, fill=MUTED)
|
||||
|
||||
# background columns
|
||||
cols = [(50,120,470,1135,'SOURCE'), (570,120,1050,1135,'ERP CACHE / MATCH'), (1200,120,1680,1135,'CALENDAR INTEGRATION')]
|
||||
for x1,y1,x2,y2,title in cols:
|
||||
d.rounded_rectangle((x1,y1,x2,y2), radius=12, fill='#181b1c', outline='#2a2f31')
|
||||
d.text((x1+20,y1+18), title, font=font_b, fill='#6f7a80')
|
||||
|
||||
# title
|
||||
d.text((60,40), 'Work Data ERD', font=font_t, fill=TEXT)
|
||||
d.text((60,78), 'Columns arranged by data flow: source -> ERP cache/match -> calendar integration', font=font_s, fill=MUTED)
|
||||
|
||||
# lines first
|
||||
orth(anchor('member','r',-25), anchor('dallyproject','l',-60), 'MemberNo', via_x=500, color=BLUE)
|
||||
orth(anchor('member','r',10), anchor('site_worksheet_record','l',-40), 'MemberNo', via_x=535, color=BLUE)
|
||||
orth(anchor('project_alias','r',-30), anchor('dallyproject','r',20), 'EntryPCode', via_x=500, color=GREEN)
|
||||
# project alias to erp and calendar
|
||||
orth(anchor('project_alias','r',10), anchor('site_worksheet_worker_cache','l',-30), 'projectCode', via_x=540, color=GREEN)
|
||||
# cache and sync to worker cache
|
||||
orth(anchor('site_worksheet_day_sync','t'), anchor('site_worksheet_worker_cache','b',80), 'project+date fetched', via_x=1110, color=LINE)
|
||||
ord_start = anchor('site_worksheet_menu_sync','t')
|
||||
orth(ord_start, anchor('site_worksheet_worker_cache','b',150), 'menu 2/3 fetched', via_x=1160, color=LINE)
|
||||
# worker cache to record
|
||||
orth(anchor('site_worksheet_worker_cache','b'), anchor('site_worksheet_record','t'), 'match name/date/project', via_x=820, color=PURPLE)
|
||||
# sources to detail
|
||||
orth(anchor('dallyproject','r',-10), anchor('work_calendar_detail','l',-80), 'source=sql', via_x=1120, color=BLUE)
|
||||
orth(anchor('site_worksheet_record','r'), anchor('work_calendar_detail','l',20), 'source=site', via_x=1120, color=PURPLE)
|
||||
orth(anchor('project_alias','r',55), anchor('work_calendar_detail','l',70), 'projectName', via_x=1120, color=GREEN)
|
||||
# detail to day
|
||||
orth(anchor('work_calendar_detail','b'), anchor('work_calendar_day','t'), 'daily summary', via_x=1435, color=LINE)
|
||||
# member to day
|
||||
orth(anchor('member','r',55), anchor('work_calendar_day','l',-60), 'MemberNo + workDate', via_x=520, color=BLUE)
|
||||
|
||||
for name in entities:
|
||||
draw_card(name)
|
||||
|
||||
# legend
|
||||
d.rounded_rectangle((60,1160,1660,1225), radius=8, fill='#1c2022', outline='#343a3e')
|
||||
d.text((80,1178), 'Legend', font=font_b, fill=TEXT)
|
||||
d.text((180,1179), 'Blue = employee relation Green = project relation Purple = ERP site worksheet matching Gray = sync/calendar flow', font=font_s, fill=MUTED)
|
||||
|
||||
img.save(PNG)
|
||||
print(PNG)
|
||||
135
exports/create_erd_dark_image.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
PNG = OUT / 'work_data_erd_dark.png'
|
||||
W, H = 1400, 1900
|
||||
img = Image.new('RGB', (W, H), '#17191a')
|
||||
d = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 17)
|
||||
font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 18)
|
||||
font_t = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 24)
|
||||
font_s = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 13)
|
||||
|
||||
BG = '#17191a'
|
||||
CARD = '#1e2224'
|
||||
CARD2 = '#202426'
|
||||
LINE = '#4b5256'
|
||||
LINE2 = '#697176'
|
||||
TEXT = '#e6ecef'
|
||||
MUTED = '#9aa4aa'
|
||||
PK = '#f4d06f'
|
||||
FK = '#8cc7ff'
|
||||
ACCENT = '#d0d6d9'
|
||||
|
||||
entities = {
|
||||
'member': {
|
||||
'xy': (150, 360), 'w': 250,
|
||||
'fields': ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']
|
||||
},
|
||||
'dallyproject': {
|
||||
'xy': (185, 720), 'w': 290,
|
||||
'fields': ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime', 'LeaveTime', 'OverTime', 'TotalHours', 'RegularHours', 'OvertimeHours']
|
||||
},
|
||||
'work_calendar_day': {
|
||||
'xy': (70, 1190), 'w': 315,
|
||||
'fields': ['PK memberNo + workDate', 'korName', 'teamName', 'rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql', 'hasSite']
|
||||
},
|
||||
'work_calendar_detail': {
|
||||
'xy': (515, 920), 'w': 310,
|
||||
'fields': ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'jobType', 'hours', 'regularHours', 'overtimeHours', 'personCount']
|
||||
},
|
||||
'project_alias': {
|
||||
'xy': (655, 70), 'w': 315,
|
||||
'fields': ['PK projectCode', 'shortName', 'bridge/project name']
|
||||
},
|
||||
'site_worksheet_record': {
|
||||
'xy': (680, 430), 'w': 340,
|
||||
'fields': ['PK projectCode + workDate', 'PK memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'note', 'personCount']
|
||||
},
|
||||
'site_worksheet_worker_cache': {
|
||||
'xy': (960, 170), 'w': 360,
|
||||
'fields': ['PK projectCode + workDate', 'PK korName + jobType', 'workText', 'note', 'personCount', 'syncedAt']
|
||||
},
|
||||
'site_worksheet_day_sync': {
|
||||
'xy': (930, 760), 'w': 310,
|
||||
'fields': ['PK projectCode + workDate', 'syncedAt']
|
||||
},
|
||||
'site_worksheet_menu_sync': {
|
||||
'xy': (860, 1090), 'w': 335,
|
||||
'fields': ['PK projectCode + workDate', 'PK selMenu', '2 = normal worksheet', '3 = tension/temp works', 'syncedAt']
|
||||
},
|
||||
}
|
||||
|
||||
# compute heights
|
||||
for e in entities.values():
|
||||
e['h'] = 52 + len(e['fields']) * 26 + 18
|
||||
|
||||
|
||||
def rounded_box(x, y, w, h, name, fields):
|
||||
d.rounded_rectangle((x, y, x+w, y+h), radius=6, fill=CARD, outline='#3c4246', width=2)
|
||||
d.rectangle((x, y, x+w, y+42), fill=CARD2)
|
||||
d.line((x, y+42, x+w, y+42), fill='#3c4246', width=1)
|
||||
d.text((x+16, y+12), name, font=font_b, fill=TEXT)
|
||||
yy = y + 58
|
||||
for f in fields:
|
||||
color = TEXT
|
||||
if f.startswith('PK'):
|
||||
color = PK
|
||||
elif f.startswith('FK'):
|
||||
color = FK
|
||||
d.text((x+18, yy), f, font=font_s, fill=color)
|
||||
yy += 26
|
||||
|
||||
|
||||
def pt(name, side):
|
||||
e = entities[name]
|
||||
x, y = e['xy']; w = e['w']; h = e['h']
|
||||
if side == 'l': return (x, y+h//2)
|
||||
if side == 'r': return (x+w, y+h//2)
|
||||
if side == 't': return (x+w//2, y)
|
||||
if side == 'b': return (x+w//2, y+h)
|
||||
|
||||
|
||||
def poly(points, label=None, label_pos=None):
|
||||
d.line(points, fill=LINE, width=2, joint='curve')
|
||||
for x, y in (points[0], points[-1]):
|
||||
d.ellipse((x-4, y-4, x+4, y+4), fill=ACCENT)
|
||||
if label:
|
||||
x, y = label_pos or points[len(points)//2]
|
||||
tw = d.textlength(label, font=font_s)
|
||||
d.rounded_rectangle((x-tw/2-7, y-12, x+tw/2+7, y+12), radius=4, fill=BG, outline='#363b3f')
|
||||
d.text((x-tw/2, y-9), label, font=font_s, fill=MUTED)
|
||||
|
||||
# title
|
||||
d.text((70, 35), 'Work Data ERD', font=font_t, fill=TEXT)
|
||||
d.text((70, 68), 'SQL work records + ERP site worksheet integration', font=font_s, fill=MUTED)
|
||||
|
||||
# relation lines behind cards
|
||||
poly([pt('member','b'), (275, 665), pt('dallyproject','t')], 'MemberNo', (300, 650))
|
||||
poly([pt('dallyproject','b'), (330, 1085), (520, 1085), pt('work_calendar_detail','l')], 'source=sql', (450, 1072))
|
||||
poly([pt('work_calendar_detail','l'), (425, 1100), (425, 1350), pt('work_calendar_day','r')], 'daily summary', (426, 1280))
|
||||
poly([pt('member','l'), (55, 460), (55, 1370), pt('work_calendar_day','l')], 'MemberNo', (56, 820))
|
||||
poly([pt('member','r'), (520, 500), (520, 560), pt('site_worksheet_record','l')], 'MemberNo', (520, 538))
|
||||
poly([pt('project_alias','b'), (815, 330), pt('site_worksheet_record','t')], 'projectCode', (810, 340))
|
||||
poly([pt('project_alias','r'), (1070, 170), pt('site_worksheet_worker_cache','l')], 'projectCode', (1050, 145))
|
||||
poly([pt('site_worksheet_worker_cache','b'), (1050, 405), (1000, 405), pt('site_worksheet_record','r')], 'match name/date/project', (1010, 410))
|
||||
poly([pt('site_worksheet_record','b'), (740, 880), pt('work_calendar_detail','t')], 'source=site', (715, 850))
|
||||
poly([pt('site_worksheet_day_sync','t'), (1005, 650), (1080, 400), pt('site_worksheet_worker_cache','b')], 'project+date fetched', (1040, 645))
|
||||
poly([pt('site_worksheet_menu_sync','t'), (935, 1030), (1085, 400), pt('site_worksheet_worker_cache','b')], 'menu 2/3 fetched', (950, 1035))
|
||||
poly([pt('project_alias','l'), (560, 185), (560, 825), pt('dallyproject','r')], 'EntryPCode', (560, 420))
|
||||
poly([pt('project_alias','b'), (750, 780), pt('work_calendar_detail','r')], 'projectName', (755, 760))
|
||||
|
||||
# draw cards
|
||||
for name, e in entities.items():
|
||||
x, y = e['xy']
|
||||
rounded_box(x, y, e['w'], e['h'], name, e['fields'])
|
||||
|
||||
# subtle legend
|
||||
d.rounded_rectangle((70, 1740, 1270, 1845), radius=8, fill='#1c2022', outline='#343a3e')
|
||||
d.text((90, 1765), 'Flow', font=font_b, fill=TEXT)
|
||||
d.text((90, 1795), 'dallyproject -> work_calendar_detail -> work_calendar_day', font=font_s, fill=MUTED)
|
||||
d.text((90, 1820), 'site_worksheet_worker_cache -> site_worksheet_record -> work_calendar_detail/day', font=font_s, fill=MUTED)
|
||||
|
||||
img.save(PNG)
|
||||
print(PNG)
|
||||
109
exports/create_erd_image.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
PNG = OUT / 'work_data_erd.png'
|
||||
SVG = OUT / 'work_data_erd.svg'
|
||||
W, H = 1900, 1280
|
||||
img = Image.new('RGB', (W, H), '#f6f8fb')
|
||||
d = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 18)
|
||||
font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 20)
|
||||
font_title = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 34)
|
||||
font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 15)
|
||||
|
||||
entities = {
|
||||
'member': (70, 110, 330, 330, ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']),
|
||||
'dallyproject': (470, 80, 780, 360, ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime / LeaveTime', 'TotalHours', 'RegularHours', 'OvertimeHours']),
|
||||
'project_alias': (1040, 110, 1330, 280, ['PK projectCode', 'shortName (bridge/project name)']),
|
||||
'site_worksheet_worker_cache': (1360, 360, 1810, 610, ['PK projectCode + workDate + korName', 'jobType', 'workText', 'note', 'personCount', 'syncedAt']),
|
||||
'site_worksheet_record': (900, 430, 1260, 690, ['PK projectCode + workDate + memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'personCount']),
|
||||
'site_worksheet_day_sync': (1370, 720, 1740, 870, ['PK projectCode + workDate', 'syncedAt']),
|
||||
'site_worksheet_menu_sync': (1370, 930, 1740, 1090, ['PK projectCode + workDate + selMenu', 'selMenu 2 = normal', 'selMenu 3 = tension/temp works', 'syncedAt']),
|
||||
'work_calendar_detail': (520, 800, 900, 1110, ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'hours / regular / overtime', 'personCount']),
|
||||
'work_calendar_day': (120, 770, 430, 1080, ['PK memberNo + workDate', 'korName / teamName / rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql / hasSite']),
|
||||
}
|
||||
|
||||
colors = {
|
||||
'member': ('#e0f2fe', '#0369a1'),
|
||||
'dallyproject': ('#ecfdf5', '#047857'),
|
||||
'project_alias': ('#fff7ed', '#c2410c'),
|
||||
'site_worksheet_worker_cache': ('#fef3c7', '#b45309'),
|
||||
'site_worksheet_record': ('#ede9fe', '#6d28d9'),
|
||||
'site_worksheet_day_sync': ('#f1f5f9', '#475569'),
|
||||
'site_worksheet_menu_sync': ('#f1f5f9', '#475569'),
|
||||
'work_calendar_detail': ('#fee2e2', '#b91c1c'),
|
||||
'work_calendar_day': ('#dbeafe', '#1d4ed8'),
|
||||
}
|
||||
|
||||
def box(name, rect, fields):
|
||||
x1,y1,x2,y2 = rect
|
||||
fill, stroke = colors[name]
|
||||
d.rounded_rectangle(rect, radius=14, fill=fill, outline=stroke, width=3)
|
||||
d.rectangle((x1, y1, x2, y1+42), fill=stroke)
|
||||
d.text((x1+14, y1+10), name, fill='white', font=font_b)
|
||||
y = y1 + 55
|
||||
for f in fields:
|
||||
d.text((x1+16, y), f, fill='#0f172a', font=font_small if len(f) > 34 else font)
|
||||
y += 26
|
||||
|
||||
def center(name, side):
|
||||
x1,y1,x2,y2 = entities[name][:4]
|
||||
if side == 'right': return (x2, (y1+y2)//2)
|
||||
if side == 'left': return (x1, (y1+y2)//2)
|
||||
if side == 'top': return ((x1+x2)//2, y1)
|
||||
if side == 'bottom': return ((x1+x2)//2, y2)
|
||||
|
||||
def line(a, aside, b, bside, label, color='#334155'):
|
||||
p1 = center(a, aside); p2 = center(b, bside)
|
||||
d.line((p1, p2), fill=color, width=3)
|
||||
# endpoint dots
|
||||
for p in (p1, p2):
|
||||
d.ellipse((p[0]-5,p[1]-5,p[0]+5,p[1]+5), fill=color)
|
||||
mx, my = (p1[0]+p2[0])//2, (p1[1]+p2[1])//2
|
||||
tw = d.textlength(label, font=font_small)
|
||||
d.rounded_rectangle((mx-tw/2-8, my-13, mx+tw/2+8, my+13), radius=7, fill='#ffffff', outline='#cbd5e1')
|
||||
d.text((mx-tw/2, my-9), label, fill=color, font=font_small)
|
||||
|
||||
# title
|
||||
d.text((70, 30), 'Work Data ERD: SQL + Site Worksheet Integration', fill='#0f172a', font=font_title)
|
||||
d.text((72, 72), 'SQLite DB: /home/hyein/jh-mh/장헌산업/matching.db', fill='#475569', font=font_small)
|
||||
|
||||
for name, data in entities.items():
|
||||
box(name, data[:4], data[4])
|
||||
|
||||
# relationships
|
||||
line('member','right','dallyproject','left','MemberNo')
|
||||
line('member','right','site_worksheet_record','left','MemberNo')
|
||||
line('member','bottom','work_calendar_day','top','MemberNo + workDate')
|
||||
line('dallyproject','bottom','work_calendar_detail','top','source=sql')
|
||||
line('site_worksheet_record','bottom','work_calendar_detail','right','source=site')
|
||||
line('work_calendar_detail','left','work_calendar_day','right','daily summary')
|
||||
line('project_alias','bottom','site_worksheet_record','top','projectCode')
|
||||
line('project_alias','right','site_worksheet_worker_cache','top','projectCode')
|
||||
line('site_worksheet_worker_cache','left','site_worksheet_record','right','match by name/date/project')
|
||||
line('site_worksheet_day_sync','top','site_worksheet_worker_cache','bottom','project+date fetched')
|
||||
line('site_worksheet_menu_sync','top','site_worksheet_worker_cache','bottom','menu 2/3 fetched')
|
||||
line('project_alias','left','dallyproject','right','EntryPCode')
|
||||
line('project_alias','bottom','work_calendar_detail','right','projectName')
|
||||
|
||||
# legend
|
||||
lx, ly = 70, 1140
|
||||
d.rounded_rectangle((lx, ly, 760, ly+95), radius=12, fill='#ffffff', outline='#cbd5e1', width=2)
|
||||
d.text((lx+18, ly+14), 'Flow', font=font_b, fill='#0f172a')
|
||||
d.text((lx+18, ly+43), 'dallyproject = SQL work records / site_worksheet_worker_cache = ERP site worksheet raw rows', font=font_small, fill='#334155')
|
||||
d.text((lx+18, ly+68), 'site_worksheet_record = matched employee rows / work_calendar_* = calendar-ready integrated data', font=font_small, fill='#334155')
|
||||
|
||||
img.save(PNG)
|
||||
|
||||
# simple SVG wrapper embeds the PNG path as text fallback is not needed; create standalone SVG rectangles too minimal
|
||||
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">
|
||||
<rect width="100%" height="100%" fill="#f6f8fb"/>
|
||||
<text x="70" y="55" font-family="DejaVu Sans, Arial" font-size="34" font-weight="700" fill="#0f172a">Work Data ERD: SQL + Site Worksheet Integration</text>
|
||||
<text x="72" y="82" font-family="DejaVu Sans, Arial" font-size="15" fill="#475569">PNG version generated at {PNG}</text>
|
||||
<image href="{PNG.name}" x="0" y="0" width="{W}" height="{H}" opacity="0"/>
|
||||
<text x="70" y="150" font-family="DejaVu Sans, Arial" font-size="22" fill="#334155">Open the PNG file for the full ERD diagram.</text>
|
||||
</svg>'''
|
||||
SVG.write_text(svg, encoding='utf-8')
|
||||
print(PNG)
|
||||
print(SVG)
|
||||
95
exports/create_erd_split_images.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
OUT=Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
font=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',17)
|
||||
font_b=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',20)
|
||||
font_t=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',30)
|
||||
font_s=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',14)
|
||||
BG='#151718'; CARD='#1f2426'; HEADER='#252b2e'; BORDER='#59636a'; TEXT='#edf2f4'; MUTED='#a9b2b8'; PK='#f5d76e'; FK='#87c8ff'; BLUE='#60d7f2'; GREEN='#80e27e'; PURPLE='#b99bff'; GRAY='#9ca7ad'
|
||||
|
||||
def draw_erd(path, title, subtitle, entities, lines, legend):
|
||||
W,H=1500,1000
|
||||
img=Image.new('RGB',(W,H),BG); d=ImageDraw.Draw(img)
|
||||
heights={k:52+len(v[3])*25+18 for k,v in entities.items()}
|
||||
def card(n):
|
||||
x,y,w,fs=entities[n]; h=heights[n]
|
||||
d.rounded_rectangle((x,y,x+w,y+h),radius=8,fill=CARD,outline=BORDER,width=2)
|
||||
d.rectangle((x,y,x+w,y+42),fill=HEADER); d.line((x,y+42,x+w,y+42),fill=BORDER,width=1)
|
||||
d.text((x+16,y+11),n,font=font_b,fill=TEXT)
|
||||
yy=y+58
|
||||
for f in fs:
|
||||
c=PK if f.startswith('PK') else FK if f.startswith('FK') else TEXT
|
||||
d.text((x+18,yy),f,font=font_s,fill=c); yy+=25
|
||||
def a(n,s,off=0):
|
||||
x,y,w,_=entities[n]; h=heights[n]
|
||||
return {'r':(x+w,y+h//2+off),'l':(x,y+h//2+off),'t':(x+w//2+off,y),'b':(x+w//2+off,y+h)}[s]
|
||||
def arrow(p1,p2,label,color,via_x=None,via_y=None):
|
||||
x1,y1=p1; x2,y2=p2
|
||||
pts=[p1]
|
||||
if via_x is not None: pts += [(via_x,y1),(via_x,y2)]
|
||||
elif via_y is not None: pts += [(x1,via_y),(x2,via_y)]
|
||||
else:
|
||||
mx=(x1+x2)//2; pts += [(mx,y1),(mx,y2)]
|
||||
pts += [p2]
|
||||
d.line(pts,fill=color,width=4,joint='curve')
|
||||
x,y=pts[-1]
|
||||
if x2>=x1: tri=[(x,y),(x-10,y-6),(x-10,y+6)]
|
||||
else: tri=[(x,y),(x+10,y-6),(x+10,y+6)]
|
||||
d.polygon(tri,fill=color)
|
||||
mid=pts[len(pts)//2]
|
||||
tw=d.textlength(label,font=font_s)
|
||||
d.rounded_rectangle((mid[0]-tw/2-8,mid[1]-14,mid[0]+tw/2+8,mid[1]+14),radius=5,fill=BG,outline='#4b5358')
|
||||
d.text((mid[0]-tw/2,mid[1]-9),label,font=font_s,fill=MUTED)
|
||||
d.text((60,35),title,font=font_t,fill=TEXT)
|
||||
d.text((60,73),subtitle,font=font_s,fill=MUTED)
|
||||
for x1,x2,col_title in [(45,445,'SOURCE'),(535,985,'MATCH / DETAIL'),(1090,1455,'OUTPUT')]:
|
||||
d.rounded_rectangle((x1,110,x2,890),radius=12,fill='#181b1c',outline='#2c3235')
|
||||
d.text((x1+20,130),col_title,font=font_b,fill='#7f8a90')
|
||||
for ln in lines:
|
||||
src,ss,dst,ds,label,color,via=ln
|
||||
kwargs={}
|
||||
if via and via[0]=='x': kwargs['via_x']=via[1]
|
||||
if via and via[0]=='y': kwargs['via_y']=via[1]
|
||||
arrow(a(src,ss),a(dst,ds),label,color,**kwargs)
|
||||
for n in entities: card(n)
|
||||
d.rounded_rectangle((60,920,1440,970),radius=8,fill='#1d2123',outline='#3a4247')
|
||||
d.text((80,936),legend,font=font_s,fill=MUTED)
|
||||
img.save(path)
|
||||
print(path)
|
||||
|
||||
people_entities={
|
||||
'member':(80,170,320,['PK MemberNo','korName','teamName','rankName','groupCode','rankCode','isRetired']),
|
||||
'dallyproject':(80,510,350,['PK id','FK MemberNo','WorkDate','EntryPCode','TotalHours','RegularHours','OvertimeHours']),
|
||||
'site_worksheet_record':(570,250,390,['PK projectCode + workDate','PK memberNo + korName','FK memberNo','jobType','workText','personCount']),
|
||||
'work_calendar_detail':(570,610,390,['PK id','source: sql / site','FK memberNo','workDate','projectCode','hours','personCount']),
|
||||
'work_calendar_day':(1120,360,360,['PK memberNo + workDate','korName / teamName / rankName','sqlHours','siteCount','sqlProjectCodes','siteProjectCodes','hasSql / hasSite'])}
|
||||
people_lines=[
|
||||
('member','r','site_worksheet_record','l','employee match',BLUE,('x',500)),
|
||||
('member','r','dallyproject','l','MemberNo',BLUE,('y',470)),
|
||||
('dallyproject','r','work_calendar_detail','l','SQL rows',BLUE,('x',505)),
|
||||
('site_worksheet_record','b','work_calendar_detail','t','site rows',PURPLE,('x',765)),
|
||||
('work_calendar_detail','r','work_calendar_day','l','daily summary',GRAY,('x',1040)),
|
||||
('member','r','work_calendar_day','l','person/day output',BLUE,('x',1030)),
|
||||
]
|
||||
|
||||
project_entities={
|
||||
'project_alias':(80,190,350,['PK projectCode','shortName','bridge/project name']),
|
||||
'dallyproject':(80,520,350,['PK id','EntryPCode','WorkDate','MemberNo','TotalHours','OvertimeHours']),
|
||||
'site_worksheet_worker_cache':(570,150,420,['PK projectCode + workDate','PK korName + jobType','workText','note','personCount','syncedAt']),
|
||||
'site_worksheet_day_sync':(570,500,390,['PK projectCode + workDate','syncedAt']),
|
||||
'site_worksheet_menu_sync':(570,700,390,['PK projectCode + workDate','PK selMenu','2 = normal worksheet','3 = tension/temp works','syncedAt']),
|
||||
'work_calendar_detail':(1120,240,370,['PK id','source: sql / site','projectCode','projectName','workText','memberNo','hours','personCount']),
|
||||
'work_calendar_day':(1120,650,370,['PK memberNo + workDate','siteProjectCodes','siteWorkTexts','sqlProjectCodes','siteCount','hasSql / hasSite'])}
|
||||
project_lines=[
|
||||
('project_alias','r','site_worksheet_worker_cache','l','projectCode',GREEN,('x',515)),
|
||||
('project_alias','r','dallyproject','l','EntryPCode',GREEN,('y',450)),
|
||||
('site_worksheet_day_sync','t','site_worksheet_worker_cache','b','date fetched',GRAY,('x',1040)),
|
||||
('site_worksheet_menu_sync','t','site_worksheet_worker_cache','b','menu 2/3 fetched',GRAY,('x',1080)),
|
||||
('site_worksheet_worker_cache','r','work_calendar_detail','l','ERP raw site rows',PURPLE,('x',1040)),
|
||||
('dallyproject','r','work_calendar_detail','l','SQL project rows',BLUE,('x',1040)),
|
||||
('project_alias','r','work_calendar_detail','l','projectName',GREEN,('x',1040)),
|
||||
('work_calendar_detail','b','work_calendar_day','t','calendar output',GRAY,('x',1305)),
|
||||
]
|
||||
|
||||
draw_erd(OUT/'work_data_erd_people.png','People-Centered ERD','How one employee is connected to SQL work, ERP site work, and calendar output',people_entities,people_lines,'Blue = employee/SQL relation Purple = ERP matched rows Gray = calendar aggregation')
|
||||
draw_erd(OUT/'work_data_erd_project.png','Project-Centered ERD','How project codes, bridge names, ERP worksheet menus, and calendar details are connected',project_entities,project_lines,'Green = project code/name Purple = ERP worksheet data Blue = SQL project rows Gray = sync/calendar flow')
|
||||
159
exports/create_erd_unified_lanes.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path('/home/hyein/jh-mh/장헌산업/exports')
|
||||
PNG = OUT / 'work_data_erd_unified_lanes.png'
|
||||
W, H = 1900, 1980
|
||||
img = Image.new('RGB', (W, H), '#151718')
|
||||
d = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 18)
|
||||
font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 21)
|
||||
font_t = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 34)
|
||||
font_s = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 15)
|
||||
font_badge = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 16)
|
||||
|
||||
BG = '#151718'
|
||||
PANEL = '#181b1c'
|
||||
CARD = '#202527'
|
||||
HEADER = '#293033'
|
||||
BORDER = '#667179'
|
||||
TEXT = '#eef3f5'
|
||||
MUTED = '#a8b1b7'
|
||||
PK = '#f5d76e'
|
||||
FK = '#87c8ff'
|
||||
PEOPLE = '#55d8f4'
|
||||
PROJECT = '#81e681'
|
||||
ERP = '#b99bff'
|
||||
CAL = '#a6b0b6'
|
||||
BADGE = '#263036'
|
||||
|
||||
entities = {
|
||||
# left source column
|
||||
'member': (80, 210, 330, ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']),
|
||||
'dallyproject': (80, 585, 360, ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime / LeaveTime', 'OverTime', 'TotalHours', 'RegularHours', 'OvertimeHours']),
|
||||
'project_alias': (80, 1035, 360, ['PK projectCode', 'shortName', 'bridge/project name']),
|
||||
|
||||
# shared center columns
|
||||
'site_worksheet_worker_cache': (640, 170, 430, ['PK projectCode + workDate', 'PK korName + jobType', 'workText', 'note', 'personCount', 'syncedAt']),
|
||||
'site_worksheet_record': (640, 570, 430, ['PK projectCode + workDate', 'PK memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'note', 'personCount']),
|
||||
'site_worksheet_day_sync': (640, 960, 410, ['PK projectCode + workDate', 'syncedAt']),
|
||||
'site_worksheet_menu_sync': (640, 1140, 410, ['PK projectCode + workDate', 'PK selMenu', '2 = normal worksheet', '3 = tension/temp works', 'syncedAt']),
|
||||
|
||||
# right output column
|
||||
'work_calendar_detail': (1350, 330, 430, ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'jobType', 'hours', 'regularHours', 'overtimeHours', 'personCount']),
|
||||
'work_calendar_day': (1350, 920, 430, ['PK memberNo + workDate', 'korName / teamName / rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql / hasSite']),
|
||||
'project_yearly_summary': (1350, 1285, 430, ['VIEW project tab output', 'projectCode + yearMonth', 'memberNo / korName', 'intranetRegularHours', 'siteHours = personCount * 8', 'commonHours', 'total = intranet + site - common']),
|
||||
}
|
||||
heights = {k: 54 + len(v[3]) * 27 + 20 for k, v in entities.items()}
|
||||
|
||||
relations = [
|
||||
('1', 'member -> dallyproject', PEOPLE),
|
||||
('2', 'member -> site_worksheet_record', PEOPLE),
|
||||
('3', 'project_alias -> dallyproject', PROJECT),
|
||||
('4', 'project_alias -> worker_cache', PROJECT),
|
||||
('5', 'day_sync: fetched project/date', CAL),
|
||||
('6', 'menu_sync: fetched menu 2/3', CAL),
|
||||
('7', 'worker_cache -> record', ERP),
|
||||
('8', 'dallyproject -> calendar_detail', PEOPLE),
|
||||
('9', 'record -> calendar_detail', ERP),
|
||||
('10', 'project_alias -> projectName', PROJECT),
|
||||
('11', 'calendar_detail -> calendar_day', CAL),
|
||||
('12', 'dallyproject -> project summary', PEOPLE),
|
||||
('13', 'record -> project summary', ERP),
|
||||
]
|
||||
|
||||
def panel(x1, y1, x2, y2, title, color):
|
||||
d.rounded_rectangle((x1, y1, x2, y2), radius=16, fill=PANEL, outline='#2e3437', width=2)
|
||||
d.text((x1 + 24, y1 + 20), title, font=font_b, fill=color)
|
||||
|
||||
|
||||
def card(name):
|
||||
x, y, w, fields = entities[name]
|
||||
h = heights[name]
|
||||
d.rounded_rectangle((x, y, x + w, y + h), radius=8, fill=CARD, outline=BORDER, width=2)
|
||||
d.rectangle((x, y, x + w, y + 46), fill=HEADER)
|
||||
d.line((x, y + 46, x + w, y + 46), fill=BORDER, width=1)
|
||||
d.text((x + 18, y + 12), name, font=font_b, fill=TEXT)
|
||||
yy = y + 64
|
||||
for f in fields:
|
||||
color = PK if f.startswith('PK') else FK if f.startswith('FK') else TEXT
|
||||
d.text((x + 20, yy), f, font=font_s, fill=color)
|
||||
yy += 27
|
||||
|
||||
|
||||
def anchor(name, side, off=0):
|
||||
x, y, w, _ = entities[name]
|
||||
h = heights[name]
|
||||
if side == 'r': return (x + w, y + h // 2 + off)
|
||||
if side == 'l': return (x, y + h // 2 + off)
|
||||
if side == 't': return (x + w // 2 + off, y)
|
||||
if side == 'b': return (x + w // 2 + off, y + h)
|
||||
|
||||
|
||||
def badge(num, x, y, color):
|
||||
d.ellipse((x - 14, y - 14, x + 14, y + 14), fill=BADGE, outline=color, width=2)
|
||||
tw = d.textlength(num, font=font_badge)
|
||||
d.text((x - tw / 2, y - 10), num, font=font_badge, fill=color)
|
||||
|
||||
|
||||
def arrow(points, color, num=None, badge_at=None):
|
||||
d.line(points, fill=color, width=4, joint='curve')
|
||||
x1, y1 = points[-2]
|
||||
x2, y2 = points[-1]
|
||||
if abs(x2 - x1) >= abs(y2 - y1):
|
||||
tri = [(x2, y2), (x2 - 12 if x2 >= x1 else x2 + 12, y2 - 7), (x2 - 12 if x2 >= x1 else x2 + 12, y2 + 7)]
|
||||
else:
|
||||
tri = [(x2, y2), (x2 - 7, y2 - 12 if y2 >= y1 else y2 + 12), (x2 + 7, y2 - 12 if y2 >= y1 else y2 + 12)]
|
||||
d.polygon(tri, fill=color)
|
||||
if num:
|
||||
bx, by = badge_at if badge_at else points[len(points)//2]
|
||||
badge(num, bx, by, color)
|
||||
|
||||
# title
|
||||
d.text((70, 40), 'Unified Work Data ERD', font=font_t, fill=TEXT)
|
||||
d.text((72, 82), 'One diagram, separated into people flow and project flow. Numbered connectors avoid line-label overlap.', font=font_s, fill=MUTED)
|
||||
|
||||
# panels
|
||||
panel(45, 130, 500, 1535, 'SOURCE TABLES', MUTED)
|
||||
panel(585, 130, 1130, 1535, 'SHARED ERP CACHE / MATCH TABLES', MUTED)
|
||||
panel(1300, 130, 1860, 1535, 'OUTPUT / VIEW TABLES', MUTED)
|
||||
|
||||
|
||||
# connectors without text labels
|
||||
arrow([anchor('member','b'), (245, 545), anchor('dallyproject','t')], PEOPLE, '1', (245, 545))
|
||||
arrow([anchor('member','r',10), (555, anchor('member','r',10)[1]), (555, anchor('site_worksheet_record','l',-40)[1]), anchor('site_worksheet_record','l',-40)], PEOPLE, '2', (555, 470))
|
||||
arrow([anchor('project_alias','t'), (260, 925), anchor('dallyproject','b')], PROJECT, '3', (260, 925))
|
||||
arrow([anchor('project_alias','r',-20), (560, anchor('project_alias','r',-20)[1]), (560, anchor('site_worksheet_worker_cache','l',-35)[1]), anchor('site_worksheet_worker_cache','l',-35)], PROJECT, '4', (560, 825))
|
||||
arrow([anchor('site_worksheet_day_sync','t'), (1110, 910), (1110, 340), anchor('site_worksheet_worker_cache','r',45)], CAL, '5', (1110, 910))
|
||||
arrow([anchor('site_worksheet_menu_sync','t'), (1160, 1120), (1160, 370), anchor('site_worksheet_worker_cache','r',90)], CAL, '6', (1160, 1120))
|
||||
arrow([anchor('site_worksheet_worker_cache','b'), (855, 520), anchor('site_worksheet_record','t')], ERP, '7', (855, 520))
|
||||
arrow([anchor('dallyproject','r',-30), (1235, anchor('dallyproject','r',-30)[1]), (1235, anchor('work_calendar_detail','l',-80)[1]), anchor('work_calendar_detail','l',-80)], PEOPLE, '8', (1235, 610))
|
||||
arrow([anchor('site_worksheet_record','r'), (1240, anchor('site_worksheet_record','r')[1]), (1240, anchor('work_calendar_detail','l',40)[1]), anchor('work_calendar_detail','l',40)], ERP, '9', (1240, 690))
|
||||
arrow([anchor('project_alias','r',35), (1200, anchor('project_alias','r',35)[1]), (1200, anchor('work_calendar_detail','l',95)[1]), anchor('work_calendar_detail','l',95)], PROJECT, '10', (1200, 1030))
|
||||
arrow([anchor('work_calendar_detail','b'), (1565, 880), anchor('work_calendar_day','t')], CAL, '11', (1565, 880))
|
||||
arrow([anchor('dallyproject','r',35), (1280, anchor('dallyproject','r',35)[1]), (1280, anchor('project_yearly_summary','l',-45)[1]), anchor('project_yearly_summary','l',-45)], PEOPLE, '12', (1280, 800))
|
||||
arrow([anchor('site_worksheet_record','r',55), (1265, anchor('site_worksheet_record','r',55)[1]), (1265, anchor('project_yearly_summary','l',30)[1]), anchor('project_yearly_summary','l',30)], ERP, '13', (1265, 1185))
|
||||
|
||||
# cards
|
||||
for name in entities:
|
||||
card(name)
|
||||
|
||||
# connector legend bottom
|
||||
legend_x, legend_y = 70, 1590
|
||||
d.rounded_rectangle((legend_x, legend_y, 1830, 1775), radius=10, fill='#1d2123', outline='#3a4247')
|
||||
d.text((legend_x + 20, legend_y + 16), 'CONNECTORS', font=font_b, fill=TEXT)
|
||||
col_w = 570
|
||||
for idx, (num, text, color) in enumerate(relations):
|
||||
col = idx // 5
|
||||
row = idx % 5
|
||||
x = legend_x + 28 + col * col_w
|
||||
y = legend_y + 55 + row * 34
|
||||
badge(num, x, y + 8, color)
|
||||
d.text((x + 28, y), text, font=font_s, fill=TEXT)
|
||||
|
||||
# mini flow labels
|
||||
d.rounded_rectangle((70, 1810, 1830, 1850), radius=8, fill='#1d2123', outline='#3a4247')
|
||||
d.text((90, 1820), 'People flow: member + dallyproject + matched ERP rows -> calendar | Project flow: dallyproject + matched ERP rows -> project summary', font=font_s, fill=MUTED)
|
||||
|
||||
img.save(PNG)
|
||||
print(PNG)
|
||||
BIN
exports/work_data_erd.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
7
exports/work_data_erd.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1900" height="1280" viewBox="0 0 1900 1280">
|
||||
<rect width="100%" height="100%" fill="#f6f8fb"/>
|
||||
<text x="70" y="55" font-family="DejaVu Sans, Arial" font-size="34" font-weight="700" fill="#0f172a">Work Data ERD: SQL + Site Worksheet Integration</text>
|
||||
<text x="72" y="82" font-family="DejaVu Sans, Arial" font-size="15" fill="#475569">PNG version generated at /home/hyein/jh-mh/장헌산업/exports/work_data_erd.png</text>
|
||||
<image href="work_data_erd.png" x="0" y="0" width="1900" height="1280" opacity="0"/>
|
||||
<text x="70" y="150" font-family="DejaVu Sans, Arial" font-size="22" fill="#334155">Open the PNG file for the full ERD diagram.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 706 B |
BIN
exports/work_data_erd_clean.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
exports/work_data_erd_columns.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
exports/work_data_erd_dark.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
exports/work_data_erd_people.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
exports/work_data_erd_project.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
exports/work_data_erd_unified_lanes.png
Normal file
|
After Width: | Height: | Size: 202 KiB |