feat: 배경지도 전환 및 테마 시스템 구현
- 배경지도 타입 전환 (일반/전자해도/야간) - 테마 연동 색상 시스템 (선박 라벨, 속도벡터 등) - mapStore에 subscribeWithSelector 적용 - 신호원 우선순위/항적 조회기간 상수 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
부모
a5131306c4
커밋
c068f55077
@ -303,7 +303,7 @@ select{
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
background:var(--secondary2) url('/images/ico_select.svg')no-repeat right 1.2rem center / 1.2rem;
|
background:var(--secondary2) url('../images/ico_select.svg')no-repeat right 1.2rem center / 1.2rem;
|
||||||
border: 1px solid var(--secondary4);
|
border: 1px solid var(--secondary4);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
@ -315,25 +315,25 @@ select{
|
|||||||
text-overflow: ellipsis;white-space: nowrap; max-width: 100%; overflow: hidden;
|
text-overflow: ellipsis;white-space: nowrap; max-width: 100%; overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, input:disabled{
|
input:not([type="range"]), input:not([type="range"]):disabled{
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-text-fill-color: inherit;
|
-webkit-text-fill-color: inherit;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
input{
|
input:not([type="range"]){
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
font-family: 'NanumSquare', sans-serif;
|
font-family: 'NanumSquare', sans-serif;
|
||||||
background-color: var(--secondary2);
|
background-color: var(--secondary2);
|
||||||
border: 1px solid var(--secondary4);
|
border: 1px solid var(--secondary4);
|
||||||
font-size: var(--fs-m);
|
font-size: var(--fs-m);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-weight: var(--fw-regular);
|
font-weight: var(--fw-regular);
|
||||||
padding: 0 1.2rem;
|
padding: 0 1.2rem;
|
||||||
}
|
}
|
||||||
input::placeholder { color:rgba(var(--white-rgb),.3); }
|
input::placeholder { color:rgba(var(--white-rgb),.3); }
|
||||||
/* Chrome, Safari, Opera, Microsoft Edge */
|
/* Chrome, Safari, Opera, Microsoft Edge */
|
||||||
@ -403,8 +403,8 @@ input:disabled {pointer-events: none;}
|
|||||||
|
|
||||||
/* checkBox */
|
/* checkBox */
|
||||||
.checkbox {display:flex;align-items:center;position:relative;cursor:pointer;}
|
.checkbox {display:flex;align-items:center;position:relative;cursor:pointer;}
|
||||||
.checkbox input + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);width:3.2rem;height:3.2rem;background:url('/images/ico_check_off.svg') no-repeat center/contain;}
|
.checkbox input + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);width:3.2rem;height:3.2rem;background:url('../images/ico_check_off.svg') no-repeat center/contain;}
|
||||||
.checkbox input:checked + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);background:url('/images/ico_check_on2.svg') no-repeat center/contain;}
|
.checkbox input:checked + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);background:url('../images/ico_check_on2.svg') no-repeat center/contain;}
|
||||||
.checkbox:focus-within input:focus-visible + span::after {outline: 2px solid var(--white);outline-offset: 2px;border-radius: .4rem;}
|
.checkbox:focus-within input:focus-visible + span::after {outline: 2px solid var(--white);outline-offset: 2px;border-radius: .4rem;}
|
||||||
|
|
||||||
.checkL {gap: .6rem;}
|
.checkL {gap: .6rem;}
|
||||||
@ -430,8 +430,8 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
/* radio - 라디오버튼 오른쪽 */
|
/* radio - 라디오버튼 오른쪽 */
|
||||||
.radio {display:flex;align-items:center;position:relative;cursor:pointer;}
|
.radio {display:flex;align-items:center;position:relative;cursor:pointer;}
|
||||||
.radio input + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);width:2rem;height:2rem;background:url('/images/ico_radio_off.svg') no-repeat center/contain;}
|
.radio input + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);width:2rem;height:2rem;background:url('../images/ico_radio_off.svg') no-repeat center/contain;}
|
||||||
.radio input:checked + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);background:url('/images/ico_radio_on.svg') no-repeat center/contain;}
|
.radio input:checked + span::after {content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);background:url('../images/ico_radio_on.svg') no-repeat center/contain;}
|
||||||
.radio:focus-within input:focus-visible + span::after {outline: 2px solid var(--white);outline-offset: 2px;border-radius: .4rem;}
|
.radio:focus-within input:focus-visible + span::after {outline: 2px solid var(--white);outline-offset: 2px;border-radius: .4rem;}
|
||||||
/* radio - 라디오버튼 왼쪽 */
|
/* radio - 라디오버튼 왼쪽 */
|
||||||
.radioL {flex-direction: row-reverse;}
|
.radioL {flex-direction: row-reverse;}
|
||||||
@ -452,8 +452,8 @@ input[type="radio"] {
|
|||||||
.numInput input {padding-right: 2rem;}
|
.numInput input {padding-right: 2rem;}
|
||||||
.numInput .spin {position: absolute; top: 50%; right: .5rem;transform: translateY(-50%); display: flex; flex-direction: column;}
|
.numInput .spin {position: absolute; top: 50%; right: .5rem;transform: translateY(-50%); display: flex; flex-direction: column;}
|
||||||
.numInput .spin button{width: 1.7rem; height: 1.1rem; cursor: pointer;}
|
.numInput .spin button{width: 1.7rem; height: 1.1rem; cursor: pointer;}
|
||||||
.numInput .spin .spinUp {background: url(/images/ico_spinup.svg)no-repeat center / contain;}
|
.numInput .spin .spinUp {background: url(../images/ico_spinup.svg)no-repeat center / contain;}
|
||||||
.numInput .spin .spinDown {background: url(/images/ico_spindown.svg)no-repeat center / contain;}
|
.numInput .spin .spinDown {background: url(../images/ico_spindown.svg)no-repeat center / contain;}
|
||||||
|
|
||||||
/* switch */
|
/* switch */
|
||||||
.switch {
|
.switch {
|
||||||
|
|||||||
@ -16,10 +16,10 @@
|
|||||||
.deep {background-color: var(--secondary1);}
|
.deep {background-color: var(--secondary1);}
|
||||||
|
|
||||||
.btnToggle {display: inline-flex;gap: .6rem;}
|
.btnToggle {display: inline-flex;gap: .6rem;}
|
||||||
.btnToggle::after {content: '';width: 1.8rem;height: 1.8rem;background: url('/images/ico_arrow_down_s.svg') no-repeat center / contain;flex-shrink: 0;transition: transform .2s ease;}
|
.btnToggle::after {content: '';width: 1.8rem;height: 1.8rem;background: url('../images/ico_arrow_down_s.svg') no-repeat center / contain;flex-shrink: 0;transition: transform .2s ease;}
|
||||||
.btnToggle.is-open::after {transform: rotate(180deg);}
|
.btnToggle.is-open::after {transform: rotate(180deg);}
|
||||||
.schBtn {width: 8rem; height: 3.5rem;display:flex;align-items:center; justify-content:center;font-size: var(--fs-ms);font-weight: var(--fw-bold); background-color: var(--primary1); color: var(--white);}
|
.schBtn {width: 8rem; height: 3.5rem;display:flex;align-items:center; justify-content:center;font-size: var(--fs-ms);font-weight: var(--fw-bold); background-color: var(--primary1); color: var(--white);}
|
||||||
.schBtn::before {content: "";display: inline-block; width: 1.4rem; height: 1.4rem; margin-right: 1rem;background:url('/images/ico_btnsch.svg') no-repeat center center/ 1.4rem; }
|
.schBtn::before {content: "";display: inline-block; width: 1.4rem; height: 1.4rem; margin-right: 1rem;background:url('../images/ico_btnsch.svg') no-repeat center center/ 1.4rem; }
|
||||||
|
|
||||||
|
|
||||||
.btnMap {width: 2.8rem; height: 2.8rem; background: url(../images/ico_btn_map.svg) no-repeat center / contain;}
|
.btnMap {width: 2.8rem; height: 2.8rem; background: url(../images/ico_btn_map.svg) no-repeat center / contain;}
|
||||||
@ -70,7 +70,7 @@
|
|||||||
/* UI-input */
|
/* UI-input */
|
||||||
.schInput{height: 3.5rem; font-family: 'NanumSquare', sans-serif;font-size: var(--fs-m); color: var(--white);background-color: var(--tertiary1);padding: 0 1.2rem; border: 0;}
|
.schInput{height: 3.5rem; font-family: 'NanumSquare', sans-serif;font-size: var(--fs-m); color: var(--white);background-color: var(--tertiary1);padding: 0 1.2rem; border: 0;}
|
||||||
.schInput::placeholder { color:rgba(var(--white-rgb),.3); }
|
.schInput::placeholder { color:rgba(var(--white-rgb),.3); }
|
||||||
.dateInput {background:url(/images/ico_input_cal.svg) no-repeat center right .5rem/2.4rem; padding-right: 3rem; cursor: pointer;}
|
.dateInput {background:url(../images/ico_input_cal.svg) no-repeat center right .5rem/2.4rem; padding-right: 3rem; cursor: pointer;}
|
||||||
.dateInput::placeholder { color:var(--white); }
|
.dateInput::placeholder { color:var(--white); }
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -122,7 +122,7 @@
|
|||||||
.acdList.input > li.input label input {flex: 1;}
|
.acdList.input > li.input label input {flex: 1;}
|
||||||
.acdList.check > li.check {border-color: rgba(var(--secondary4-rgb),.3); }
|
.acdList.check > li.check {border-color: rgba(var(--secondary4-rgb),.3); }
|
||||||
|
|
||||||
.toggleListBtn {display: flex;align-items: center;width: 2.4rem;height: 2.4rem;background: url('/images/ico_arrow_toggle_down.svg') no-repeat center / contain;transition: transform 0.3s ease;}
|
.toggleListBtn {display: flex;align-items: center;width: 2.4rem;height: 2.4rem;background: url('../images/ico_arrow_toggle_down.svg') no-repeat center / contain;transition: transform 0.3s ease;}
|
||||||
.toggleListBtn.open {transform: rotate(180deg);}
|
.toggleListBtn.open {transform: rotate(180deg);}
|
||||||
|
|
||||||
/* 사이드패널 - 상세정보 박스 */
|
/* 사이드패널 - 상세정보 박스 */
|
||||||
@ -136,9 +136,9 @@
|
|||||||
.detailBox > li.dbHeader .name {font-size: var(--fs-ml); font-weight:var(--fw-bold);}
|
.detailBox > li.dbHeader .name {font-size: var(--fs-ml); font-weight:var(--fw-bold);}
|
||||||
.detailBox > li.dbHeader .type {font-size: var(--fs-ml); font-weight:var(--fw-bold);color:var(--primary1);}
|
.detailBox > li.dbHeader .type {font-size: var(--fs-ml); font-weight:var(--fw-bold);color:var(--primary1);}
|
||||||
.detailBox > li.dbHeader .num {font-size: var(--fs-m); font-weight:var(--fw-bold);}
|
.detailBox > li.dbHeader .num {font-size: var(--fs-m); font-weight:var(--fw-bold);}
|
||||||
.detailBox > li.dbHeader .icoArrow {width:2rem;height:2rem;background:url('/images/ico_arrow_right_rnd_off.svg') no-repeat center/contain; margin-left: .5rem;}
|
.detailBox > li.dbHeader .icoArrow {width:2rem;height:2rem;background:url('../images/ico_arrow_right_rnd_off.svg') no-repeat center/contain; margin-left: .5rem;}
|
||||||
.detailBox:has(.icoArrow:hover) {border: solid 2px var(--primary1);}
|
.detailBox:has(.icoArrow:hover) {border: solid 2px var(--primary1);}
|
||||||
.detailBox > li.dbHeader .icoArrow:hover {background:url('/images/ico_arrow_right_rnd_on.svg') no-repeat center/contain;}
|
.detailBox > li.dbHeader .icoArrow:hover {background:url('../images/ico_arrow_right_rnd_on.svg') no-repeat center/contain;}
|
||||||
.detailBox > li > span {display: flex; color: var(--white); min-height: 2.1rem;}
|
.detailBox > li > span {display: flex; color: var(--white); min-height: 2.1rem;}
|
||||||
.detailBox > li > .tit { font-size: var(--fs-ml);font-weight: var(--fw-bold);}
|
.detailBox > li > .tit { font-size: var(--fs-ml);font-weight: var(--fw-bold);}
|
||||||
.detailBox > li > .label{min-width:9rem;flex-shrink: 0; font-weight:var(--fw-regular);}
|
.detailBox > li > .label{min-width:9rem;flex-shrink: 0; font-weight:var(--fw-regular);}
|
||||||
@ -178,12 +178,12 @@
|
|||||||
========================= */
|
========================= */
|
||||||
.toastContainer {position: fixed; bottom: 1.5rem; right: 5rem; display: flex; flex-direction: column; gap: .7rem;}
|
.toastContainer {position: fixed; bottom: 1.5rem; right: 5rem; display: flex; flex-direction: column; gap: .7rem;}
|
||||||
.toast {display: flex; justify-content: space-between; width: 56rem; height: 5rem; padding: 1rem 1rem 1rem 4.5rem;border-radius: .4rem;}
|
.toast {display: flex; justify-content: space-between; width: 56rem; height: 5rem; padding: 1rem 1rem 1rem 4.5rem;border-radius: .4rem;}
|
||||||
.toastWarining {background: #8B6600 url(/images/ico_toast_warning.svg)no-repeat top 1rem left 1.4rem /2.4rem;border: 1px solid #FFBC02;}
|
.toastWarining {background: #8B6600 url(../images/ico_toast_warning.svg)no-repeat top 1rem left 1.4rem /2.4rem;border: 1px solid #FFBC02;}
|
||||||
.toastCaution {background: #750000 url(/images/ico_toast_caution.svg)no-repeat top 1rem left 1.4rem /2.4rem;border: 1px solid #FF0000;}
|
.toastCaution {background: #750000 url(../images/ico_toast_caution.svg)no-repeat top 1rem left 1.4rem /2.4rem;border: 1px solid #FF0000;}
|
||||||
.toast .toastMsg {font-size: var(--fs-ml);color: var(--white); font-weight: var(--fw-bold); }
|
.toast .toastMsg {font-size: var(--fs-ml);color: var(--white); font-weight: var(--fw-bold); }
|
||||||
.toast .toastR {display: flex; align-items: center;gap: 1rem;}
|
.toast .toastR {display: flex; align-items: center;gap: 1rem;}
|
||||||
.toast .toastR .toastAction {font-size: var(--fs-s);color: var(--white); font-weight: var(--fw-regular);text-decoration: underline;}
|
.toast .toastR .toastAction {font-size: var(--fs-s);color: var(--white); font-weight: var(--fw-regular);text-decoration: underline;}
|
||||||
.toast .toastR .toastClose {width: 2.4rem;height: 2.4rem; background: url(/images/ico_toast_close.svg)no-repeat center /contain;}
|
.toast .toastR .toastClose {width: 2.4rem;height: 2.4rem; background: url(../images/ico_toast_close.svg)no-repeat center /contain;}
|
||||||
/* */
|
/* */
|
||||||
.cicle {width: 1.6rem; height: 1.6rem; border-radius: 50%; }
|
.cicle {width: 1.6rem; height: 1.6rem; border-radius: 50%; }
|
||||||
.default {background-color: var(--primary1);}
|
.default {background-color: var(--primary1);}
|
||||||
@ -196,19 +196,19 @@
|
|||||||
.popupMap {display: flex;flex-direction: column;position: absolute;top: 40%; left: 70%;transform: translate(-50%, -50%); width: auto;height: auto; background-color: var(--gray-scale3); z-index: 80;}
|
.popupMap {display: flex;flex-direction: column;position: absolute;top: 40%; left: 70%;transform: translate(-50%, -50%); width: auto;height: auto; background-color: var(--gray-scale3); z-index: 80;}
|
||||||
.popupMap > .pmHeader {display: flex;justify-content: space-between; align-items: center; height: 4rem; padding: .5rem 1rem;}
|
.popupMap > .pmHeader {display: flex;justify-content: space-between; align-items: center; height: 4rem; padding: .5rem 1rem;}
|
||||||
.popupMap > .pmHeader > .rowL{ display: flex;align-items: center; gap: .5rem;}
|
.popupMap > .pmHeader > .rowL{ display: flex;align-items: center; gap: .5rem;}
|
||||||
.popupMap > .pmHeader > .rowL > .shipType {width: 2.4rem; height: 2.4rem; background:var(--gray-scale2) url(/images/ico_ship_type_fising.svg)no-repeat center / 2.4rem;}
|
.popupMap > .pmHeader > .rowL > .shipType {width: 2.4rem; height: 2.4rem; background:var(--gray-scale2) url(../images/ico_ship_type_fising.svg)no-repeat center / 2.4rem;}
|
||||||
.popupMap > .pmBody {display: flex; flex-direction: column; padding: .8rem 1.2rem .8rem 1.2rem}
|
.popupMap > .pmBody {display: flex; flex-direction: column; padding: .8rem 1.2rem .8rem 1.2rem}
|
||||||
.popupMap > .pmFooter {display: flex; flex-direction: column; align-items: flex-start; justify-content: center; height: 3.2rem; border-top: 1px solid var(--gray-scale5); padding: 0 1rem;font-size: var(--fs-xs); color: var(--gray-scaleB);}
|
.popupMap > .pmFooter {display: flex; flex-direction: column; align-items: flex-start; justify-content: center; height: 3.2rem; border-top: 1px solid var(--gray-scale5); padding: 0 1rem;font-size: var(--fs-xs); color: var(--gray-scaleB);}
|
||||||
/* 선박정보팝업 */
|
/* 선박정보팝업 */
|
||||||
.popupMap.shipInfo { width: 29rem;}
|
.popupMap.shipInfo { width: 29rem;}
|
||||||
.popupMap.shipInfo > .pmHeader > .rowL > .shipName {font-size: var(--fs-ml); font-weight:var(--fw-bold);color: var(--gray-scaleD);}
|
.popupMap.shipInfo > .pmHeader > .rowL > .shipName {font-size: var(--fs-ml); font-weight:var(--fw-bold);color: var(--gray-scaleD);}
|
||||||
.popupMap.shipInfo > .pmHeader > .rowL > .shipNum {font-size: var(--fs-m); font-weight:var(--fw-regular);color: var(--gray-scaleB);}
|
.popupMap.shipInfo > .pmHeader > .rowL > .shipNum {font-size: var(--fs-m); font-weight:var(--fw-regular);color: var(--gray-scaleB);}
|
||||||
.popupMap.shipInfo > .pmHeader > .pmClose {width: 2.4rem;height: 2.4rem; background: url(/images/ico_ship_info_close.svg)no-repeat center /contain;}
|
.popupMap.shipInfo > .pmHeader > .pmClose {width: 2.4rem;height: 2.4rem; background: url(../images/ico_ship_info_close.svg)no-repeat center /contain;}
|
||||||
|
|
||||||
/* 기상관측팝업 */
|
/* 기상관측팝업 */
|
||||||
.popupMap.osbInfo { width: 22rem; left: 45%;}
|
.popupMap.osbInfo { width: 22rem; left: 45%;}
|
||||||
.popupMap.osbInfo > .pmHeader > .rowL > .title {font-size: var(--fs-l); font-weight:var(--fw-bold);}
|
.popupMap.osbInfo > .pmHeader > .rowL > .title {font-size: var(--fs-l); font-weight:var(--fw-bold);}
|
||||||
.popupMap.osbInfo > .pmHeader > .pmClose {width: 2.4rem;height: 2.4rem; background: url(/images/ico_ship_info_close.svg)no-repeat center /contain;}
|
.popupMap.osbInfo > .pmHeader > .pmClose {width: 2.4rem;height: 2.4rem; background: url(../images/ico_ship_info_close.svg)no-repeat center /contain;}
|
||||||
/* popup-기상관측상태 */
|
/* popup-기상관측상태 */
|
||||||
.osbStatus {display: flex; flex-direction: column; gap: .6rem;}
|
.osbStatus {display: flex; flex-direction: column; gap: .6rem;}
|
||||||
.osbStatus li.date {border-radius: .5rem; background-color: #61666F; padding: 1rem; font-weight: var(--fw-bold);}
|
.osbStatus li.date {border-radius: .5rem; background-color: #61666F; padding: 1rem; font-weight: var(--fw-bold);}
|
||||||
@ -221,14 +221,14 @@
|
|||||||
.pmGallery .galleryView{ width: 100%; height: 100%;}
|
.pmGallery .galleryView{ width: 100%; height: 100%;}
|
||||||
.pmGallery .galleryView img{ width: 100%; height: 100%;object-fit: cover;}
|
.pmGallery .galleryView img{ width: 100%; height: 100%;object-fit: cover;}
|
||||||
.pmGallery .navBtn { position: absolute; width: 3.4rem; height: 3.4rem; top: 0; bottom: 0; margin: auto 0; display: flex; align-items: center; justify-content: center;}
|
.pmGallery .navBtn { position: absolute; width: 3.4rem; height: 3.4rem; top: 0; bottom: 0; margin: auto 0; display: flex; align-items: center; justify-content: center;}
|
||||||
.pmGallery .prev {background: url(/images/ico_arrow_left_big.svg) no-repeat center / 3.4rem; left: 0;}
|
.pmGallery .prev {background: url(../images/ico_arrow_left_big.svg) no-repeat center / 3.4rem; left: 0;}
|
||||||
.pmGallery .next {background: url(/images/ico_arrow_right_big.svg) no-repeat center / 3.4rem; right: 0;}
|
.pmGallery .next {background: url(../images/ico_arrow_right_big.svg) no-repeat center / 3.4rem; right: 0;}
|
||||||
|
|
||||||
/* popup-Action */
|
/* popup-Action */
|
||||||
.shipAction {display: flex; align-items: center; justify-content: space-between;}
|
.shipAction {display: flex; align-items: center; justify-content: space-between;}
|
||||||
.shipAction .detailBtn {position: relative; display: inline-flex; align-items: center; justify-content: center; width: 8rem; height: 2.8rem; background-color: var(--gray-scale6); color: var(--white); font-size: var(--fs-xs); font-weight: var(--fw-bold); padding-right: 2rem;}
|
.shipAction .detailBtn {position: relative; display: inline-flex; align-items: center; justify-content: center; width: 8rem; height: 2.8rem; background-color: var(--gray-scale6); color: var(--white); font-size: var(--fs-xs); font-weight: var(--fw-bold); padding-right: 2rem;}
|
||||||
.shipAction .detailBtn::after {content: "";position: absolute; width: 2rem; height: 2rem; top: 50%; right: 0.5rem;transform: translateY(-50%);background: url("/images/ico_cate.svg") no-repeat center / contain;}
|
.shipAction .detailBtn::after {content: "";position: absolute; width: 2rem; height: 2rem; top: 50%; right: 0.5rem;transform: translateY(-50%);background: url("../images/ico_cate.svg") no-repeat center / contain;}
|
||||||
.shipAction .favBtn {display: flex;align-items: center; justify-content: center; width: 2.8rem; height: 2.8rem; background: url(/images/ico_star.svg)no-repeat center / 2rem; background-color: var(--gray-scale5);}
|
.shipAction .favBtn {display: flex;align-items: center; justify-content: center; width: 2.8rem; height: 2.8rem; background: url(../images/ico_star.svg)no-repeat center / 2rem; background-color: var(--gray-scale5);}
|
||||||
.shipAction .shipTypeIco {display: flex; align-items: center; gap: .2rem; padding: 0 1rem;}
|
.shipAction .shipTypeIco {display: flex; align-items: center; gap: .2rem; padding: 0 1rem;}
|
||||||
.shipAction .shipTypeIco li {display: flex; justify-content: center; align-items: center; width: 1.7rem; height: 1.7rem;border-radius: .2rem; background-color: var(--gray-scale5);color: var(--white); font-weight: var(--fw-heavy);font-size: var(--fs-xs);}
|
.shipAction .shipTypeIco li {display: flex; justify-content: center; align-items: center; width: 1.7rem; height: 1.7rem;border-radius: .2rem; background-color: var(--gray-scale5);color: var(--white); font-weight: var(--fw-heavy);font-size: var(--fs-xs);}
|
||||||
|
|
||||||
@ -257,8 +257,8 @@
|
|||||||
.shipStatus li.schedule {justify-content:space-between; background-color: var(--gray-scale2); padding: .5rem 1rem; border-top: 1px solid var(--gray-scale5);border-bottom: 1px solid var(--gray-scale5);}
|
.shipStatus li.schedule {justify-content:space-between; background-color: var(--gray-scale2); padding: .5rem 1rem; border-top: 1px solid var(--gray-scale5);border-bottom: 1px solid var(--gray-scale5);}
|
||||||
.shipStatus .depart,
|
.shipStatus .depart,
|
||||||
.shipStatus .arrive {font-size: var(--fs-xs); color: var(--gray-scaleD);font-weight:var(--fw-bold);padding-left: 1.7rem;}
|
.shipStatus .arrive {font-size: var(--fs-xs); color: var(--gray-scaleD);font-weight:var(--fw-bold);padding-left: 1.7rem;}
|
||||||
.shipStatus .depart {background: url(/images/ico_clock1.svg)no-repeat center left / 1.2rem;}
|
.shipStatus .depart {background: url(../images/ico_clock1.svg)no-repeat center left / 1.2rem;}
|
||||||
.shipStatus .arrive {background: url(/images/ico_clock2.svg)no-repeat center left / 1.2rem;}
|
.shipStatus .arrive {background: url(../images/ico_clock2.svg)no-repeat center left / 1.2rem;}
|
||||||
.shipStatus .scheduleDate {font-size: var(--fs-xs); color: var(--gray-scaleB);}
|
.shipStatus .scheduleDate {font-size: var(--fs-xs); color: var(--gray-scaleB);}
|
||||||
.shipStatus li.status { display: grid;grid-template-columns: 1fr 13rem 1fr;align-items: stretch;border-bottom: 1px solid var(--gray-scale5);}
|
.shipStatus li.status { display: grid;grid-template-columns: 1fr 13rem 1fr;align-items: stretch;border-bottom: 1px solid var(--gray-scale5);}
|
||||||
.shipStatus li.status .statusItem { display: flex; flex-direction: column; align-items: flex-start; position: relative; padding:.5rem 1rem;}
|
.shipStatus li.status .statusItem { display: flex; flex-direction: column; align-items: flex-start; position: relative; padding:.5rem 1rem;}
|
||||||
@ -296,7 +296,7 @@
|
|||||||
.popupUtill {display: flex; flex-direction: column; width: 52.5rem; height:auto;max-height: 80vh;overflow: hidden; background-color: var(--secondary2); border: .1rem solid var(--secondary3); padding:2.5rem 3rem;}
|
.popupUtill {display: flex; flex-direction: column; width: 52.5rem; height:auto;max-height: 80vh;overflow: hidden; background-color: var(--secondary2); border: .1rem solid var(--secondary3); padding:2.5rem 3rem;}
|
||||||
.popupUtill > .puHeader {display: flex; justify-content: space-between; align-items: center; padding-bottom: 2rem;}
|
.popupUtill > .puHeader {display: flex; justify-content: space-between; align-items: center; padding-bottom: 2rem;}
|
||||||
.popupUtill > .puHeader > .title {font-weight: var(--fw-bold); font-size: var(--fs-xl);}
|
.popupUtill > .puHeader > .title {font-weight: var(--fw-bold); font-size: var(--fs-xl);}
|
||||||
.popupUtill > .puHeader > .puClose {width: 2.2rem;height: 2.2rem; background: url(/images/ico_pop_utill_close.svg)no-repeat center /contain;}
|
.popupUtill > .puHeader > .puClose {width: 2.2rem;height: 2.2rem; background: url(../images/ico_pop_utill_close.svg)no-repeat center /contain;}
|
||||||
.popupUtill > .puBody {flex: 1; min-height: 0; overflow-y: auto; padding-bottom: 1rem;}
|
.popupUtill > .puBody {flex: 1; min-height: 0; overflow-y: auto; padding-bottom: 1rem;}
|
||||||
.popupUtill > .puFooter { margin-top: auto; padding-top: 1rem;}
|
.popupUtill > .puFooter { margin-top: auto; padding-top: 1rem;}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { createShipLayers, clearClusterCache } from '../map/layers/shipLayer';
|
|||||||
import useShipStore from '../stores/shipStore';
|
import useShipStore from '../stores/shipStore';
|
||||||
import { useTrackQueryStore } from '../tracking/stores/trackQueryStore';
|
import { useTrackQueryStore } from '../tracking/stores/trackQueryStore';
|
||||||
import useTrackingModeStore from '../stores/trackingModeStore';
|
import useTrackingModeStore from '../stores/trackingModeStore';
|
||||||
|
import { useMapStore } from '../stores/mapStore';
|
||||||
import { getTrackQueryLayers } from '../tracking/utils/trackQueryLayerUtils';
|
import { getTrackQueryLayers } from '../tracking/utils/trackQueryLayerUtils';
|
||||||
import { getReplayLayers } from '../replay/utils/replayLayerRegistry';
|
import { getReplayLayers } from '../replay/utils/replayLayerRegistry';
|
||||||
import { shipBatchRenderer } from '../map/ShipBatchRenderer';
|
import { shipBatchRenderer } from '../map/ShipBatchRenderer';
|
||||||
@ -58,6 +59,7 @@ export default function useShipLayer(map) {
|
|||||||
controller: false,
|
controller: false,
|
||||||
layers: [],
|
layers: [],
|
||||||
useDevicePixels: true,
|
useDevicePixels: true,
|
||||||
|
pickingRadius: 12,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('[Deck.gl] Error:', error);
|
console.error('[Deck.gl] Error:', error);
|
||||||
},
|
},
|
||||||
@ -292,6 +294,22 @@ export default function useShipLayer(map) {
|
|||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [map]);
|
}, [map]);
|
||||||
|
|
||||||
|
// === mapStore 테마(배경지도) 변경 시 선박 레이어 리렌더 ===
|
||||||
|
// 테마 변경 시 선박명/속도벡터/선박크기 색상이 변경되므로 즉시 렌더링
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = useMapStore.subscribe(
|
||||||
|
(state) => state.baseMapType,
|
||||||
|
() => {
|
||||||
|
if (deckRef.current && map) {
|
||||||
|
clearClusterCache();
|
||||||
|
shipBatchRenderer.immediateRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deckCanvas: canvasRef.current,
|
deckCanvas: canvasRef.current,
|
||||||
deckRef,
|
deckRef,
|
||||||
|
|||||||
@ -10,27 +10,78 @@ import WebGLTileLayer from 'ol/layer/WebGLTile';
|
|||||||
const EPSG_3857 = 'EPSG:3857';
|
const EPSG_3857 = 'EPSG:3857';
|
||||||
const EPSG_4326 = 'EPSG:4326';
|
const EPSG_4326 = 'EPSG:4326';
|
||||||
|
|
||||||
|
// 1x1 투명 PNG (타일 로드 실패 시 대체용)
|
||||||
|
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 타일 로드 함수 (fetch 사용으로 콘솔 에러 완전 방지)
|
||||||
|
* - 404/네트워크 에러 시 투명 이미지로 대체
|
||||||
|
* - 브라우저 네트워크 탭에는 표시되지만 콘솔 에러는 없음
|
||||||
|
*/
|
||||||
|
function silentTileLoadFunction(imageTile, src) {
|
||||||
|
const img = imageTile.getImage();
|
||||||
|
|
||||||
|
fetch(src)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Tile not found');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
img.src = TRANSPARENT_PIXEL;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 레이어 설정
|
* 레이어 설정
|
||||||
*/
|
*/
|
||||||
export const mapLayerConfig = {
|
export const mapLayerConfig = {
|
||||||
// 세계 지도 (줌 0-11)
|
// 일반지도 (줌 0-11)
|
||||||
worldLayer: {
|
worldLayer: {
|
||||||
source: new XYZ({
|
source: new XYZ({
|
||||||
url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp',
|
url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp',
|
||||||
minZoom: 0,
|
minZoom: 0,
|
||||||
maxZoom: 11,
|
maxZoom: 11,
|
||||||
attributions: 'ⓒ OpenStreetMap',
|
attributions: 'ⓒ OpenStreetMap',
|
||||||
|
tileLoadFunction: silentTileLoadFunction,
|
||||||
}),
|
}),
|
||||||
preload: Infinity,
|
preload: Infinity,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 동아시아 상세 (줌 12-15)
|
// 전자해도 (줌 0-11) - URL은 임시로 worldLayer와 동일
|
||||||
|
encLayer: {
|
||||||
|
source: new XYZ({
|
||||||
|
url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp',
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 11,
|
||||||
|
attributions: 'ⓒ OpenStreetMap',
|
||||||
|
tileLoadFunction: silentTileLoadFunction,
|
||||||
|
}),
|
||||||
|
preload: Infinity,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 야간지도 (줌 5-15, 타일은 6-11레벨까지만 로드 → 12+ 확대 표시)
|
||||||
|
darkLayer: {
|
||||||
|
source: new XYZ({
|
||||||
|
url: '/MAPS/SIMPLE_B_webp/{z}/{x}/{y}.webp',
|
||||||
|
minZoom: 6,
|
||||||
|
maxZoom: 11, // 타일은 11레벨까지만 로드 (12+ 는 11레벨 타일 확대)
|
||||||
|
tileLoadFunction: silentTileLoadFunction,
|
||||||
|
}),
|
||||||
|
preload: Infinity,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 동아시아 상세 (줌 12-15, 타일은 12레벨까지만 로드 → 13+ 확대 표시)
|
||||||
eastAsiaLayer: {
|
eastAsiaLayer: {
|
||||||
source: new XYZ({
|
source: new XYZ({
|
||||||
url: '/MAPS/EAST_ASIA_webp/{z}/{x}/{y}.webp',
|
url: '/MAPS/EAST_ASIA_webp/{z}/{x}/{y}.webp',
|
||||||
minZoom: 12,
|
minZoom: 12,
|
||||||
maxZoom: 15,
|
maxZoom: 12, // 타일은 12레벨까지만 로드 (13+ 는 12레벨 타일 확대)
|
||||||
|
tileLoadFunction: silentTileLoadFunction,
|
||||||
}),
|
}),
|
||||||
preload: 0,
|
preload: 0,
|
||||||
minZoom: 12,
|
minZoom: 12,
|
||||||
@ -44,6 +95,7 @@ export const mapLayerConfig = {
|
|||||||
url: '/MAPS/KOR_webp/{z}/{x}/{y}.webp',
|
url: '/MAPS/KOR_webp/{z}/{x}/{y}.webp',
|
||||||
minZoom: 16,
|
minZoom: 16,
|
||||||
maxZoom: 17,
|
maxZoom: 17,
|
||||||
|
tileLoadFunction: silentTileLoadFunction,
|
||||||
}),
|
}),
|
||||||
preload: Infinity,
|
preload: Infinity,
|
||||||
minZoom: 16,
|
minZoom: 16,
|
||||||
@ -54,14 +106,38 @@ export const mapLayerConfig = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 베이스맵 레이어 생성
|
* 베이스맵 레이어 생성
|
||||||
|
* @param {string} baseMapType - 배경지도 타입 ('normal' | 'enc' | 'dark')
|
||||||
*/
|
*/
|
||||||
export const createBaseLayers = () => {
|
export const createBaseLayers = (baseMapType = 'dark') => {
|
||||||
const worldMap = new WebGLTileLayer(mapLayerConfig.worldLayer);
|
// 3가지 배경지도 레이어 생성
|
||||||
const eastAsiaMap = new WebGLTileLayer(mapLayerConfig.eastAsiaLayer);
|
const worldMap = new WebGLTileLayer({
|
||||||
const korMap = new WebGLTileLayer(mapLayerConfig.korLayer);
|
...mapLayerConfig.worldLayer,
|
||||||
|
visible: baseMapType === 'normal',
|
||||||
|
});
|
||||||
|
const encMap = new WebGLTileLayer({
|
||||||
|
...mapLayerConfig.encLayer,
|
||||||
|
visible: baseMapType === 'enc',
|
||||||
|
});
|
||||||
|
const darkMap = new WebGLTileLayer({
|
||||||
|
...mapLayerConfig.darkLayer,
|
||||||
|
visible: baseMapType === 'dark',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 상세 지도 레이어 (일반지도/전자해도 전용, 야간지도에서는 숨김)
|
||||||
|
const isDarkMode = baseMapType === 'dark';
|
||||||
|
const eastAsiaMap = new WebGLTileLayer({
|
||||||
|
...mapLayerConfig.eastAsiaLayer,
|
||||||
|
visible: !isDarkMode,
|
||||||
|
});
|
||||||
|
const korMap = new WebGLTileLayer({
|
||||||
|
...mapLayerConfig.korLayer,
|
||||||
|
visible: !isDarkMode,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
worldMap,
|
worldMap,
|
||||||
|
encMap,
|
||||||
|
darkMap,
|
||||||
eastAsiaMap,
|
eastAsiaMap,
|
||||||
korMap,
|
korMap,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,10 +15,21 @@ import {
|
|||||||
} from '../../types/constants';
|
} from '../../types/constants';
|
||||||
import useShipStore from '../../stores/shipStore';
|
import useShipStore from '../../stores/shipStore';
|
||||||
import useTrackingModeStore from '../../stores/trackingModeStore';
|
import useTrackingModeStore from '../../stores/trackingModeStore';
|
||||||
|
import { useMapStore, THEME_COLORS, THEME_TYPES } from '../../stores/mapStore';
|
||||||
|
|
||||||
// 아이콘 아틀라스 이미지
|
// 아이콘 아틀라스 이미지
|
||||||
import atlasImg from '../../assets/img/icon/atlas.png';
|
import atlasImg from '../../assets/img/icon/atlas.png';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 테마 색상 가져오기
|
||||||
|
* @returns {Object} 테마 색상 객체
|
||||||
|
*/
|
||||||
|
function getCurrentThemeColors() {
|
||||||
|
const { getTheme } = useMapStore.getState();
|
||||||
|
const theme = getTheme();
|
||||||
|
return THEME_COLORS[theme] || THEME_COLORS[THEME_TYPES.LIGHT];
|
||||||
|
}
|
||||||
|
|
||||||
// 추적 선박 아이콘 (인라인 SVG data URL)
|
// 추적 선박 아이콘 (인라인 SVG data URL)
|
||||||
const TRACKED_SHIP_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
const TRACKED_SHIP_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
<circle cx="16" cy="16" r="14" stroke="#00D4FF" stroke-width="2" fill="none" stroke-dasharray="4 2"/>
|
<circle cx="16" cy="16" r="14" stroke="#00D4FF" stroke-width="2" fill="none" stroke-dasharray="4 2"/>
|
||||||
@ -56,6 +67,9 @@ const signalClusterCache = {
|
|||||||
lastRenderTrigger: 0,
|
lastRenderTrigger: 0,
|
||||||
clusteredData: [],
|
clusteredData: [],
|
||||||
positionHash: '',
|
positionHash: '',
|
||||||
|
// 밀도 제한된 ships 배열 변경 감지용
|
||||||
|
lastShipsLength: 0,
|
||||||
|
lastShipsHash: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 클러스터 갱신 주기 (N회 렌더링마다 재계산)
|
// 클러스터 갱신 주기 (N회 렌더링마다 재계산)
|
||||||
@ -208,6 +222,8 @@ export function clearClusterCache() {
|
|||||||
signalClusterCache.lastRenderTrigger = 0;
|
signalClusterCache.lastRenderTrigger = 0;
|
||||||
signalClusterCache.positionHash = '';
|
signalClusterCache.positionHash = '';
|
||||||
signalClusterCache.clusteredData = [];
|
signalClusterCache.clusteredData = [];
|
||||||
|
signalClusterCache.lastShipsLength = 0;
|
||||||
|
signalClusterCache.lastShipsHash = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -231,6 +247,24 @@ function canGenerateSignalSVG(ship, isIntegrate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ships 배열의 featureId 해시 생성 (배열 변경 감지용)
|
||||||
|
* @param {Array} ships - 선박 배열
|
||||||
|
* @returns {string} featureId 기반 해시
|
||||||
|
*/
|
||||||
|
function computeShipsHash(ships) {
|
||||||
|
if (ships.length === 0) return '';
|
||||||
|
// 처음, 1/4, 중간, 3/4, 마지막 선박의 featureId를 샘플링
|
||||||
|
const indices = [
|
||||||
|
0,
|
||||||
|
Math.floor(ships.length / 4),
|
||||||
|
Math.floor(ships.length / 2),
|
||||||
|
Math.floor(ships.length * 3 / 4),
|
||||||
|
ships.length - 1
|
||||||
|
];
|
||||||
|
return indices.map(i => ships[Math.min(i, ships.length - 1)]?.featureId || '').join('|');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 신호상태용 클러스터링 결과 가져오기
|
* 신호상태용 클러스터링 결과 가져오기
|
||||||
* 실제로 SVG가 생성 가능한 선박만 대상으로 클러스터링
|
* 실제로 SVG가 생성 가능한 선박만 대상으로 클러스터링
|
||||||
@ -244,22 +278,27 @@ function canGenerateSignalSVG(ship, isIntegrate) {
|
|||||||
function getSignalClusteredShips(ships, zoom, isIntegrate, renderTrigger = 0) {
|
function getSignalClusteredShips(ships, zoom, isIntegrate, renderTrigger = 0) {
|
||||||
const zoomInt = Math.floor(zoom);
|
const zoomInt = Math.floor(zoom);
|
||||||
|
|
||||||
|
// ships 배열 변경 감지용 해시 (밀도 제한 결과가 달라지면 캐시 무효화)
|
||||||
|
const shipsHash = computeShipsHash(ships);
|
||||||
|
const shipsLengthMatch = signalClusterCache.lastShipsLength === ships.length;
|
||||||
|
const shipsHashMatch = signalClusterCache.lastShipsHash === shipsHash;
|
||||||
|
|
||||||
// 실제로 SVG 생성 가능한 선박만 필터링
|
// 실제로 SVG 생성 가능한 선박만 필터링
|
||||||
const shipsWithSignal = ships.filter(ship => canGenerateSignalSVG(ship, isIntegrate));
|
const shipsWithSignal = ships.filter(ship => canGenerateSignalSVG(ship, isIntegrate));
|
||||||
const positionHash = computePositionHash(shipsWithSignal);
|
const positionHash = computePositionHash(shipsWithSignal);
|
||||||
|
|
||||||
// 캐시 유효 조건
|
// 캐시 유효 조건 (ships 배열 변경도 감지)
|
||||||
const zoomMatch = signalClusterCache.lastZoom === zoomInt;
|
const zoomMatch = signalClusterCache.lastZoom === zoomInt;
|
||||||
const integrateMatch = signalClusterCache.lastIsIntegrate === isIntegrate;
|
const integrateMatch = signalClusterCache.lastIsIntegrate === isIntegrate;
|
||||||
const sizeMatch = Math.abs(signalClusterCache.lastDataLength - shipsWithSignal.length) < shipsWithSignal.length * 0.1;
|
const sizeMatch = Math.abs(signalClusterCache.lastDataLength - shipsWithSignal.length) < Math.max(1, shipsWithSignal.length * 0.1);
|
||||||
const positionMatch = signalClusterCache.positionHash === positionHash;
|
const positionMatch = signalClusterCache.positionHash === positionHash;
|
||||||
|
|
||||||
// 렌더 트리거 주기 체크
|
// 렌더 트리거 주기 체크
|
||||||
const triggerDiff = renderTrigger - signalClusterCache.lastRenderTrigger;
|
const triggerDiff = renderTrigger - signalClusterCache.lastRenderTrigger;
|
||||||
const needsPeriodicRefresh = triggerDiff >= CLUSTER_REFRESH_INTERVAL;
|
const needsPeriodicRefresh = triggerDiff >= CLUSTER_REFRESH_INTERVAL;
|
||||||
|
|
||||||
// 캐시 히트
|
// 캐시 히트: 모든 조건 충족 시에만 (ships 배열 변경도 체크)
|
||||||
if (zoomMatch && integrateMatch && sizeMatch && positionMatch && !needsPeriodicRefresh) {
|
if (zoomMatch && integrateMatch && sizeMatch && positionMatch && shipsLengthMatch && shipsHashMatch && !needsPeriodicRefresh) {
|
||||||
return signalClusterCache.clusteredData;
|
return signalClusterCache.clusteredData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +312,8 @@ function getSignalClusteredShips(ships, zoom, isIntegrate, renderTrigger = 0) {
|
|||||||
signalClusterCache.lastRenderTrigger = renderTrigger;
|
signalClusterCache.lastRenderTrigger = renderTrigger;
|
||||||
signalClusterCache.positionHash = positionHash;
|
signalClusterCache.positionHash = positionHash;
|
||||||
signalClusterCache.clusteredData = clustered;
|
signalClusterCache.clusteredData = clustered;
|
||||||
|
signalClusterCache.lastShipsLength = ships.length;
|
||||||
|
signalClusterCache.lastShipsHash = shipsHash;
|
||||||
|
|
||||||
return clustered;
|
return clustered;
|
||||||
}
|
}
|
||||||
@ -400,6 +441,10 @@ export function createShipIconLayer(ships, zoom = 10, darkSignalIds = null) {
|
|||||||
* @returns {Array} 라인 데이터 [{ sourcePosition, targetPosition, color }]
|
* @returns {Array} 라인 데이터 [{ sourcePosition, targetPosition, color }]
|
||||||
*/
|
*/
|
||||||
function buildSpeedVectorData(ships) {
|
function buildSpeedVectorData(ships) {
|
||||||
|
// 테마 기반 색상
|
||||||
|
const themeColors = getCurrentThemeColors();
|
||||||
|
const vectorColor = themeColors.speedVector;
|
||||||
|
|
||||||
return ships
|
return ships
|
||||||
.filter((ship) => Number(ship.sog) > SPEED_THRESHOLD) // 항해중 선박만 (아이콘 기준과 동일)
|
.filter((ship) => Number(ship.sog) > SPEED_THRESHOLD) // 항해중 선박만 (아이콘 기준과 동일)
|
||||||
.map((ship) => {
|
.map((ship) => {
|
||||||
@ -427,7 +472,7 @@ function buildSpeedVectorData(ships) {
|
|||||||
return {
|
return {
|
||||||
sourcePosition: toLonLat([projX, projY]),
|
sourcePosition: toLonLat([projX, projY]),
|
||||||
targetPosition: toLonLat([projX + xAdd, projY + yAdd]),
|
targetPosition: toLonLat([projX + xAdd, projY + yAdd]),
|
||||||
color: [0, 0, 0, 200], // 검정색
|
color: vectorColor,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -641,6 +686,9 @@ export function createShipDimLayer(ships, zoom) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 테마 기반 색상
|
||||||
|
const themeColors = getCurrentThemeColors();
|
||||||
|
|
||||||
return new PathLayer({
|
return new PathLayer({
|
||||||
id: 'ship-dim-layer',
|
id: 'ship-dim-layer',
|
||||||
data: dimData,
|
data: dimData,
|
||||||
@ -649,7 +697,7 @@ export function createShipDimLayer(ships, zoom) {
|
|||||||
widthMinPixels: 1,
|
widthMinPixels: 1,
|
||||||
widthMaxPixels: 3,
|
widthMaxPixels: 3,
|
||||||
getPath: (d) => d.path,
|
getPath: (d) => d.path,
|
||||||
getColor: [0, 100, 200, 180], // 파란색 반투명
|
getColor: themeColors.shipDim,
|
||||||
getWidth: 2,
|
getWidth: 2,
|
||||||
jointRounded: true,
|
jointRounded: true,
|
||||||
capRounded: true,
|
capRounded: true,
|
||||||
@ -966,6 +1014,9 @@ export function createShipLabelLayer(ships, zoom, labelOptions = { showShipName:
|
|||||||
|
|
||||||
const fontSize = zoom < 11 ? 10 : zoom < 13 ? 12 : 14;
|
const fontSize = zoom < 11 ? 10 : zoom < 13 ? 12 : 14;
|
||||||
|
|
||||||
|
// 테마 기반 색상
|
||||||
|
const themeColors = getCurrentThemeColors();
|
||||||
|
|
||||||
return new TextLayer({
|
return new TextLayer({
|
||||||
id: 'ship-label-layer',
|
id: 'ship-label-layer',
|
||||||
data: labelData,
|
data: labelData,
|
||||||
@ -973,7 +1024,7 @@ export function createShipLabelLayer(ships, zoom, labelOptions = { showShipName:
|
|||||||
getPosition: (d) => [d.longitude, d.latitude],
|
getPosition: (d) => [d.longitude, d.latitude],
|
||||||
getText: (d) => d.labelText,
|
getText: (d) => d.labelText,
|
||||||
getSize: fontSize,
|
getSize: fontSize,
|
||||||
getColor: [30, 30, 30, 255],
|
getColor: themeColors.shipLabel,
|
||||||
getAngle: 0,
|
getAngle: 0,
|
||||||
getTextAnchor: 'start',
|
getTextAnchor: 'start',
|
||||||
getAlignmentBaseline: 'center',
|
getAlignmentBaseline: 'center',
|
||||||
@ -982,7 +1033,7 @@ export function createShipLabelLayer(ships, zoom, labelOptions = { showShipName:
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
characterSet: 'auto',
|
characterSet: 'auto',
|
||||||
outlineWidth: 2,
|
outlineWidth: 2,
|
||||||
outlineColor: [255, 255, 255, 255],
|
outlineColor: themeColors.shipLabelOutline,
|
||||||
billboard: true,
|
billboard: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,74 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { subscribeWithSelector } from 'zustand/middleware';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배경지도 타입
|
||||||
|
* - normal: 일반지도
|
||||||
|
* - enc: 전자해도
|
||||||
|
* - dark: 야간지도
|
||||||
|
*/
|
||||||
|
export const BASE_MAP_TYPES = {
|
||||||
|
NORMAL: 'normal',
|
||||||
|
ENC: 'enc',
|
||||||
|
DARK: 'dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테마 타입 (배경지도에 연동)
|
||||||
|
* - light: 일반지도, 전자해도
|
||||||
|
* - dark: 야간지도
|
||||||
|
*/
|
||||||
|
export const THEME_TYPES = {
|
||||||
|
LIGHT: 'light',
|
||||||
|
DARK: 'dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배경지도 -> 테마 매핑
|
||||||
|
*/
|
||||||
|
const BASE_MAP_TO_THEME = {
|
||||||
|
[BASE_MAP_TYPES.NORMAL]: THEME_TYPES.LIGHT,
|
||||||
|
[BASE_MAP_TYPES.ENC]: THEME_TYPES.LIGHT,
|
||||||
|
[BASE_MAP_TYPES.DARK]: THEME_TYPES.DARK,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테마별 색상 정의
|
||||||
|
* - 선박 레이어에서 사용
|
||||||
|
*/
|
||||||
|
export const THEME_COLORS = {
|
||||||
|
[THEME_TYPES.LIGHT]: {
|
||||||
|
shipLabel: [30, 30, 30, 255],
|
||||||
|
shipLabelOutline: [255, 255, 255, 255],
|
||||||
|
speedVector: [0, 0, 0, 200],
|
||||||
|
shipDim: [0, 100, 200, 180],
|
||||||
|
},
|
||||||
|
[THEME_TYPES.DARK]: {
|
||||||
|
shipLabel: [255, 255, 255, 255],
|
||||||
|
shipLabelOutline: [30, 30, 30, 255],
|
||||||
|
speedVector: [255, 255, 255, 200],
|
||||||
|
shipDim: [100, 200, 255, 180],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지도 상태 관리 스토어
|
* 지도 상태 관리 스토어
|
||||||
*/
|
*/
|
||||||
export const useMapStore = create((set, get) => ({
|
export const useMapStore = create(subscribeWithSelector((set, get) => ({
|
||||||
// 지도 인스턴스
|
// 지도 인스턴스
|
||||||
map: null,
|
map: null,
|
||||||
setMap: (map) => set({ map }),
|
setMap: (map) => set({ map }),
|
||||||
|
|
||||||
|
// 배경지도 타입 (기본: 야간지도)
|
||||||
|
baseMapType: BASE_MAP_TYPES.DARK,
|
||||||
|
setBaseMapType: (type) => set({ baseMapType: type }),
|
||||||
|
|
||||||
|
// 현재 테마 (배경지도에 연동)
|
||||||
|
getTheme: () => BASE_MAP_TO_THEME[get().baseMapType] || THEME_TYPES.LIGHT,
|
||||||
|
|
||||||
|
// 현재 테마 색상 가져오기
|
||||||
|
getThemeColors: () => THEME_COLORS[get().getTheme()],
|
||||||
|
|
||||||
// 줌 레벨
|
// 줌 레벨
|
||||||
zoom: 7,
|
zoom: 7,
|
||||||
setZoom: (zoom) => set({ zoom }),
|
setZoom: (zoom) => set({ zoom }),
|
||||||
@ -35,8 +96,8 @@ export const useMapStore = create((set, get) => ({
|
|||||||
setCenter: (center) => set({ center }),
|
setCenter: (center) => set({ center }),
|
||||||
|
|
||||||
// 측정 도구
|
// 측정 도구
|
||||||
activeMeasureTool: null, // 'distance' | 'area' | 'rangeRing' | null
|
activeMeasureTool: null,
|
||||||
areaShape: null, // 'Polygon' | 'Box' | 'Circle' | null
|
areaShape: null,
|
||||||
|
|
||||||
setMeasureTool: (tool) => set((state) => ({
|
setMeasureTool: (tool) => set((state) => ({
|
||||||
activeMeasureTool: state.activeMeasureTool === tool ? null : tool,
|
activeMeasureTool: state.activeMeasureTool === tool ? null : tool,
|
||||||
@ -58,4 +119,4 @@ export const useMapStore = create((set, get) => ({
|
|||||||
[layerName]: !state.layerVisibility[layerName],
|
[layerName]: !state.layerVisibility[layerName],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}));
|
})));
|
||||||
|
|||||||
@ -14,6 +14,32 @@ export const SIGNAL_SOURCE_CODE_VTS_AIS = '000004';
|
|||||||
export const SIGNAL_SOURCE_CODE_RADAR = '000005';
|
export const SIGNAL_SOURCE_CODE_RADAR = '000005';
|
||||||
export const SIGNAL_SOURCE_CODE_D_MF_HF = '000016';
|
export const SIGNAL_SOURCE_CODE_D_MF_HF = '000016';
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// 신호원 우선순위 (동적 대표 선정용, 숫자가 작을수록 높은 우선순위)
|
||||||
|
// 참조: mda-react-front/docs/dynamic-priority.md §1
|
||||||
|
// =====================
|
||||||
|
export const SOURCE_PRIORITY_RANK = {
|
||||||
|
'000005': 0, // VTS-Radar
|
||||||
|
'000004': 1, // VTS-AIS
|
||||||
|
'000001': 2, // AIS
|
||||||
|
'000003': 3, // V-Pass
|
||||||
|
'000002': 4, // E-Nav
|
||||||
|
'000016': 5, // D-MF/HF
|
||||||
|
};
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// 신호원 코드 → ship 객체의 is_active 프로퍼티 키 매핑
|
||||||
|
// 참조: mda-react-front/docs/dynamic-priority.md §3
|
||||||
|
// =====================
|
||||||
|
export const SOURCE_TO_ACTIVE_KEY = {
|
||||||
|
'000001': 'ais',
|
||||||
|
'000003': 'vpass',
|
||||||
|
'000002': 'enav',
|
||||||
|
'000004': 'vtsAis',
|
||||||
|
'000016': 'dMfHf',
|
||||||
|
'000005': 'vtsRadar',
|
||||||
|
};
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// 선박 종류 코드 (Ship Kind Code)
|
// 선박 종류 코드 (Ship Kind Code)
|
||||||
// =====================
|
// =====================
|
||||||
@ -298,3 +324,10 @@ export const SIGNAL_SOURCE_LIST = [
|
|||||||
{ code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D MF/HF' },
|
{ code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D MF/HF' },
|
||||||
{ code: SIGNAL_SOURCE_CODE_RADAR, label: 'RADAR' },
|
{ code: SIGNAL_SOURCE_CODE_RADAR, label: 'RADAR' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// 항적/리플레이 조회기간 설정
|
||||||
|
// (향후 로그인 세션별 계정 권한에 따라 커스텀 가능)
|
||||||
|
// =====================
|
||||||
|
export const TRACK_QUERY_MAX_DAYS = 7; // 최대 조회기간 (일)
|
||||||
|
export const TRACK_QUERY_DEFAULT_DAYS = 3; // 기본 조회기간 (일)
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user