1189 lines
35 KiB
HTML
1189 lines
35 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>노후관로관리 실제 화면 기반 샘플</title>
|
||
<style>
|
||
/*
|
||
실제 화면 기준 참조
|
||
- Layout: packages/common/components/src/ui/layout/nofooter.tsx
|
||
- Header: packages/common/components/src/ui/header/index.tsx
|
||
- LNB: packages/common/components/src/ui/lnb/index.tsx
|
||
- Gis/GisTree: packages/common/business_components/src/ui/map
|
||
- Detail: apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx
|
||
- VerticalTable: apps/sn/app/components/ui/verticalTable.tsx
|
||
*/
|
||
|
||
:root {
|
||
--primary: #258eea;
|
||
--primary-dark: #152346;
|
||
--download-green: #1fd078;
|
||
--label-cell: #dbeafe;
|
||
--header-height: 60px;
|
||
--lnb-width: 220px;
|
||
--tree-width: 250px;
|
||
--detail-width: 350px;
|
||
--border: #e1e7eb;
|
||
--table-border: #dbe2e6;
|
||
--panel-border: #d1d5db;
|
||
--panel-outline: #bdbdbd;
|
||
--apply-green: #31a83b;
|
||
--label: #555;
|
||
--muted: #777;
|
||
--map-bg: #eee9d9;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-width: 1440px;
|
||
overflow: hidden;
|
||
color: #111;
|
||
font-family: "Noto Sans KR", "Malgun Gothic", Arial, sans-serif;
|
||
font-size: 14px;
|
||
background: #fff;
|
||
}
|
||
|
||
button,
|
||
input,
|
||
select {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.app-shell {
|
||
height: 100vh;
|
||
display: grid;
|
||
grid-template-rows: var(--header-height) 1fr;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.site-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: var(--header-height);
|
||
border-bottom: 1px solid var(--border);
|
||
background: #fff;
|
||
padding: 0 16px;
|
||
z-index: 30;
|
||
}
|
||
|
||
.brand-area {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
min-width: 210px;
|
||
}
|
||
|
||
.sidebar-trigger {
|
||
width: 30px;
|
||
height: 30px;
|
||
display: grid;
|
||
place-items: center;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #111;
|
||
font-size: 22px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.logo-block {
|
||
display: grid;
|
||
justify-items: center;
|
||
line-height: 1;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.logo-text .blue {
|
||
color: #2392d0;
|
||
}
|
||
|
||
.logo-text .green {
|
||
color: #6ac04c;
|
||
}
|
||
|
||
.logo-caption {
|
||
margin-top: 5px;
|
||
color: #111;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 4px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.global-nav {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 18px;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.global-nav a {
|
||
position: relative;
|
||
color: #111;
|
||
text-decoration: none;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.04em;
|
||
}
|
||
|
||
.global-nav a::after {
|
||
content: "";
|
||
position: absolute;
|
||
right: -10px;
|
||
top: 50%;
|
||
width: 1px;
|
||
height: 16px;
|
||
background: var(--border);
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.global-nav a:last-child::after {
|
||
display: none;
|
||
}
|
||
|
||
.header-tools {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 120px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.search-circle {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
color: #fff;
|
||
background: #1598e8;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.user-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
color: #555;
|
||
font-size: 13px;
|
||
border: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.page-body {
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.lnb {
|
||
width: var(--lnb-width);
|
||
flex: 0 0 var(--lnb-width);
|
||
border-right: 1px solid var(--border);
|
||
background: #fff;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.lnb-item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 42px;
|
||
padding: 0 14px 0 26px;
|
||
color: #555;
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.lnb-item.active {
|
||
color: #258eea;
|
||
background: #f4f8fc;
|
||
}
|
||
|
||
.lnb-item .arrow {
|
||
color: #888;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.content {
|
||
position: relative;
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
background: #f7f7f7;
|
||
}
|
||
|
||
.gis-map {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(ellipse at 28% 28%, rgba(95, 138, 93, 0.2) 0 19%, transparent 20%),
|
||
radial-gradient(ellipse at 53% 32%, rgba(226, 218, 166, 0.35) 0 24%, transparent 25%),
|
||
linear-gradient(104deg, transparent 0 56%, rgba(234, 219, 161, 0.72) 56.1% 58%, transparent 58.1%),
|
||
linear-gradient(4deg, transparent 0 31%, rgba(234, 219, 161, 0.72) 31.1% 33%, transparent 33.1%),
|
||
var(--map-bg);
|
||
}
|
||
|
||
.gis-map::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0.45;
|
||
background-image:
|
||
repeating-radial-gradient(ellipse at 30% 45%, transparent 0 22px, rgba(158, 151, 111, 0.35) 23px 24px, transparent 25px 42px),
|
||
repeating-linear-gradient(87deg, transparent 0 110px, rgba(180, 180, 160, 0.35) 111px 112px);
|
||
}
|
||
|
||
.river,
|
||
.road,
|
||
.map-line,
|
||
.pipe-line,
|
||
.area-polygon {
|
||
position: absolute;
|
||
z-index: 1;
|
||
transform-origin: left center;
|
||
}
|
||
|
||
.river {
|
||
width: 34px;
|
||
height: 100%;
|
||
left: 55%;
|
||
top: 0;
|
||
transform: rotate(-16deg);
|
||
background: rgba(95, 170, 202, 0.28);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.road {
|
||
height: 25px;
|
||
border: 1px solid rgba(190, 176, 126, 0.65);
|
||
border-radius: 999px;
|
||
background: #fff8dc;
|
||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.78);
|
||
z-index: 2;
|
||
}
|
||
|
||
.map-line {
|
||
height: 2px;
|
||
border-radius: 999px;
|
||
z-index: 3;
|
||
}
|
||
|
||
.red-line {
|
||
background: #ff1b1b;
|
||
}
|
||
|
||
.purple-line {
|
||
background: #9b35ff;
|
||
}
|
||
|
||
.green-line {
|
||
background: #65d35f;
|
||
}
|
||
|
||
.pipe-line {
|
||
height: 4px;
|
||
border: 0;
|
||
padding: 0;
|
||
border-radius: 999px;
|
||
background: #496179;
|
||
z-index: 4;
|
||
}
|
||
|
||
.pipe-line.old {
|
||
height: 6px;
|
||
background: #e3222b;
|
||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.72), 0 0 0 7px rgba(227, 34, 43, 0.22);
|
||
}
|
||
|
||
.pipe-line.selected {
|
||
height: 8px;
|
||
box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.95), 0 0 0 10px rgba(37, 142, 234, 0.26);
|
||
}
|
||
|
||
.map-label {
|
||
position: absolute;
|
||
z-index: 5;
|
||
padding: 4px 7px;
|
||
color: #444;
|
||
background: rgba(255, 255, 255, 0.78);
|
||
border: 1px solid #ccd6df;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.area-polygon {
|
||
border: 2px solid #258eea;
|
||
background: rgba(37, 142, 234, 0.14);
|
||
clip-path: polygon(8% 9%, 90% 0, 100% 56%, 74% 100%, 12% 78%, 0 38%);
|
||
z-index: 3;
|
||
}
|
||
|
||
.area-polygon button {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #14466f;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.top-search {
|
||
position: absolute;
|
||
left: calc(var(--tree-width) + 280px);
|
||
top: 18px;
|
||
z-index: 20;
|
||
display: grid;
|
||
grid-template-columns: 150px 150px 180px 58px;
|
||
gap: 18px;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.top-search select,
|
||
.top-search button {
|
||
height: 30px;
|
||
border-radius: 3px;
|
||
border: 1px solid #d7dde3;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.top-search select {
|
||
padding: 0 10px;
|
||
color: #555;
|
||
background: #fff;
|
||
}
|
||
|
||
.top-search button {
|
||
border-color: #1a99ee;
|
||
color: #fff;
|
||
background: #1598e8;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.gis-tree {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
z-index: 40;
|
||
width: var(--tree-width);
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-right: 1px solid var(--panel-border);
|
||
background: #fff;
|
||
overflow: hidden;
|
||
padding: 8px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.tree-panel {
|
||
display: flex;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
border: 1px solid var(--panel-outline);
|
||
border-radius: 3px;
|
||
background: #fff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tree-panel.layer-panel {
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
.tree-panel.config-panel {
|
||
flex: 0 0 288px;
|
||
}
|
||
|
||
.tree-panel.collapsed {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.tree-panel.collapsed .tree-scroll,
|
||
.tree-panel.collapsed .setting-body {
|
||
display: none;
|
||
}
|
||
|
||
.panel-title {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 34px;
|
||
padding: 0 28px;
|
||
color: #4b5563;
|
||
border-bottom: 1px solid #eeeeee;
|
||
background: #fff;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.panel-title .collapse-toggle {
|
||
position: absolute;
|
||
left: 8px;
|
||
top: 50%;
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #555;
|
||
transform: translateY(-50%);
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.gis-tree .tree-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 6px 10px 14px;
|
||
}
|
||
|
||
.tree-row {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 26px;
|
||
color: #333;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tree-row.depth-1 {
|
||
padding-left: 18px;
|
||
}
|
||
|
||
.tree-row.depth-2 {
|
||
padding-left: 34px;
|
||
}
|
||
|
||
.tree-row input {
|
||
width: 14px;
|
||
height: 14px;
|
||
margin: 0 6px 0 0;
|
||
accent-color: #258eea;
|
||
}
|
||
|
||
.tree-row .twisty {
|
||
width: 14px;
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tree-row .icon {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
margin-right: 5px;
|
||
border: 1px solid #999;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.tree-row .list-icon {
|
||
margin-left: 5px;
|
||
width: 14px;
|
||
height: 14px;
|
||
border: 1px solid #999;
|
||
background:
|
||
linear-gradient(#777 0 0) 2px 3px / 10px 1px no-repeat,
|
||
linear-gradient(#777 0 0) 2px 6px / 10px 1px no-repeat,
|
||
linear-gradient(#777 0 0) 2px 9px / 10px 1px no-repeat,
|
||
#fff;
|
||
}
|
||
|
||
.tree-toggle {
|
||
position: absolute;
|
||
left: var(--tree-width);
|
||
top: 50%;
|
||
z-index: 41;
|
||
width: 24px;
|
||
height: 36px;
|
||
border: 1px solid #cfcfcf;
|
||
background: #fff;
|
||
color: #555;
|
||
transform: translateY(-50%);
|
||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.setting-body {
|
||
padding: 8px 12px 10px;
|
||
}
|
||
|
||
.setting-row {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 30px;
|
||
gap: 5px;
|
||
color: #333;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.setting-row label {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.setting-row input {
|
||
width: 40px;
|
||
height: 22px;
|
||
border: 1px solid #ccd6df;
|
||
padding: 0 4px;
|
||
color: #333;
|
||
background: #fff;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.apply-icon {
|
||
display: inline-grid;
|
||
place-items: center;
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
background: var(--apply-green);
|
||
color: #fff;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.field {
|
||
display: grid;
|
||
gap: 5px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.field span {
|
||
color: #555;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.field input,
|
||
.field select {
|
||
width: 100%;
|
||
height: 32px;
|
||
border: 1px solid #ccd6df;
|
||
border-radius: 2px;
|
||
padding: 0 9px;
|
||
color: #333;
|
||
background: #fff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.btn-row {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn {
|
||
height: 32px;
|
||
min-width: 58px;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 2px;
|
||
background: #fff;
|
||
color: #111;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.btn.primary {
|
||
border-color: var(--primary);
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn.download {
|
||
border-color: var(--download-green);
|
||
background: var(--download-green);
|
||
color: #fff;
|
||
}
|
||
|
||
.detail-popup {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
z-index: 50;
|
||
width: var(--detail-width);
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-left: 1px solid var(--panel-border);
|
||
background: #fff;
|
||
}
|
||
|
||
.detail-header {
|
||
position: relative;
|
||
min-height: 70px;
|
||
padding: 12px 38px 12px 14px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.detail-header h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
line-height: 1.45;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.detail-header .close {
|
||
position: absolute;
|
||
right: 11px;
|
||
top: 18px;
|
||
border: 0;
|
||
color: #fff;
|
||
background: transparent;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.detail-scroll {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
.card-header {
|
||
margin: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.card-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.vertical-card {
|
||
margin: 16px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
border: 1px solid var(--table-border);
|
||
background: #fff;
|
||
}
|
||
|
||
.vertical-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.vertical-table th,
|
||
.vertical-table td {
|
||
height: 41px;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 8px 9px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.vertical-table th {
|
||
width: 130px;
|
||
background: var(--label-cell);
|
||
color: #111827;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.vertical-table td {
|
||
color: #111;
|
||
background: #fff;
|
||
font-weight: 500;
|
||
word-break: keep-all;
|
||
}
|
||
|
||
.list-modal {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 100;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.34);
|
||
}
|
||
|
||
.list-modal.on {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-body {
|
||
width: 1040px;
|
||
max-height: 78vh;
|
||
display: grid;
|
||
grid-template-rows: auto auto 1fr auto;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border: 1px solid var(--table-border);
|
||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.modal-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.modal-title h2 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.modal-title button {
|
||
border: 0;
|
||
background: transparent;
|
||
color: #fff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.modal-search {
|
||
display: grid;
|
||
grid-template-columns: 120px 160px 160px 1fr;
|
||
gap: 10px;
|
||
align-items: end;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.ui-table-wrap {
|
||
overflow: auto;
|
||
padding: 12px;
|
||
}
|
||
|
||
.ui-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.ui-table th,
|
||
.ui-table td {
|
||
height: 38px;
|
||
border: 1px solid var(--table-border);
|
||
padding: 8px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.ui-table th {
|
||
background: #eaf3fb;
|
||
color: #647784;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.ui-table tbody tr:hover {
|
||
background: #edf4fb;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 10px 16px;
|
||
color: #666;
|
||
border-top: 1px solid var(--border);
|
||
font-size: 12px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
right: 24px;
|
||
bottom: 24px;
|
||
z-index: 120;
|
||
display: none;
|
||
max-width: 420px;
|
||
padding: 12px 14px;
|
||
color: #fff;
|
||
background: var(--primary-dark);
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.toast.on {
|
||
display: block;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<header class="site-header">
|
||
<div class="brand-area">
|
||
<button class="sidebar-trigger" type="button" title="메뉴">▯</button>
|
||
<div class="logo-block">
|
||
<div class="logo-text"><span class="blue">HASU</span><span class="green">ON</span></div>
|
||
<div class="logo-caption">디지털하수관로 시스템</div>
|
||
</div>
|
||
</div>
|
||
<nav class="global-nav" aria-label="상단 시스템 메뉴">
|
||
<a href="#">유역하수도포털</a>
|
||
<a href="#">통합플랫폼관리시스템</a>
|
||
<a href="#">디지털자기고 관리 시스템</a>
|
||
<a href="#">물재이용 대민사이트</a>
|
||
<a href="#">물재이용 대민</a>
|
||
<a href="#">하수처리시설 운영실태 분석 시스템</a>
|
||
<a href="#">디지털하수관로시스템</a>
|
||
<a href="#">하수도 통계 시스템</a>
|
||
</nav>
|
||
<div class="header-tools">
|
||
<button class="search-circle" type="button" title="검색">⌕</button>
|
||
<button class="user-chip" type="button">● 관리자⌄</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="page-body">
|
||
<aside class="lnb" aria-label="좌측 메뉴">
|
||
<a class="lnb-item active" href="#">하수관망도</a>
|
||
<a class="lnb-item" href="#">I/I 분석 <span class="arrow">›</span></a>
|
||
<a class="lnb-item" href="#">운영정보 <span class="arrow">›</span></a>
|
||
</aside>
|
||
|
||
<main class="content">
|
||
<section class="gis-map" aria-label="GIS 지도">
|
||
<div class="river"></div>
|
||
<div class="road" style="left:20%;top:31%;width:66%;transform:rotate(3deg)"></div>
|
||
<div class="road" style="left:38%;top:57%;width:68%;transform:rotate(-18deg)"></div>
|
||
<div class="road" style="left:62%;top:8%;width:54%;transform:rotate(94deg)"></div>
|
||
|
||
<span class="map-label" style="left:46%;top:17%">하수처리시설</span>
|
||
<span class="map-label" style="left:55%;top:49%">왕숙천</span>
|
||
<span class="map-label" style="left:26%;top:24%">진건로</span>
|
||
|
||
<div class="map-line red-line" style="left:58%;top:12%;width:18%;transform:rotate(104deg)"></div>
|
||
<div class="map-line red-line" style="left:70%;top:20%;width:21%;transform:rotate(-18deg)"></div>
|
||
<div class="map-line purple-line" style="left:57%;top:74%;width:30%;transform:rotate(-33deg)"></div>
|
||
<div class="map-line green-line" style="left:56%;top:26%;width:18%;transform:rotate(-64deg)"></div>
|
||
|
||
<button class="pipe-line old selected" data-pipe="PPL-001" style="left:25%;top:31%;width:30%;transform:rotate(-14deg)" title="41360-PPL-0001"></button>
|
||
<button class="pipe-line" data-pipe="PPL-002" style="left:34%;top:49%;width:31%;transform:rotate(23deg)" title="41360-PPL-0002"></button>
|
||
<button class="pipe-line" data-pipe="PPL-003" style="left:59%;top:50%;width:28%;transform:rotate(-31deg)" title="11680-PPL-0142"></button>
|
||
<button class="pipe-line old" data-pipe="PPL-004" style="left:49%;top:64%;width:43%;transform:rotate(12deg)" title="11710-PPL-0209"></button>
|
||
|
||
<div class="area-polygon hidden" style="left:48%;top:23%;width:18%;height:22%">
|
||
<button type="button" data-area="AREA-001">진건처리구역</button>
|
||
</div>
|
||
<div class="area-polygon hidden" style="left:68%;top:48%;width:22%;height:25%">
|
||
<button type="button" data-area="AREA-002">송파처리구역</button>
|
||
</div>
|
||
</section>
|
||
|
||
<form class="top-search">
|
||
<select aria-label="시도"><option>시도</option><option>경기도</option></select>
|
||
<select aria-label="시군구"><option>시군구</option><option>용인시</option><option>남양주시</option></select>
|
||
<select aria-label="시설"><option>하수처리시설</option><option>하수관로</option></select>
|
||
<button type="button">조회</button>
|
||
</form>
|
||
|
||
<aside class="gis-tree" aria-label="시설물 레이어 트리">
|
||
<section id="layerPanel" class="tree-panel layer-panel">
|
||
<div class="panel-title">
|
||
<button class="collapse-toggle" type="button" data-target="layerPanel">⌄</button>
|
||
<span>시설물</span>
|
||
</div>
|
||
<div id="manageTree" class="tree-scroll">
|
||
<div class="tree-row"><span class="twisty">›</span><input type="checkbox" /><span>시설</span></div>
|
||
<div class="tree-row"><span class="twisty">›</span><input type="checkbox" checked /><span>침수위험지도(중점관리지역)</span></div>
|
||
<div class="tree-row"><span class="twisty">›</span><input type="checkbox" /><span>도시침수지도(정보제공포털)</span></div>
|
||
<div class="tree-row"><span class="twisty"></span><input type="checkbox" /><span>지반침하정보</span><button id="groundListIcon" class="list-icon" type="button" title="목록"></button></div>
|
||
<div class="tree-row"><span class="twisty"></span><input id="oldPipeLayer" type="checkbox" checked /><span>노후관로</span><button id="openListByTree" class="list-icon" type="button" title="목록"></button></div>
|
||
</div>
|
||
|
||
<div id="analysisTree" class="tree-scroll hidden">
|
||
<div class="tree-row"><span class="twisty">⌄</span><input type="checkbox" checked /><span>하수관로</span></div>
|
||
<div class="tree-row depth-1"><span class="twisty"></span><input type="checkbox" checked /><span>합류관</span></div>
|
||
<div class="tree-row depth-1"><span class="twisty"></span><input type="checkbox" checked /><span>우수관</span></div>
|
||
<div class="tree-row depth-1"><span class="twisty"></span><input type="checkbox" checked /><span>오수관</span></div>
|
||
<div class="tree-row"><span class="twisty"></span><input type="checkbox" checked /><span>하수처리구역</span></div>
|
||
<div class="tree-row"><span class="twisty"></span><input type="checkbox" /><span>노후관로</span><button id="openListByAnalysisTree" class="list-icon" type="button" title="목록"></button></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="oldFilter" class="tree-panel config-panel">
|
||
<div class="panel-title">
|
||
<button class="collapse-toggle" type="button" data-target="oldFilter">⌄</button>
|
||
<span>지반침하정보</span>
|
||
</div>
|
||
<div class="setting-body">
|
||
<div class="setting-row">
|
||
<label for="ageInput">경과년도(년)</label>
|
||
<input id="ageInput" value="30" inputmode="numeric" />
|
||
<button id="applyAgeButton" class="apply-icon" type="button" title="적용">✓</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
<button class="tree-toggle" type="button">‹</button>
|
||
|
||
<aside class="detail-popup">
|
||
<header class="detail-header">
|
||
<h3 id="detailTitle">하수관로 정보 <br />관리번호 : 41360-PPL-0001</h3>
|
||
<button class="close" type="button">×</button>
|
||
</header>
|
||
<div class="detail-scroll">
|
||
<div class="card-header"><h3 id="detailSectionTitle">기본정보</h3></div>
|
||
<div class="vertical-card">
|
||
<table class="vertical-table"><tbody id="detailTable"></tbody></table>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="listModal" class="list-modal">
|
||
<section class="modal-body">
|
||
<header class="modal-title">
|
||
<h2>노후관로 현황</h2>
|
||
<button id="closeListModal" type="button">×</button>
|
||
</header>
|
||
<div class="modal-search">
|
||
<label class="field"><span>경과년도</span><input id="modalAge" value="20" /></label>
|
||
<label class="field"><span>시도</span><select id="modalSido"><option>전체</option><option>경기도</option><option>서울특별시</option></select></label>
|
||
<label class="field"><span>시군구</span><select id="modalSigungu"><option>전체</option><option>용인시</option><option>남양주시</option><option>송파구</option></select></label>
|
||
<div class="btn-row">
|
||
<button id="searchRows" class="btn primary" type="button">조회</button>
|
||
<button id="downloadRows" class="btn download" type="button">다운로드</button>
|
||
</div>
|
||
</div>
|
||
<div class="ui-table-wrap">
|
||
<table class="ui-table">
|
||
<thead><tr><th>NO</th><th>시설관리번호</th><th>설치일자</th><th>관경(mm)</th><th>연장(m)</th><th>용도</th><th>재질</th><th>경과년도</th></tr></thead>
|
||
<tbody id="gridBody"></tbody>
|
||
</table>
|
||
</div>
|
||
<footer class="modal-footer">
|
||
<span id="gridCount">조회 결과 0건</span>
|
||
<span>행 더블클릭 시 기존 `onFeatureSelect` 흐름으로 지도 이동 + 상세 표출</span>
|
||
</footer>
|
||
</section>
|
||
</div>
|
||
|
||
<div id="toast" class="toast"></div>
|
||
|
||
<script>
|
||
const currentYear = 2026;
|
||
let screen = "manage";
|
||
let threshold = 20;
|
||
|
||
const pipes = [
|
||
{ id: "PPL-001", fcltMngNo: "41360-PPL-0001", instMngNo: "41360PB005R-01", instlYmd: "1998-12-15", mapshtNo: "NJ52-9-12-034", pdmtVl: "450 mm", prlgVl: "132.5 m", usgCd: "합류관", mqltCd: "철근콘크리트관(RC)", avgGrdn: "2.1", drtwMltvArea: "2,140", sido: "경기도", sigungu: "남양주시", areaId: "AREA-001" },
|
||
{ id: "PPL-002", fcltMngNo: "41360-PPL-0002", instMngNo: "41360PB005R-02", instlYmd: "1987-06-03", mapshtNo: "NJ52-9-12-035", pdmtVl: "600 mm", prlgVl: "215.8 m", usgCd: "오수관", mqltCd: "흄관(HP)", avgGrdn: "1.8", drtwMltvArea: "3,560", sido: "경기도", sigungu: "남양주시", areaId: "AREA-001" },
|
||
{ id: "PPL-003", fcltMngNo: "11680-PPL-0142", instMngNo: "11680GN-0142", instlYmd: "2004-04-21", mapshtNo: "NJ52-5-07-110", pdmtVl: "300 mm", prlgVl: "88.4 m", usgCd: "우수관", mqltCd: "염화비닐관(PVC)", avgGrdn: "3.2", drtwMltvArea: "-", sido: "서울특별시", sigungu: "강남구", areaId: "AREA-002" },
|
||
{ id: "PPL-004", fcltMngNo: "11710-PPL-0209", instMngNo: "11710SP-0209", instlYmd: "1974-10-11", mapshtNo: "NJ52-5-07-114", pdmtVl: "800 mm", prlgVl: "310.2 m", usgCd: "합류관", mqltCd: "주철관(CIP)", avgGrdn: "1.2", drtwMltvArea: "5,210", sido: "서울특별시", sigungu: "송파구", areaId: "AREA-002" }
|
||
];
|
||
|
||
const areas = {
|
||
"AREA-001": { name: "진건처리구역", manageNo: "41360PB005R", sido: "경기도", sigungu: "남양주시", plant: "진건" },
|
||
"AREA-002": { name: "송파처리구역", manageNo: "11710SP001R", sido: "서울특별시", sigungu: "송파구", plant: "탄천" }
|
||
};
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const ageOf = (pipe) => currentYear - Number(pipe.instlYmd.slice(0, 4));
|
||
|
||
function row(label, value) {
|
||
return `<tr><th>${label}</th><td>${value}</td></tr>`;
|
||
}
|
||
|
||
function renderPipeDetail(pipeId) {
|
||
const pipe = pipes.find((item) => item.id === pipeId);
|
||
if (!pipe) return;
|
||
document.querySelectorAll(".pipe-line").forEach((line) => line.classList.toggle("selected", line.dataset.pipe === pipeId));
|
||
$("detailTitle").innerHTML = `하수관로 정보 <br />관리번호 : ${pipe.fcltMngNo}`;
|
||
$("detailSectionTitle").textContent = "기본정보";
|
||
$("detailTable").innerHTML = [
|
||
row("시설관리번호", pipe.fcltMngNo),
|
||
row("기관관리번호", pipe.instMngNo),
|
||
row("설치일자", pipe.instlYmd),
|
||
row("도엽번호", pipe.mapshtNo),
|
||
row("관경(mm)", pipe.pdmtVl),
|
||
row("연장(m)", pipe.prlgVl),
|
||
row("용도", pipe.usgCd),
|
||
row("재질", pipe.mqltCd),
|
||
row("평균구배(‰)", pipe.avgGrdn),
|
||
row("오수배수면적(㎡)", pipe.drtwMltvArea),
|
||
row("경과년도", `${ageOf(pipe)}년`)
|
||
].join("");
|
||
}
|
||
|
||
function renderAreaDetail(areaId) {
|
||
const area = areas[areaId];
|
||
const areaPipes = pipes.filter((pipe) => pipe.areaId === areaId);
|
||
const ages = areaPipes.map(ageOf);
|
||
const group = (use, mode) => {
|
||
const values = areaPipes.filter((pipe) => pipe.usgCd === use).map(ageOf);
|
||
if (!values.length) return "-";
|
||
if (mode === "avg") return (values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(1);
|
||
if (mode === "max") return Math.max(...values);
|
||
return Math.min(...values);
|
||
};
|
||
document.querySelectorAll(".area-polygon").forEach((item) => item.classList.toggle("selected", item.querySelector("button").dataset.area === areaId));
|
||
$("detailTitle").innerHTML = `하수처리구역 정보 <br />관리번호 : ${area.manageNo}`;
|
||
$("detailSectionTitle").textContent = "기본정보";
|
||
$("detailTable").innerHTML = [
|
||
row("하수처리구역명", area.name),
|
||
row("시도명", area.sido),
|
||
row("시군구명", area.sigungu),
|
||
row("하수처리시설명", area.plant),
|
||
row("평균 매설년도", `전체 ${((ages.reduce((sum, value) => sum + value, 0) / ages.length)).toFixed(1)} / 합류 ${group("합류관", "avg")} / 우수 ${group("우수관", "avg")} / 오수 ${group("오수관", "avg")}`),
|
||
row("최장 매설년도", `전체 ${Math.max(...ages)} / 합류 ${group("합류관", "max")} / 우수 ${group("우수관", "max")} / 오수 ${group("오수관", "max")}`),
|
||
row("최신 매설년도", `전체 ${Math.min(...ages)} / 합류 ${group("합류관", "min")} / 우수 ${group("우수관", "min")} / 오수 ${group("오수관", "min")}`),
|
||
row("보수이력", "<button class='btn primary' type='button'>조회</button>")
|
||
].join("");
|
||
}
|
||
|
||
function rowsByFilter() {
|
||
const age = Number($("modalAge").value);
|
||
const sido = $("modalSido").value;
|
||
const sigungu = $("modalSigungu").value;
|
||
return pipes
|
||
.filter((pipe) => ageOf(pipe) >= age)
|
||
.filter((pipe) => sido === "전체" || pipe.sido === sido)
|
||
.filter((pipe) => sigungu === "전체" || pipe.sigungu === sigungu)
|
||
.sort((a, b) => ageOf(b) - ageOf(a));
|
||
}
|
||
|
||
function renderGrid() {
|
||
const age = Number($("modalAge").value);
|
||
if (!Number.isInteger(age) || age < 1 || age > 100) {
|
||
toast("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||
return;
|
||
}
|
||
const rows = rowsByFilter();
|
||
$("gridCount").textContent = `조회 결과 ${rows.length.toLocaleString("ko-KR")}건`;
|
||
$("gridBody").innerHTML = rows.map((pipe, index) => `
|
||
<tr data-pipe="${pipe.id}">
|
||
<td>${index + 1}</td>
|
||
<td>${pipe.fcltMngNo}</td>
|
||
<td>${pipe.instlYmd}</td>
|
||
<td>${pipe.pdmtVl.replace(" mm", "")}</td>
|
||
<td>${pipe.prlgVl.replace(" m", "")}</td>
|
||
<td>${pipe.usgCd}</td>
|
||
<td>${pipe.mqltCd}</td>
|
||
<td>${ageOf(pipe)}</td>
|
||
</tr>
|
||
`).join("");
|
||
document.querySelectorAll("#gridBody tr").forEach((tr) => {
|
||
tr.addEventListener("dblclick", () => {
|
||
renderPipeDetail(tr.dataset.pipe);
|
||
$("listModal").classList.remove("on");
|
||
toast("선택 관로로 지도 이동 후 상세정보를 표시했습니다.");
|
||
});
|
||
});
|
||
}
|
||
|
||
function openList() {
|
||
$("modalAge").value = String(threshold);
|
||
$("modalSido").value = "전체";
|
||
$("modalSigungu").value = "전체";
|
||
renderGrid();
|
||
$("listModal").classList.add("on");
|
||
}
|
||
|
||
function toast(message) {
|
||
const target = $("toast");
|
||
target.textContent = message;
|
||
target.classList.add("on");
|
||
clearTimeout(toast.timer);
|
||
toast.timer = setTimeout(() => target.classList.remove("on"), 2400);
|
||
}
|
||
|
||
function setScreen(next) {
|
||
screen = next;
|
||
const isAnalysis = screen === "analysis";
|
||
$("manageTree").classList.toggle("hidden", isAnalysis);
|
||
$("analysisTree").classList.toggle("hidden", !isAnalysis);
|
||
$("oldFilter").classList.toggle("hidden", isAnalysis);
|
||
document.querySelectorAll(".area-polygon").forEach((item) => item.classList.toggle("hidden", !isAnalysis));
|
||
if (isAnalysis) renderAreaDetail("AREA-001");
|
||
else renderPipeDetail("PPL-001");
|
||
}
|
||
|
||
document.querySelectorAll(".pipe-line").forEach((line) => line.addEventListener("click", () => renderPipeDetail(line.dataset.pipe)));
|
||
document.querySelectorAll(".area-polygon button").forEach((button) => button.addEventListener("click", () => renderAreaDetail(button.dataset.area)));
|
||
$("applyAgeButton").addEventListener("click", () => {
|
||
const age = Number($("ageInput").value);
|
||
if (!Number.isInteger(age) || age < 1 || age > 100) {
|
||
toast("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||
return;
|
||
}
|
||
threshold = age;
|
||
document.querySelectorAll(".pipe-line").forEach((line) => {
|
||
const pipe = pipes.find((item) => item.id === line.dataset.pipe);
|
||
line.classList.toggle("old", ageOf(pipe) >= threshold);
|
||
});
|
||
toast(`기준 ${threshold}년 이상 노후관로를 지도에 반영했습니다.`);
|
||
});
|
||
$("openListByTree").addEventListener("click", openList);
|
||
$("openListByAnalysisTree").addEventListener("click", openList);
|
||
$("groundListIcon").addEventListener("click", () => toast("지반침하정보 목록 아이콘 클릭 샘플입니다."));
|
||
$("closeListModal").addEventListener("click", () => $("listModal").classList.remove("on"));
|
||
$("searchRows").addEventListener("click", renderGrid);
|
||
$("downloadRows").addEventListener("click", () => toast("실제 구현 시 downloadExcelWithKoreanHeaders를 호출합니다."));
|
||
$("listModal").addEventListener("click", (event) => {
|
||
if (event.target === $("listModal")) $("listModal").classList.remove("on");
|
||
});
|
||
|
||
// 샘플 전환용: 실제 앱에서는 라우팅 메뉴로 분리된다.
|
||
window.showAgedPipeManage = () => setScreen("manage");
|
||
window.showAgedPipeAnalysis = () => setScreen("analysis");
|
||
|
||
document.querySelectorAll(".collapse-toggle").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
const panel = $(button.dataset.target);
|
||
const collapsed = panel.classList.toggle("collapsed");
|
||
button.textContent = collapsed ? "›" : "⌄";
|
||
});
|
||
});
|
||
|
||
setScreen("manage");
|
||
</script>
|
||
</body>
|
||
</html>
|