Compare commits
6 Commits
76992e262d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195941a402 | ||
|
|
f0017a8703 | ||
| dc0fc6e41f | |||
|
|
f2a717df4f | ||
|
|
7bbbdbe977 | ||
| 268cf9e50f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -91,6 +91,7 @@ out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export default async function useOverlay() {
|
||||
if (import.meta.server) {
|
||||
// SSR에서는 cytoscape-overlays를 사용하지 않음
|
||||
return null
|
||||
}
|
||||
// 전체 export 객체를 반환
|
||||
return await import('cytoscape-overlays')
|
||||
}
|
||||
// SPA 모드에서는 항상 클라이언트에서만 실행됨
|
||||
return await import("cytoscape-overlays");
|
||||
}
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
if (import.meta.client) {
|
||||
const userStore = useUserStore();
|
||||
const { hasPagePermission } = usePermission();
|
||||
const userStore = useUserStore();
|
||||
const { hasPagePermission } = usePermission();
|
||||
|
||||
// 공개 라우트 목록 (로그인 없이 접근 가능)
|
||||
const publicRoutes = ["/login", "/register", "/auth-error"];
|
||||
// 공개 라우트 목록 (로그인 없이 접근 가능)
|
||||
const publicRoutes = ["/login", "/register", "/auth-error"];
|
||||
|
||||
// 공개 라우트인지 확인
|
||||
const isPublicRoute = publicRoutes.some(route => to.path === route);
|
||||
// 공개 라우트인지 확인
|
||||
const isPublicRoute = publicRoutes.some(route => to.path === route);
|
||||
|
||||
// 공개 라우트가 아닌 경우 로그인 체크
|
||||
if (!isPublicRoute && !userStore.isLoggedIn) {
|
||||
return navigateTo("/login");
|
||||
// 공개 라우트가 아닌 경우 로그인 체크
|
||||
if (!isPublicRoute && !userStore.isLoggedIn) {
|
||||
return navigateTo("/login");
|
||||
}
|
||||
|
||||
// 로그인된 사용자의 경우 권한 체크
|
||||
if (userStore.isLoggedIn) {
|
||||
// 홈화면 경로는 항상 허용
|
||||
if (to.path === "/" || to.path === "/1/") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 로그인된 사용자의 경우 권한 체크
|
||||
if (userStore.isLoggedIn) {
|
||||
// 홈화면 경로는 항상 허용
|
||||
if (to.path === "/" || to.path === "/1/") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지 권한 체크
|
||||
if (!hasPagePermission(to.path)) {
|
||||
console.log(`페이지 권한이 없습니다.: ${to.path}`);
|
||||
alert(`페이지 권한이 없습니다.`);
|
||||
return navigateTo("/");
|
||||
}
|
||||
// 페이지 권한 체크
|
||||
if (!hasPagePermission(to.path)) {
|
||||
console.log(`페이지 권한이 없습니다.: ${to.path}`);
|
||||
alert(`페이지 권한이 없습니다.`);
|
||||
return navigateTo("/");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-05-15",
|
||||
ssr: false,
|
||||
devtools: { enabled: true },
|
||||
modules: [
|
||||
"@nuxt/eslint",
|
||||
@@ -36,7 +36,7 @@ export default defineNuxtConfig({
|
||||
shim: false,
|
||||
strict: true,
|
||||
},
|
||||
plugins: ["~/plugins/vue3-tui-grid.client.ts"],
|
||||
plugins: ["~/plugins/vue3-tui-grid.ts"],
|
||||
components: [
|
||||
{ path: "~/components/base", pathPrefix: false }, // @base/ 접두사 제거
|
||||
{ path: "~/components/layout", pathPrefix: false }, // @layout/ 접두사 제거
|
||||
|
||||
6046
package-lock.json
generated
6046
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@
|
||||
"@nuxt/eslint": "^1.4.1",
|
||||
"@nuxt/icon": "^1.14.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxtjs/tailwindcss": "^7.0.0-beta.0",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"ag-grid-community": "^34.0.0",
|
||||
"ag-grid-vue3": "^34.0.0",
|
||||
@@ -26,6 +25,7 @@
|
||||
"echarts": "^5.6.0",
|
||||
"eslint": "^9.29.0",
|
||||
"ipx": "^3.1.1",
|
||||
"molstar": "^5.1.2",
|
||||
"nuxt": "^3.17.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
@@ -36,11 +36,12 @@
|
||||
"vue3-tui-grid": "^0.1.51"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4.1.11"
|
||||
"tailwindcss": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
87
pages/[tabId]/test/molstar.vue
Normal file
87
pages/[tabId]/test/molstar.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="molstar-page">
|
||||
<PageDescription>
|
||||
<h1>Mol* 뷰어 테스트</h1>
|
||||
<div class="box">
|
||||
<p>간단한 PDB 구조(1CRN)를 불러와 Mol*로 렌더링합니다.</p>
|
||||
</div>
|
||||
</PageDescription>
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="viewer-wrap"
|
||||
:style="{ height: containerHeight }"
|
||||
>
|
||||
<div ref="viewerRef" class="molstar-viewer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, ref } from "vue";
|
||||
import { Viewer } from "molstar/lib/apps/viewer/app";
|
||||
import "molstar/build/viewer/molstar.css";
|
||||
|
||||
definePageMeta({
|
||||
title: "Mol* 테스트",
|
||||
description: "Mol* 뷰어 테스트 페이지",
|
||||
});
|
||||
|
||||
const viewerRef = ref<HTMLDivElement | null>(null);
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const containerHeight = ref<string>("80vh");
|
||||
let viewer: Viewer | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
if (!viewerRef.value) return;
|
||||
|
||||
viewer = await Viewer.create(viewerRef.value, {
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
viewportShowExpand: true,
|
||||
});
|
||||
|
||||
// 예시 구조: 1CRN
|
||||
await viewer.loadStructureFromUrl(
|
||||
"https://files.rcsb.org/download/1CRN.pdb",
|
||||
"pdb"
|
||||
);
|
||||
|
||||
const resizeToAvailableHeight = () => {
|
||||
if (!containerRef.value) return;
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
const bottomPadding = 16; // 여백
|
||||
const available = window.innerHeight - rect.top - bottomPadding;
|
||||
containerHeight.value = `${Math.max(300, available)}px`;
|
||||
viewer?.handleResize();
|
||||
};
|
||||
|
||||
resizeToAvailableHeight();
|
||||
window.addEventListener("resize", resizeToAvailableHeight);
|
||||
// 정리 함수 저장
|
||||
(containerRef as any)._off = () =>
|
||||
window.removeEventListener("resize", resizeToAvailableHeight);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
viewer?.dispose?.();
|
||||
viewer = null;
|
||||
(containerRef as any)?._off?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.viewer-wrap {
|
||||
width: 100%;
|
||||
overflow: hidden; /* 내부 Mol*에서 자체 스크롤 관리 */
|
||||
}
|
||||
.molstar-viewer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msp-plugin .msp-layout-expanded {
|
||||
position: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -19,18 +19,6 @@ export default defineNuxtPlugin(() => {
|
||||
...(isFormData ? {} : { "Content-Type": "application/json" }),
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
// 3) SSR 쿠키 포워딩
|
||||
if (import.meta.server) {
|
||||
const cookie = useRequestHeaders(["cookie"])?.cookie;
|
||||
const reqUrl = String(request);
|
||||
const isBackendApi =
|
||||
!reqUrl.startsWith("http") || reqUrl.startsWith(baseURL);
|
||||
|
||||
if (cookie && isBackendApi) {
|
||||
options.headers = { ...(options.headers || {}), cookie } as any;
|
||||
}
|
||||
}
|
||||
},
|
||||
onResponseError({ response }) {
|
||||
// 공통 로깅
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import TuiGrid from 'vue3-tui-grid'
|
||||
import 'tui-grid/dist/tui-grid.css'
|
||||
|
||||
export default defineNuxtPlugin(nuxtApp => {
|
||||
nuxtApp.vueApp.use(TuiGrid)
|
||||
})
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import TuiGrid from 'vue3-tui-grid'
|
||||
import 'tui-grid/dist/tui-grid.css'
|
||||
|
||||
export default defineNuxtPlugin(nuxtApp => {
|
||||
nuxtApp.vueApp.use(TuiGrid)
|
||||
})
|
||||
65
public/cy_custom.js
Normal file
65
public/cy_custom.js
Normal file
@@ -0,0 +1,65 @@
|
||||
(function () {
|
||||
const EXTREME_ZOOM_MIN = 1e-4;
|
||||
const EXTREME_ZOOM_MAX = 1e4;
|
||||
|
||||
function applyCustomZoomHandling(cy) {
|
||||
let zoomFramePending = false;
|
||||
|
||||
if (!cy || typeof cy.zoom !== 'function') {
|
||||
console.warn('🚫 유효하지 않은 Cytoscape 인스턴스입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
cy.userZoomingEnabled(false); // Cytoscape 기본 줌 비활성화
|
||||
|
||||
const container = cy.container();
|
||||
if (!container) {
|
||||
console.warn('🚫 Cytoscape container를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
container.addEventListener(
|
||||
'wheel',
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (zoomFramePending) return;
|
||||
zoomFramePending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const currentZoom = cy.zoom();
|
||||
|
||||
const clampedDelta = Math.max(-100, Math.min(100, event.deltaY));
|
||||
const zoomFactor = 1 + (clampedDelta > 0 ? -0.05 : 0.05);
|
||||
const nextZoom = currentZoom * zoomFactor;
|
||||
|
||||
const isValid =
|
||||
isFinite(nextZoom) &&
|
||||
nextZoom >= EXTREME_ZOOM_MIN &&
|
||||
nextZoom <= EXTREME_ZOOM_MAX;
|
||||
|
||||
if (isValid) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const renderedPosition = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
cy.zoom({
|
||||
level: nextZoom,
|
||||
renderedPosition
|
||||
});
|
||||
} else {
|
||||
console.warn("🚫 휠 줌 비정상 값 차단:", nextZoom);
|
||||
}
|
||||
|
||||
zoomFramePending = false;
|
||||
});
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 전역(window)으로 함수 등록
|
||||
window.applyCustomZoomHandling = applyCustomZoomHandling;
|
||||
})();
|
||||
|
||||
28
public/igv.css
Normal file
28
public/igv.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/* IGV blue lines & UI custom style (기본 파란색: #3579f6) */
|
||||
.igv-blue-line {
|
||||
background: #3579f6 !important;
|
||||
width: 2px !important;
|
||||
z-index: 1000;
|
||||
}
|
||||
.igv-blue-fill {
|
||||
background: rgba(53, 121, 246, 0.18) !important;
|
||||
z-index: 999;
|
||||
}
|
||||
.igv-control-btn {
|
||||
background: #f5faff;
|
||||
color: #3579f6;
|
||||
border: 1.5px solid #3579f6;
|
||||
border-radius: 5px;
|
||||
padding: 6px 16px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border 0.2s, color 0.2s;
|
||||
}
|
||||
.igv-control-btn:hover {
|
||||
background: #3579f6;
|
||||
color: #fff;
|
||||
border: 2px solid #2456a6;
|
||||
}
|
||||
.igv-blue-line.selected {
|
||||
box-shadow: 0 0 8px #3579f6;
|
||||
}
|
||||
67298
public/igv.js
Normal file
67298
public/igv.js
Normal file
File diff suppressed because one or more lines are too long
222
public/igv_custom.js
Normal file
222
public/igv_custom.js
Normal file
@@ -0,0 +1,222 @@
|
||||
// igv_custom.js
|
||||
// IGV 브라우저가 생성된 후, 파란색 라인 2개를 ideogram/viewport에 추가하고 드래그로 이동 가능하게 함
|
||||
// 라인 위치 변경 시 window에 커스텀 이벤트('igv-blue-lines-changed')를 dispatch
|
||||
|
||||
(function() {
|
||||
// 설정값
|
||||
const LINE_COLOR = '#007bff';
|
||||
const LINE_WIDTH = 2;
|
||||
const INIT_RATIO_1 = 0.3; // 30% 위치
|
||||
const INIT_RATIO_2 = 0.7; // 70% 위치
|
||||
|
||||
// 내부 상태
|
||||
let viewportRect = null;
|
||||
let dragging = null; // 'start' or 'end' or null
|
||||
let dragOffset = 0;
|
||||
let startRatio = INIT_RATIO_1;
|
||||
let endRatio = INIT_RATIO_2;
|
||||
|
||||
// 라인 DOM
|
||||
let viewportLines = [];
|
||||
let viewportFill = null;
|
||||
let ideogramLines = [];
|
||||
let ideogramFill = null;
|
||||
|
||||
// IGV가 준비되면 실행
|
||||
function waitForIGVAndInit() {
|
||||
const check = () => {
|
||||
const viewport = document.querySelector('.igv-viewport-content');
|
||||
if (viewport) {
|
||||
setupLines();
|
||||
} else {
|
||||
setTimeout(check, 300);
|
||||
}
|
||||
};
|
||||
check();
|
||||
}
|
||||
|
||||
// 라인 생성 및 이벤트 바인딩
|
||||
function setupLines() {
|
||||
// 모든 파란 라인 삭제
|
||||
document.querySelectorAll('.igv-blue-line').forEach(el => el.remove());
|
||||
// 기존 fill 오버레이 삭제
|
||||
document.querySelectorAll('.igv-blue-fill').forEach(el => el.remove());
|
||||
|
||||
// 모든 .igv-viewport-content 중, height가 16px 초과인 첫 번째를 메인 뷰포트로 간주
|
||||
const viewports = Array.from(document.querySelectorAll('.igv-viewport-content'));
|
||||
const mainViewport = viewports.find(vp => {
|
||||
const h = vp.offsetHeight || parseInt(vp.style.height, 10);
|
||||
return h > 16;
|
||||
});
|
||||
if (!mainViewport) return;
|
||||
|
||||
// 이미 있으면 중복 생성 방지
|
||||
if (mainViewport.parentNode.querySelector('.igv-blue-line')) return;
|
||||
|
||||
viewportRect = mainViewport.getBoundingClientRect();
|
||||
|
||||
// 라인 2개 생성 (뷰포트)
|
||||
viewportLines = [createLine('start', 'viewport', mainViewport), createLine('end', 'viewport', mainViewport)];
|
||||
viewportLines.forEach(line => mainViewport.parentNode.appendChild(line));
|
||||
// fill 오버레이 생성 및 추가 (뷰포트)
|
||||
viewportFill = document.createElement('div');
|
||||
viewportFill.className = 'igv-blue-fill';
|
||||
Object.assign(viewportFill.style, {
|
||||
position: 'absolute',
|
||||
top: (viewportRect.top - mainViewport.getBoundingClientRect().top) + 'px',
|
||||
height: viewportRect.height + 'px',
|
||||
background: 'rgba(0,123,255,0.18)',
|
||||
zIndex: 999,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: '2px',
|
||||
transition: 'left 0.08s, width 0.08s',
|
||||
});
|
||||
mainViewport.parentNode.appendChild(viewportFill);
|
||||
|
||||
// === ideogram(미니맵)에도 라인/오버레이 추가 ===
|
||||
const ideogram = document.querySelector('.igv-ideogram-content') || document.querySelector('.igv-ideogram');
|
||||
if (ideogram) {
|
||||
const ideogramRect = ideogram.getBoundingClientRect();
|
||||
ideogramLines = [createLine('start', 'ideogram', ideogram), createLine('end', 'ideogram', ideogram)];
|
||||
ideogramLines.forEach(line => ideogram.parentNode.appendChild(line));
|
||||
ideogramFill = document.createElement('div');
|
||||
ideogramFill.className = 'igv-blue-fill';
|
||||
Object.assign(ideogramFill.style, {
|
||||
position: 'absolute',
|
||||
top: (ideogramRect.top - ideogram.parentNode.getBoundingClientRect().top) + 'px',
|
||||
height: ideogramRect.height + 'px',
|
||||
background: 'rgba(0,123,255,0.18)',
|
||||
zIndex: 999,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: '2px',
|
||||
transition: 'left 0.08s, width 0.08s',
|
||||
});
|
||||
ideogram.parentNode.appendChild(ideogramFill);
|
||||
}
|
||||
|
||||
updateLinePositions();
|
||||
bindDragEvents();
|
||||
}
|
||||
|
||||
// 라인 DOM 생성
|
||||
function createLine(which, area, parentEl) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'igv-blue-line';
|
||||
line.dataset.which = which;
|
||||
line.dataset.area = area;
|
||||
line._parentEl = parentEl; // 커스텀 속성으로 부모 저장
|
||||
Object.assign(line.style, {
|
||||
position: 'absolute',
|
||||
top: area === 'ideogram' ? (viewportRect.top - parentEl.getBoundingClientRect().top) + 'px' : (viewportRect.top - parentEl.getBoundingClientRect().top) + 'px',
|
||||
width: LINE_WIDTH + 'px',
|
||||
height: area === 'ideogram' ? viewportRect.height + 'px' : viewportRect.height + 'px',
|
||||
background: LINE_COLOR,
|
||||
zIndex: 1000,
|
||||
cursor: 'ew-resize',
|
||||
userSelect: 'none',
|
||||
});
|
||||
line.addEventListener('mousedown', (e) => startDrag(which, e));
|
||||
return line;
|
||||
}
|
||||
|
||||
// 라인 위치 갱신
|
||||
function updateLinePositions() {
|
||||
viewportRect = document.querySelector('.igv-viewport-content').getBoundingClientRect();
|
||||
let baseArea = document.querySelector('.igv-viewport-content canvas, .igv-viewport-content svg');
|
||||
let baseRect = baseArea ? baseArea.getBoundingClientRect() : viewportRect;
|
||||
// viewport 라인 위치 (실제 베이스 표시 영역 기준)
|
||||
const baseWidth = baseRect.width;
|
||||
const baseLeft = baseRect.left;
|
||||
const startX_view = baseLeft + baseWidth * startRatio;
|
||||
const endX_view = baseLeft + baseWidth * endRatio;
|
||||
viewportLines[0].style.left = (startX_view - viewportLines[0]._parentEl.getBoundingClientRect().left) + 'px';
|
||||
viewportLines[1].style.left = (endX_view - viewportLines[1]._parentEl.getBoundingClientRect().left) + 'px';
|
||||
// fill 오버레이 위치/크기 갱신
|
||||
if (viewportFill) {
|
||||
const left = Math.min(startX_view, endX_view) - viewportFill.parentNode.getBoundingClientRect().left;
|
||||
const width = Math.abs(endX_view - startX_view);
|
||||
viewportFill.style.left = left + 'px';
|
||||
viewportFill.style.width = width + 'px';
|
||||
}
|
||||
// === ideogram(미니맵) 라인/오버레이 위치 갱신 ===
|
||||
const ideogram = document.querySelector('.igv-ideogram-content') || document.querySelector('.igv-ideogram');
|
||||
if (ideogram && ideogramLines.length === 2 && ideogramFill) {
|
||||
const ideogramRect = ideogram.getBoundingClientRect();
|
||||
const ideogramWidth = ideogramRect.width;
|
||||
const ideogramLeft = ideogramRect.left;
|
||||
const startX_ideo = ideogramLeft + ideogramWidth * startRatio;
|
||||
const endX_ideo = ideogramLeft + ideogramWidth * endRatio;
|
||||
ideogramLines[0].style.left = (startX_ideo - ideogram.parentNode.getBoundingClientRect().left) + 'px';
|
||||
ideogramLines[1].style.left = (endX_ideo - ideogram.parentNode.getBoundingClientRect().left) + 'px';
|
||||
// fill
|
||||
const left = Math.min(startX_ideo, endX_ideo) - ideogramFill.parentNode.getBoundingClientRect().left;
|
||||
const width = Math.abs(endX_ideo - startX_ideo);
|
||||
ideogramFill.style.left = left + 'px';
|
||||
ideogramFill.style.width = width + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그 이벤트 바인딩
|
||||
function bindDragEvents() {
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
}
|
||||
|
||||
function startDrag(which, e) {
|
||||
dragging = which;
|
||||
dragOffset = e.clientX;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function onDrag(e) {
|
||||
if (!dragging) return;
|
||||
const areaWidth = viewportRect.width;
|
||||
let x = e.clientX - viewportRect.left;
|
||||
let ratio = x / areaWidth;
|
||||
ratio = Math.max(0, Math.min(1, ratio));
|
||||
if (dragging === 'start') {
|
||||
startRatio = Math.min(ratio, endRatio - 0.01);
|
||||
} else {
|
||||
endRatio = Math.max(ratio, startRatio + 0.01);
|
||||
}
|
||||
updateLinePositions();
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
dragging = null;
|
||||
}
|
||||
|
||||
// 라인 위치 변경 이벤트 발생
|
||||
function fireChangeEvent() {
|
||||
console.log('fireChangeEvent 호출됨', { startRatio, endRatio });
|
||||
window.dispatchEvent(new CustomEvent('igv-blue-lines-changed', {
|
||||
detail: {
|
||||
startRatio,
|
||||
endRatio
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 외부에서 라인 위치를 강제로 지정할 수 있도록 export
|
||||
window.igvCustom = {
|
||||
setLineRatios: (start, end) => {
|
||||
startRatio = start;
|
||||
endRatio = end;
|
||||
updateLinePositions();
|
||||
fireChangeEvent();
|
||||
},
|
||||
getLineRatios: () => ({ startRatio, endRatio }),
|
||||
setupLines: setupLines
|
||||
};
|
||||
|
||||
// 스타일 추가
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `.igv-blue-line { pointer-events: auto !important; }
|
||||
.igv-blue-fill { pointer-events: none !important; }`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// IGV가 준비되면 실행
|
||||
setTimeout(waitForIGVAndInit, 1000);
|
||||
})();
|
||||
@@ -302,6 +302,17 @@ export const MOCK_PERMISSIONS: UserPermissions = {
|
||||
description: "테스트 등록 페이지",
|
||||
menuYn: "N",
|
||||
},
|
||||
{
|
||||
oid: 26,
|
||||
code: "P0118",
|
||||
name: "molstar",
|
||||
type: "PAGE",
|
||||
path: "/test/molstar",
|
||||
parentCode: "PG01",
|
||||
sortOrder: 18,
|
||||
description: "Mol* 뷰어 테스트 페이지",
|
||||
menuYn: "Y",
|
||||
},
|
||||
|
||||
// 관리자 페이지그룹 하위 페이지들 (PG02 > P0201~P0203)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user