feat: 배경지도 전환 및 테마 시스템 구현

- 배경지도 타입 전환 (일반/전자해도/야간)
- 테마 연동 색상 시스템 (선박 라벨, 속도벡터 등)
- mapStore에 subscribeWithSelector 적용
- 신호원 우선순위/항적 조회기간 상수 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HeungTak Lee 2026-02-05 06:36:09 +09:00
부모 a5131306c4
커밋 c068f55077
7개의 변경된 파일294개의 추가작업 그리고 55개의 파일을 삭제

파일 보기

@ -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; // 기본 조회기간 (일)