정상 DNA와 돌연변이 DNA를 비교 분석하는 웹 프로그램의 개발 과정과 전체 소스코드
기존 파이썬 콘솔 프로그램을 프론트엔드(HTML·CSS·JavaScript) + 백엔드(Python) 구조의 웹 시스템으로 업그레이드하고, 국가 암 공공데이터 연계까지 확장한 프로젝트입니다.
이 프로젝트는 작업 지시(사용자)와 수행·답변(Claude)이 오간 대화로 진행되었습니다. 전체 대화 기록에서 지시와 수행 내용을 추출해 시간 순서대로 정리했습니다.
| 순번 | 작업 지시 (요약) | 수행 내용·답변 (요약) | 결과물 |
|---|---|---|---|
| 1 | 기존 콘솔 프로그램(카카오톡 zip 이미지 6장)과 ChatGPT 문서를 검토한 뒤 웹으로 업그레이드 — 프론트/백엔드 분리, 염기별 색상(빨강=틀린 염기), 양쪽 비교, bat 실행, NCBI 스타일 한국어 화면, 샘플 서열 | 이미지에서 기존 분석 로직(변이 탐지·핫스팟·위험도)을 파악하고 파이썬 표준 라이브러리만 쓰는 백엔드 + HTML/CSS/JS 프론트엔드로 재구축. run.bat 제작, NCBI 5종 샘플 탑재. 기존 프로그램과 동일한 결과(변이 3개·50%)가 나오는 것을 검증 | D:\dna 웹 프로그램 1차 완성 |
| 2 | Route 53에 개설한 DNS를 EC2 #2에 올려 hyunah.astec-mobility.com으로 인터넷 접속되게 해달라 | PC에서 SSH 키를 찾아 EC2 접속 → ~/hyunah 업로드, systemd 자동 실행 서비스 등록, Caddy 리버스 프록시 + HTTPS 인증서 자동 발급 구성, 외부 접속 검증 | https://hyunah.astec-mobility.com 공개 |
| 3 | 블로그 이미지처럼 DNA가 입체로 나열되게, 한쪽 서열만 입력하는 게 정상인지 답변, 마우스 드래그로 상하좌우 회전되게 | 외부 라이브러리 없이 순수 캔버스로 3D 이중나선 구현(드래그 회전·휠 확대·자동 회전). 상보 가닥(A-T, G-C)을 자동 생성해 이중나선을 완성하며, '한쪽 가닥만 입력하는 것이 NCBI 표준'임을 화면 안내문과 답변으로 설명 | 3D 이중나선 기능 |
| 4 | BRCA1·EGFR이 설명과 달리 0%로 나온다 — 암 발생 확률과 임상 결과 데이터를 인터넷 근거로 채워달라 | NCBI에서 실제 RefSeq 서열을 내려받아 검증한 뒤 실제 병원성 변이(BRCA1 C61G, EGFR L858R, KRAS G12D, HBB E6V)를 반영. 문헌 기반 위험도(72%·90% 등)와 치료·임상시험 데이터, ClinVar 링크를 담은 임상 정보 카드 신설 | 실제 임상 데이터 반영 |
| 5 | run.bat 실행 시 에러 발생 — 인터넷 없는 곳에서도 작동하게 하고 파이썬 3.14도 자동으로 깔리게 | 한글 인코딩 충돌이 원인임을 바이트 단위로 진단 → ASCII 전용으로 재작성. 파이썬 3.14 임베디드 버전(11.5MB)을 폴더에 통째로 내장해 설치·인터넷 없이 더블클릭 실행 가능하게 구성 | 오프라인 실행 패키지 |
| 6 | CSS가 적용되지 않은 상태를 임시로 보고 싶다 → 다시 원상 복구 | 스타일시트 연결을 주석 처리해 비활성화, 확인 후 원복 | - |
| 7 | 오늘 작업을 크롬만으로 볼 수 있게 — 설명과 코딩 라인 포함, 복사-붙여넣기 검증 확실히 | 실제 소스 파일을 읽어 보고서를 만드는 생성기 제작(코드 불일치 원천 차단). 줄번호는 복사에서 제외되고 복사 버튼으로 순수 코드만 복사됨을 자동 테스트로 검증. 로컬 파일 + /report 웹 공개 | 작업보고서.html · /report |
| 8 | 공공데이터 API 키로 데이터 연동이 되는지 체크, dna2 폴더로 복사해 EC2에 올리고 메인에 이동 박스 | 국립암센터 HMSH2 API 검증(6,894건·69페이지) → dna2 개발: 서버가 수집·집계 후 24시간 캐시하는 /api/cancerstats, 통계 화면. EC2에 별도 서비스로 배포(/dna2), 메인 화면에 NEW 배너 박스 추가 | 공공데이터 연계판(/dna2) |
| 9 | 이 API에서 DNA 염기서열을 가져와 연동할 수 있는지 체크 | 전체 응답 필드를 표본 조사 → 염기서열은 없음(개인 유전정보 보호로 집계값만 공개) 확인. 대신 loss/intact(단백질 소실=돌연변이 증거) 필드를 발견해 보고하고 MSH2 서열 연계를 제안 | 분석 보고 |
| 10 | MSH2 추가, 공공데이터를 주제와 연관되게 재분류, 돌연변이의 인체 영향을 고2 수준으로 사례별 설명, 치료법·생존기간 추가, (추가) 핫스팟 설명도 | MSH2 실서열(NM_000251) 검증 후 R406X 넌센스 변이 샘플 추가. 통계를 loss/intact 중심으로 재집계(소실율 5.8%, 40세 미만 11.7% 발견). 6종 전체에 '인체에 미치는 영향'(비유 설명)·'생존율·예후'·'핫스팟 설명' 추가 | 6종 샘플 + 재분류 통계 |
| 11 | 샘플 카드를 3칸 2줄로 균등 배치하고, MSH2 옆에 다른 색으로 '공공데이터 연계 샘플' 표시, 보고서 갱신 | 3열 균등 그리드와 초록 배지 적용. 브라우저 캐시 문제를 발견해 버전 태그를 도입, 방문자가 항상 최신 화면을 받도록 개선 | 화면 배치 개선 |
| 12 | 샘플이 아닌 수기 입력 염기서열도 분석되는지 체크 | 6가지 시나리오(임의 서열·길이 불일치·잘못된 문자·소문자/공백·동일 서열·300bp) + 화면 타이핑 흐름 검증 → 전부 정상. 검사 중 3D 캔버스 리사이즈 순서 문제를 발견해 함께 수정 | 검증 보고 + 안정성 수정 |
| 13 | 작업 지시·수행 내역을 보고서에 반영, 3D 모델 2종 추가(카툰 + 자유 선택)로 선택 가능하게, HTML/CSS/JS를 클릭형 그래픽 설명서로 | 작업 내역 표와 '웹 기술 설명서' 섹션 신설(실제 사이트 축소판을 클릭하면 해당 HTML·CSS 코드 표시). 3D 모델 선택 기능 구현: 기본 모델 / 카툰 모델(굵은 외곽선·플랫 색상) / 네온 홀로그램(어두운 배경·발광 효과) | 보고서 확장 + 3D 모델 3종 |
카카오톡으로 받은 기존 콘솔 프로그램 이미지 6장과 ChatGPT 문서(p53 단백질 벡터)를 검토하여 기존 분석 로직(변이 탐지, 핫스팟, 위험도 계산)을 파악했습니다.
백엔드는 파이썬 표준 라이브러리만 사용해 어떤 파이썬 버전(3.8~3.14)에서도 실행되도록 했고, 프론트엔드는 NCBI 사이트를 참고한 한국어 화면으로 만들었습니다.
Route 53에 만든 hyunah.astec-mobility.com 도메인을 EC2 서버에 연결했습니다. systemd 서비스로 자동 실행을 등록하고 Caddy로 HTTPS를 적용했습니다.
외부 라이브러리 없이 순수 JavaScript 캔버스로 이중나선을 구현했습니다. 기본/카툰/네온 홀로그램 3종 모델 중 선택할 수 있고, 드래그로 상하좌우 회전, 휠로 확대·축소가 됩니다.
NCBI에서 실제 유전자 서열(RefSeq)을 내려받아 검증한 뒤, BRCA1 C61G·EGFR L858R·KRAS G12D·HBB E6V·MSH2 R406X 등 실제 병원성 변이와 문헌 기반 위험도·치료 정보를 반영했습니다.
run.bat의 한글 인코딩 문제를 해결하고, Python 3.14 임베디드 버전을 폴더에 내장시켜 파이썬 설치나 인터넷 없이도 더블클릭 한 번으로 실행되게 했습니다.
국립암센터 대장암 HMSH2 검사결과 오픈API를 연동해 공공데이터 연계판(/dna2)을 만들었습니다. 단백질 소실(loss) 비율 5.8%, 40세 미만 11.7% 등 실측 통계를 시각화했습니다.
지금 보고 있는 이 작업 보고서를 만들어 로컬과 인터넷 양쪽에서 열람할 수 있게 했습니다. 보고서는 실제 소스 파일을 읽어 자동 생성되므로 코드와 항상 일치합니다.
브라우저(프론트엔드)가 요청을 보내면 백엔드가 JSON으로 답하고, 화면이 이를 그래픽으로 표시합니다.
| 구성 요소 | 기술 | 역할 |
|---|---|---|
| 프론트엔드 | HTML + CSS + JavaScript | 서열 비교 렌더링, 3D 이중나선, 분석 API 호출 |
| 백엔드 | Python 3 (표준 라이브러리만) | 정적 파일 제공, 돌연변이 분석, 샘플·임상 데이터 |
| 배포 | AWS EC2 + Caddy + systemd | 인터넷 공개(HTTPS), 자동 재시작 |
| 주소 | 방식 | 설명 |
|---|---|---|
| /analyzer | 페이지 | 유전자 돌연변이 분석 시스템 (분석 화면) |
| /report | 페이지 | 이 작업 보고서 |
| /api/health | GET | 서버 동작 확인 (파이썬 버전 반환) |
| /api/samples | GET | 유전자 샘플 6종 + 기본 p53 서열 반환 |
| /api/analyze | POST | 정상·돌연변이 서열을 받아 변이 목록, 위험도, 임상 정보 반환 |
모든 샘플 서열은 NCBI RefSeq에서 실제 서열을 내려받아 대조 검증했으며, 변이는 ClinVar 등 문헌에 보고된 실제 임상 변이입니다.
| 유전자 | RefSeq | 변이 | 관련 질환 | 핵심 임상 정보 |
|---|---|---|---|---|
| TP53 (p53) | NM_000546 | 데모 변이 3개 | 거의 모든 암 | 전체 암의 약 50%에서 변이. 리-프라우메니 증후군 |
| KRAS | NM_004985 | c.35G>A (G12D) | 췌장암·대장암 | 췌장암의 약 90%가 KRAS 변이. G12D 표적 억제제 임상 중 |
| HBB | NM_000518 | c.20A>T (E6V) | 겸상적혈구병 | 2023년 CRISPR 치료제 카스게비 FDA 승인 |
| BRCA1 | NM_007294 | c.181T>G (C61G) | 유방암·난소암 | 보인자 평생 유방암 위험 55~72%. PARP 억제제 감수성 |
| EGFR | NM_005228 | c.2573T>G (L858R) | 폐선암 | EGFR-TKI 반응률 70~80%. 오시머티닙 PFS 18.9개월(FLAURA) |
| MSH2 | NM_000251 | c.1216C>T (R406X) | 대장암(린치증후군) | 보인자 대장암 위험 50~80%. 면역항암제 감수성(KEYNOTE-177) |
웹 페이지는 세 가지 기술이 역할을 나눠 만듭니다. 집짓기에 비유하면 아래와 같습니다. (아래 축소판은 분석 시스템 화면(/analyzer) 기준입니다)
아래 코드는 이 보고서를 만드는 시점의 실제 파일에서 읽어 수록한 것입니다 (유전자 돌연변이 분석 시스템 범위). 각 블록의 [코드 복사] 버튼을 누르면 줄번호 없이 코드만 클립보드에 복사되어, 붙여넣기로 바로 실행·검증할 수 있습니다.
파이썬 표준 라이브러리만으로 만든 HTTP 서버. 정적 파일 제공 + 돌연변이 분석 API + NCBI 실제 서열 샘플 + 임상 데이터를 담당합니다.
# -*- coding: utf-8 -*-
"""
p53 유전자 돌연변이 분석 시스템 - 백엔드 서버
================================================
- Python 3.12 권장 (표준 라이브러리만 사용하므로 Python 3.8 이상 어디서나 실행 가능)
- 외부 패키지 설치 불필요 (범용성 보장)
- 실행: python backend/server.py (또는 run.bat)
"""
import json
import os
import sys
import webbrowser
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse
# ------------------------------------------------------------------
# 경로 설정
# ------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
HOST = os.environ.get("HOST", "0.0.0.0") # 서버 배포 시 127.0.0.1 권장
OPEN_BROWSER = os.environ.get("NO_BROWSER", "") == "" # NO_BROWSER=1 이면 브라우저 자동 열기 생략
# ------------------------------------------------------------------
# 기본 서열 (TP53 CDS 시작 부분, NCBI NM_000546 참고)
# ------------------------------------------------------------------
DEFAULT_NORMAL = "ATGGAGGAGCCGCAGTCAGATCCTAGCGTCGAGCCCCCTCTGAGTCAGGAAACATTTTCAGACCT"
DEFAULT_MUTANT = "ATGGAGGAGTCGCAGTCATATCCTAGCGTCGAGCCCCCTCTGAGTCAGGAAACATCTTCAGACCT"
# 발표용 가상 p53 핫스팟 (기존 프로그램과 동일)
HOTSPOT = [10, 20, 30, 40]
# ------------------------------------------------------------------
# NCBI 참고 샘플 서열 (실제 RefSeq CDS에서 발췌, 실제 임상 변이 반영)
# - offset : 표시 구간 첫 염기의 CDS 좌표 (c. 위치)
# - hotspots : 표시 구간 내(로컬 위치) 중요 변이 구간
# - known_variant : 실제 임상 변이 (로컬 위치)
# - clinical : 임상 데이터 (문헌·ClinVar 기반)
# ------------------------------------------------------------------
BRCA1_NORMAL = (
"ATGGATTTATCTGCTCTTCGCGTTGAAGAAGTACAAAATGTCATTAATGCTATGCAGAAAATCTTAGAG"
"TGTCCCATCTGTCTGGAGTTGATCAAGGAACCTGTCTCCACAAAGTGTGACCACATATTTTGCAAATTT"
"TGCATGCTGAAACTTCTCAACCAGAAGAAAGGGCCTTCACAGTGTCCTTTATGTAAGAATGATATAACC"
) # NM_007294.4 CDS c.1~207 (실제 서열)
BRCA1_MUTANT = BRCA1_NORMAL[:180] + "G" + BRCA1_NORMAL[181:] # c.181T>G (C61G)
EGFR_NORMAL = (
"AGGAACGTACTGGTGAAAACACCGCAGCATGTCAAGATCACAGATTTTGGGCTGGCCAAACTGCTGGGT"
"GCGGAAGAGAAAGAATACCAT"
) # NM_005228.5 CDS c.2521~2610 (엑손 21 부근, 실제 서열)
EGFR_MUTANT = EGFR_NORMAL[:52] + "G" + EGFR_NORMAL[53:] # c.2573T>G (L858R)
MSH2_NORMAL = (
"GCCAAGAAGTTTCAAAGACAAGCAGCAAACTTACAAGATTGTTACCGACTCTATCAGGGTATAAATCAA"
"CTACCTAATGTTATACAGGCT"
) # NM_000251.3 CDS c.1171~1260 (실제 서열)
MSH2_MUTANT = MSH2_NORMAL[:45] + "T" + MSH2_NORMAL[46:] # c.1216C>T (R406X, CGA→TGA)
SAMPLES = [
{
"id": "tp53",
"gene": "TP53 (p53)",
"refseq": "NM_000546",
"title": "TP53 종양억제 유전자",
"desc": "인간 종양억제 단백질 p53. 절반 이상의 암에서 돌연변이가 발견되는 대표적 유전자입니다. (발표용 데모 변이 3개)",
"normal": DEFAULT_NORMAL,
"mutant": DEFAULT_MUTANT,
"offset": 1,
"hotspots": [10, 20, 30, 40],
"known_variant": None,
"clinical": {
"variant_name": "데모 변이 (10, 19, 56번 위치)",
"summary": "TP53은 '게놈의 수호자'로 불리는 종양억제 유전자로, 전체 인간 암의 약 50%에서 변이가 발견됩니다.",
"cancers": "거의 모든 암종 (유방암, 폐암, 대장암, 난소암, 뇌종양 등)",
"risk_info": "생식세포 TP53 변이 → 리-프라우메니 증후군: 70세까지 암 발생 위험 남성 약 70%, 여성 약 90%",
"outcomes": "TP53 변이 종양은 일반적으로 예후가 불량하고 일부 항암제에 내성을 보이는 경향이 있습니다. 실제 핫스팟 코돈은 R175, G245, R248, R273 등입니다.",
"mechanism": "p53 단백질은 세포가 분열하기 전에 DNA에 손상이 없는지 검사하는 '세포의 감시자'입니다. 손상이 발견되면 분열을 멈추고 수리를 지시하거나, 수리가 불가능하면 세포를 스스로 죽게 만듭니다. 돌연변이로 이 감시자가 사라지면 손상된 세포가 검문 없이 계속 분열하고, 비정상 세포가 쌓여 종양이 됩니다.",
"survival": "예후는 암종과 변이 위치에 따라 크게 다릅니다. 생식세포 변이(리-프라우메니 증후군) 보인자는 정기 전신 MRI 검진으로 암을 조기에 발견하면 치료 성적을 크게 높일 수 있습니다.",
"hotspot_info": "이 데모에서는 10·20·30·40번 위치를 핫스팟으로 설정했습니다. 실제 TP53의 핫스팟은 R175, G245, R248, R273 코돈으로, 전 세계 암 환자에게서 같은 위치의 변이가 반복적으로 발견됩니다. DNA를 붙잡는 부위라서 한 글자만 바뀌어도 기능이 크게 망가지는 자리입니다.",
"sources": [
{"label": "NCBI Gene: TP53", "url": "https://www.ncbi.nlm.nih.gov/gene/7157"},
{"label": "ClinVar: TP53", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=TP53%5Bgene%5D"},
],
},
},
{
"id": "kras",
"gene": "KRAS",
"refseq": "NM_004985",
"title": "KRAS 원암 유전자 — G12D",
"desc": "세포 성장 신호 GTPase. c.35G>A (G12D) — 췌장암에서 가장 흔한 발암 드라이버 변이입니다.",
"normal": "ATGACTGAATATAAACTTGTGGTAGTTGGAGCTGGTGGCGTAGGCAAGAGTGCCTTGACGATACAGCTA",
"mutant": "ATGACTGAATATAAACTTGTGGTAGTTGGAGCTGATGGCGTAGGCAAGAGTGCCTTGACGATACAGCTA",
"offset": 1,
"hotspots": [34, 35, 36, 37, 38, 39], # 코돈 12·13 (G12/G13 핫스팟)
"known_variant": {"positions": [35], "name": "c.35G>A (p.Gly12Asp, G12D)"},
"clinical": {
"variant_name": "KRAS c.35G>A (G12D)",
"summary": "KRAS는 인간 암에서 가장 흔히 변이되는 원암유전자이며, G12D는 그중 대표적인 핫스팟 변이입니다.",
"cancers": "췌장암(약 90%가 KRAS 변이, 그중 G12D 최다), 대장암(약 40~45%), 폐선암(약 30%)",
"risk_info": "체세포 드라이버 변이 — 종양 발생·성장을 직접 유도하는 강력한 발암 신호",
"outcomes": "KRAS 변이 대장암은 항EGFR 항체(세툭시맙) 치료에 반응하지 않습니다. G12C용 소토라십·아다그라십이 승인되었고, G12D 표적 억제제(MRTX1133 등)는 임상시험 중입니다. 일반적으로 예후 불량 인자로 알려져 있습니다.",
"mechanism": "KRAS는 세포에 '분열하라'는 신호를 전달하는 스위치 단백질입니다. 정상일 때는 신호가 오면 켜지고 일이 끝나면 스스로 꺼집니다. 그런데 12번째 아미노산이 글리신에서 아스파트산으로 바뀌면(G12D) 스위치가 꺼지지 않게 고장 납니다. 성장 신호가 없어도 세포는 계속 '분열하라'는 명령을 받게 됩니다. 액셀이 밟힌 채로 고장 난 자동차와 같습니다.",
"survival": "췌장암의 5년 상대생존율은 약 16%(한국 국가암등록통계)로 주요 암 중 가장 낮은 편입니다. 대장암은 약 74%로 상대적으로 높습니다. G12C 억제제 승인과 G12D 억제제 임상시험으로 치료 성적 개선이 기대되고 있습니다.",
"hotspot_info": "코돈 12·13(c.34~39, 화면의 ★ 표시)이 실제 핫스팟입니다. KRAS 변이 암의 대부분이 이 여섯 염기 안에서 일어납니다. 이 자리는 스위치를 끄는 부품이 닿는 위치라, 여기가 바뀌면 스위치가 꺼지지 않게 됩니다.",
"risk_percent": 85,
"risk_level": "매우 높음",
"risk_title": "암 관련 위험도 (드라이버 변이)",
"treatment": "높음 (표적치료 임상시험)",
"approaches": [
{"name": "KRAS 표적 억제제", "desc": "G12C: 소토라십/아다그라십 승인, G12D: MRTX1133 임상시험 중"},
{"name": "병용 항암화학요법", "desc": "FOLFIRINOX 등 표준 화학요법과 병행"},
{"name": "면역항암 병용 연구", "desc": "KRAS 변이 특이 신생항원 백신·세포치료 연구 진행 중"},
],
"sources": [
{"label": "ClinVar: KRAS G12D", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=KRAS+G12D"},
{"label": "NCBI Gene: KRAS", "url": "https://www.ncbi.nlm.nih.gov/gene/3845"},
],
},
},
{
"id": "hbb",
"gene": "HBB",
"refseq": "NM_000518",
"title": "베타글로빈 유전자 — 겸상적혈구",
"desc": "c.20A>T (Glu6Val) 점돌연변이 하나가 겸상적혈구병을 일으킵니다. (암이 아닌 유전질환 예시)",
"normal": "ATGGTGCATCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGTGGATGAA",
"mutant": "ATGGTGCATCTGACTCCTGTGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGTGGATGAA",
"offset": 1,
"hotspots": [19, 20, 21], # 코돈 6 (E6V)
"known_variant": {"positions": [20], "name": "c.20A>T (p.Glu6Val, HbS)"},
"clinical": {
"variant_name": "HBB c.20A>T (Glu6Val, 겸상 헤모글로빈 HbS)",
"summary": "단 1개의 염기 치환(A→T)이 아미노산 1개(글루탐산→발린)를 바꿔 적혈구를 낫 모양으로 변형시키는, 점돌연변이 질환의 교과서적 사례입니다. 암이 아닌 유전성 혈액질환입니다.",
"cancers": "해당 없음 (유전성 빈혈 질환)",
"risk_info": "동형접합(SS)이면 겸상적혈구병 발병. 전 세계 연간 약 30만 명 이상의 신생아가 겸상적혈구병을 갖고 태어납니다. 이형접합 보인자는 말라리아 저항성을 보입니다.",
"outcomes": "혈관폐색 위기(극심한 통증), 용혈성 빈혈, 장기 손상이 나타날 수 있습니다. 하이드록시우레아로 증상을 줄일 수 있고, 2023년 세계 최초 CRISPR 유전자편집 치료제 카스게비(Casgevy)가 미국 FDA 승인을 받았습니다.",
"mechanism": "6번째 아미노산이 글루탐산(물과 잘 어울리는 성질)에서 발린(물을 싫어하는 성질)으로 바뀌면, 산소가 부족할 때 헤모글로빈 분자들이 서로 달라붙어 긴 막대 모양으로 뭉칩니다. 이 막대가 적혈구를 안에서 밀어 둥근 도넛 모양이 낫 모양으로 변형됩니다. 낫 모양 적혈구는 좁은 모세혈관에 걸려 혈관을 막고(극심한 통증 발작), 수명도 정상(120일)보다 훨씬 짧아(10~20일) 빈혈이 생깁니다.",
"survival": "치료를 받지 못하면 과거 평균 수명이 40~50대에 머물렀으나, 신생아 선별검사·하이드록시우레아·수혈 관리로 크게 개선되었습니다. 2023년 승인된 CRISPR 치료(카스게비)는 임상시험에서 환자 대부분의 통증 발작을 없앴습니다.",
"hotspot_info": "코돈 6(c.19~21, 화면의 ★ 표시)이 핵심 위치입니다. 겸상적혈구병 환자의 변이는 사실상 전부 이 한 자리(c.20A>T)에서 발견됩니다. 헤모글로빈 분자의 바깥 표면에 해당해, 여기가 바뀌면 분자끼리 달라붙게 됩니다.",
"risk_percent": 95,
"risk_level": "매우 높음",
"risk_title": "질병 발병 위험도 (동형접합 시)",
"treatment": "높음 (CRISPR 치료제 승인)",
"approaches": [
{"name": "CRISPR 유전자편집 치료 (Casgevy)", "desc": "환자 조혈모세포를 편집해 태아 헤모글로빈을 재활성화 — 2023년 FDA 승인"},
{"name": "하이드록시우레아", "desc": "태아 헤모글로빈 생성을 유도하여 겸상화 감소"},
{"name": "조혈모세포 이식", "desc": "적합한 공여자가 있는 경우 완치 가능"},
],
"sources": [
{"label": "ClinVar: HBB c.20A>T", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=HBB+c.20A%3ET"},
{"label": "NCBI Gene: HBB", "url": "https://www.ncbi.nlm.nih.gov/gene/3043"},
],
},
},
{
"id": "brca1",
"gene": "BRCA1",
"refseq": "NM_007294",
"title": "BRCA1 유방암 감수성 유전자 — C61G",
"desc": "c.181T>G (Cys61Gly) — RING 도메인을 파괴하는 병원성 변이. 유방암·난소암 위험이 크게 증가합니다.",
"normal": BRCA1_NORMAL,
"mutant": BRCA1_MUTANT,
"offset": 1,
"hotspots": [68, 69, 181], # 185delAG(c.68_69) 창시자 변이 구간 + C61G 위치
"known_variant": {"positions": [181], "name": "c.181T>G (p.Cys61Gly)"},
"clinical": {
"variant_name": "BRCA1 c.181T>G (Cys61Gly) — ClinVar 병원성(Pathogenic)",
"summary": "RING 도메인의 아연 결합 시스테인을 파괴하여 BARD1 결합과 유비퀴틴 연결효소 기능을 상실시키는 대표적 병원성 변이입니다. 유럽(특히 폴란드) 창시자 변이 중 하나입니다.",
"cancers": "유방암, 난소암, 전립선암, 췌장암",
"risk_info": "BRCA1 병원성 변이 보인자의 80세까지 누적 발병 위험: 유방암 약 55~72%, 난소암 약 39~44% (일반 여성 유방암 위험 약 12%)",
"outcomes": "BRCA1 변이 종양은 상동재조합 수선 결함으로 PARP 억제제(올라파립 등)와 백금계 항암제에 감수성을 보입니다. 예방적 유방·난소 절제술로 발병 위험을 크게 낮출 수 있습니다 (해당 절제술 사례로 안젤리나 졸리가 널리 알려져 있습니다).",
"mechanism": "BRCA1은 두 가닥이 모두 끊어진 DNA를 정확하게 이어붙이는 '수리공' 단백질입니다. 61번째 시스테인은 수리 도구(RING 구조)에 아연을 붙잡아 두는 못 역할을 하는데, 글리신으로 바뀌면 못이 빠져 도구가 통째로 망가집니다. 끊어진 DNA가 제대로 수리되지 못하면 세포는 대충 이어붙이기(오류 많은 수리)를 하게 되고, 그 과정에서 돌연변이가 계속 쌓여 유방·난소 세포가 암세포로 변할 확률이 크게 올라갑니다.",
"survival": "한국 유방암 5년 상대생존율은 약 94%이며 0~1기에 조기 발견하면 98% 이상입니다. 난소암은 약 65%입니다. BRCA 변이 난소암에서 올라파립 유지요법은 재발 위험을 크게 낮췄습니다(SOLO-1 임상). 보인자는 정기 MRI 검진과 예방 수술로 위험을 관리할 수 있습니다.",
"hotspot_info": "c.68~69(★ 표시)는 아슈케나지 유대인 집단에서 유독 흔한 185delAG 창시자 변이 구간이고, c.181(★)은 이번에 분석하는 C61G 위치입니다. 창시자 변이란 한 조상에게서 생긴 변이가 후손 집단 전체에 퍼진 경우로, 특정 집단에서 같은 위치의 변이가 반복 발견됩니다.",
"risk_percent": 72,
"risk_level": "매우 높음",
"risk_title": "평생 유방암 발병 위험도 (보인자)",
"treatment": "높음 (PARP 억제제·예방적 수술)",
"approaches": [
{"name": "PARP 억제제 (올라파립 등)", "desc": "BRCA 변이 종양의 DNA 수선 결함을 이용한 합성치사 표적치료"},
{"name": "예방적 절제술·정기 검진", "desc": "MRI 정기 검진, 예방적 유방·난소난관 절제술로 위험 감소"},
{"name": "백금계 항암제", "desc": "DNA 손상 유발 약물(카보플라틴 등)에 높은 감수성"},
],
"sources": [
{"label": "ClinVar: BRCA1 c.181T>G", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=BRCA1+c.181T%3EG"},
{"label": "NCBI Gene: BRCA1", "url": "https://www.ncbi.nlm.nih.gov/gene/672"},
],
},
},
{
"id": "egfr",
"gene": "EGFR",
"refseq": "NM_005228",
"title": "EGFR 폐암 드라이버 — L858R",
"desc": "c.2573T>G (Leu858Arg) — 비소세포폐암의 대표 드라이버 변이. 표시 구간은 엑손 21 부근(c.2521~2610)입니다.",
"normal": EGFR_NORMAL,
"mutant": EGFR_MUTANT,
"offset": 2521,
"hotspots": [52, 53, 54], # 코돈 858 (L858R)
"known_variant": {"positions": [53], "name": "c.2573T>G (p.Leu858Arg, L858R)"},
"clinical": {
"variant_name": "EGFR c.2573T>G (L858R) — 폐선암 드라이버",
"summary": "티로신 키나아제 도메인을 항시 활성화시키는 변이로, 엑손19 결실과 함께 EGFR 변이 폐암의 2대 변이(전체 EGFR 변이의 약 40%)입니다. 아시아인·비흡연자 폐선암에서 특히 흔합니다.",
"cancers": "비소세포폐암(폐선암) — 아시아인 폐선암의 약 40~50%, 서양인의 약 10~15%에서 EGFR 변이 발견",
"risk_info": "체세포 드라이버 변이 — 폐암 조직검사에서 발견 시 표적치료 대상 여부를 결정하는 핵심 바이오마커",
"outcomes": "EGFR-TKI(게피티닙, 엘로티닙, 오시머티닙) 반응률 약 70~80%. FLAURA 임상시험에서 오시머티닙 1차 치료 무진행 생존기간 18.9개월로 표준 치료(10.2개월) 대비 크게 개선. 치료 중 T790M 등 내성 변이가 생길 수 있어 추적 검사가 중요합니다.",
"mechanism": "EGFR은 세포 표면에 솟아 있는 '성장 신호 안테나'입니다. 정상일 때는 성장인자가 도착했을 때만 켜져서 '분열해도 좋다'는 신호를 안으로 전달합니다. 858번째 류신이 아르지닌으로 바뀌면(L858R) 안테나 내부의 스위치 구조가 뒤틀려, 신호가 오지 않아도 항상 켜진 상태가 됩니다. 폐 세포는 쉬지 않고 분열 명령을 받게 되고, 이것이 폐선암의 시작이 됩니다.",
"survival": "FLAURA 임상시험에서 오시머티닙 1차 치료 시 전체 생존기간 중앙값이 38.6개월로, 기존 항암화학요법 시대보다 크게 연장되었습니다. 뇌전이에도 효과가 있어 EGFR 변이 확인 시 표적치료가 표준입니다.",
"hotspot_info": "코돈 858(c.2572~2574, 화면의 ★ 표시)이 실제 핫스팟입니다. 엑손19 결실과 함께 EGFR 변이 폐암의 약 85%가 이 두 구역에 몰려 있습니다. 안테나의 스위치 잠금장치에 해당하는 자리라, 여기가 바뀌면 항상 켜짐 상태가 됩니다.",
"risk_percent": 90,
"risk_level": "매우 높음",
"risk_title": "암 관련 위험도 (드라이버 변이)",
"treatment": "높음 (EGFR-TKI 표적치료)",
"approaches": [
{"name": "3세대 EGFR-TKI (오시머티닙)", "desc": "1차 표준치료 — 뇌전이에도 효과, PFS 18.9개월 (FLAURA)"},
{"name": "1·2세대 TKI (게피티닙·엘로티닙·아파티닙)", "desc": "반응률 약 70~80%의 경구 표적치료제"},
{"name": "내성 변이 추적 검사", "desc": "T790M·C797S 등 내성 변이 발생 시 치료 전략 변경"},
],
"sources": [
{"label": "ClinVar: EGFR L858R", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=EGFR+L858R"},
{"label": "NCBI Gene: EGFR", "url": "https://www.ncbi.nlm.nih.gov/gene/1956"},
],
},
},
{
"id": "msh2",
"gene": "MSH2",
"badge": "공공데이터 연계 샘플",
"refseq": "NM_000251",
"title": "MSH2 불일치 복구 유전자 — 린치증후군",
"desc": "c.1216C>T (Arg406Ter) — 단백질이 중간에서 끊기는 넌센스 변이. 유전성 대장암(린치증후군)의 원인입니다. 표시 구간은 c.1171~1260입니다.",
"normal": MSH2_NORMAL,
"mutant": MSH2_MUTANT,
"offset": 1171,
"hotspots": [46, 47, 48], # 코돈 406 (R406X)
"known_variant": {"positions": [46], "name": "c.1216C>T (p.Arg406Ter, R406X)"},
"clinical": {
"variant_name": "MSH2 c.1216C>T (Arg406Ter, R406X) — ClinVar 병원성(Pathogenic)",
"summary": "아미노산을 지정하던 코돈(CGA)이 종결 신호(TGA)로 바뀌는 넌센스 변이입니다. 단백질이 절반만 만들어지다 끊겨 DNA 오류 교정 기능을 완전히 잃습니다. 이 유전자의 변이가 유전성 대장암인 린치증후군을 일으키며, 대장암 환자의 면역병리 HMSH2 검사가 바로 이 단백질이 존재하는지(intact) 사라졌는지(loss)를 확인하는 검사입니다.",
"cancers": "대장암, 자궁내막암, 위암, 난소암 (린치증후군 관련 암)",
"risk_info": "린치증후군 보인자의 대장암 평생 발병 위험 약 50~80% (일반인 약 5%). 전체 대장암의 약 2~4%가 린치증후군에 의한 것으로 추정됩니다.",
"outcomes": "MSH2 기능이 사라진 암은 돌연변이가 매우 많아(고빈도 미소부수체 불안정성, MSI-H) 오히려 면역항암제가 잘 듣습니다. KEYNOTE-177 임상시험에서 펨브롤리주맙이 무진행 생존기간 16.5개월로 항암화학요법(8.2개월)의 2배를 기록했습니다.",
"mechanism": "MSH2는 DNA가 복제될 때 생긴 '오타'를 찾아내는 교정 담당 단백질입니다. c.1216 위치의 C가 T로 바뀌면 406번째 아미노산 자리에 '여기서 번역을 멈춰라'는 종결 신호가 생겨, 934개 아미노산짜리 단백질이 405개까지만 만들어지다 끊깁니다. 교정 기능이 사라진 세포는 분열할 때마다 오타(돌연변이)가 수백 배 빠르게 쌓이고, 그 오타가 성장 조절 유전자를 망가뜨리면 대장암이 됩니다.",
"survival": "린치증후군 보인자가 1~2년마다 대장내시경 검진을 받으면 대장암 사망률이 크게 감소합니다. 한국 대장암 5년 상대생존율은 약 74%이며, MSI-H 대장암은 면역항암제 반응이 좋아 진행성이라도 장기 생존 사례가 늘고 있습니다.",
"hotspot_info": "코돈 406(c.1216~1218, 화면의 ★ 표시)은 CGA 코돈입니다. C 다음에 G가 오는 자리(CpG 부위)는 화학적으로 C가 T로 바뀌기 쉬워서, DNA에서 돌연변이가 유난히 자주 생기는 대표적인 핫스팟입니다. CGA가 TGA(종결 신호)로 바뀌면 단백질이 중간에서 끊깁니다.",
"risk_percent": 70,
"risk_level": "매우 높음",
"risk_title": "평생 대장암 발병 위험도 (보인자)",
"treatment": "높음 (정기 내시경·면역항암제)",
"approaches": [
{"name": "정기 대장내시경 감시", "desc": "1~2년 간격 검진으로 전암 단계 용종을 제거 — 사망률을 크게 낮추는 핵심 전략"},
{"name": "면역항암제 (펨브롤리주맙)", "desc": "MSI-H 대장암 1차 치료 — KEYNOTE-177에서 화학요법 대비 무진행 생존 2배"},
{"name": "가족 유전자 검사", "desc": "보인자의 부모·형제·자녀는 50% 확률로 같은 변이 보유 — 조기 검진 대상"},
],
"sources": [
{"label": "ClinVar: MSH2 c.1216C>T", "url": "https://www.ncbi.nlm.nih.gov/clinvar/?term=MSH2+c.1216C%3ET"},
{"label": "NCBI Gene: MSH2", "url": "https://www.ncbi.nlm.nih.gov/gene/4436"},
],
},
},
]
SAMPLES_BY_ID = {s["id"]: s for s in SAMPLES}
# ------------------------------------------------------------------
# 분석 로직 (기존 콘솔 프로그램의 로직을 계승·확장)
# ------------------------------------------------------------------
def gc_content(seq):
"""GC 함량(%) 계산"""
if not seq:
return 0.0
gc = seq.count("G") + seq.count("C")
return round(gc / len(seq) * 100, 1)
def validate_sequence(seq):
"""A, T, G, C 이외의 문자 검출"""
return [ch for ch in set(seq) if ch not in "ATGC"]
def mutation_finder_with(normal, mutant, hotspots, offset):
"""두 서열 비교 — 로컬 위치·CDS(c.) 위치·핫스팟 여부 포함"""
mutations = []
for i in range(min(len(normal), len(mutant))):
if normal[i] != mutant[i]:
pos = i + 1
mutations.append({
"position": pos,
"c_position": pos + offset - 1,
"normal_base": normal[i],
"mutant_base": mutant[i],
"is_hotspot": pos in hotspots,
})
return mutations
def analyze(normal, mutant, sample_id=None):
normal = "".join(normal.split()).upper()
mutant = "".join(mutant.split()).upper()
errors = []
bad_n = validate_sequence(normal)
bad_m = validate_sequence(mutant)
if not normal or not mutant:
errors.append("서열을 입력해 주세요.")
if bad_n:
errors.append("정상 서열에 허용되지 않는 문자가 있습니다: " + ", ".join(sorted(bad_n)))
if bad_m:
errors.append("돌연변이 서열에 허용되지 않는 문자가 있습니다: " + ", ".join(sorted(bad_m)))
if errors:
return {"ok": False, "errors": errors}
warnings = []
if len(normal) != len(mutant):
warnings.append(
"두 서열의 길이가 다릅니다 (정상 {}bp / 돌연변이 {}bp). "
"짧은 쪽 길이까지만 비교합니다.".format(len(normal), len(mutant))
)
# 샘플 정보 (핫스팟·CDS 좌표·임상 데이터)
sample = SAMPLES_BY_ID.get(sample_id) if sample_id else None
hotspots = sample["hotspots"] if sample else HOTSPOT
offset = sample["offset"] if sample else 1
mutations = mutation_finder_with(normal, mutant, hotspots, offset)
hotspot_found = any(m["is_hotspot"] for m in mutations)
# 기본 위험도 계산: 변이 1개당 10%, 핫스팟 변이 1개당 +20%, 최대 100%
risk = len(mutations) * 10
for m in mutations:
if m["is_hotspot"]:
risk += 20
risk = min(risk, 100)
if risk >= 80:
level, treatment = "매우 높음", "높음"
elif risk >= 50:
level, treatment = "높음", "중간~높음"
elif risk >= 20:
level, treatment = "중간", "중간"
else:
level, treatment = "낮음", "낮음"
risk_title = "암 관련 위험도"
approaches = [
{"name": "정상 유전자 도입", "desc": "정상 유전자를 세포에 전달합니다."},
{"name": "CRISPR 유전자 교정", "desc": "돌연변이를 직접 교정합니다."},
{"name": "표적 약물 활용", "desc": "남아 있는 단백질 기능을 강화하거나 변이 단백질을 억제합니다."},
]
# 알려진 임상 변이가 실제로 검출된 경우 → 문헌 기반 임상 데이터로 대체
clinical = None
variant_detected = False
if sample:
clinical = dict(sample.get("clinical") or {})
kv = sample.get("known_variant")
if kv:
found_positions = {m["position"] for m in mutations}
variant_detected = set(kv["positions"]).issubset(found_positions)
if variant_detected:
risk = clinical.get("risk_percent", risk)
level = clinical.get("risk_level", level)
treatment = clinical.get("treatment", treatment)
risk_title = clinical.get("risk_title", risk_title)
if clinical.get("approaches"):
approaches = clinical["approaches"]
clinical["variant_detected"] = variant_detected
clinical["known_variant_name"] = kv["name"] if kv else None
return {
"ok": True,
"normal": normal,
"mutant": mutant,
"warnings": warnings,
"mutations": mutations,
"mutation_count": len(mutations),
"hotspot": hotspots,
"hotspot_found": hotspot_found,
"offset": offset,
"risk": risk,
"risk_level": level,
"risk_title": risk_title,
"treatment_priority": treatment,
"approaches": approaches,
"clinical": clinical,
"sample_gene": sample["gene"] if sample else None,
"stats": {
"normal_length": len(normal),
"mutant_length": len(mutant),
"normal_gc": gc_content(normal),
"mutant_gc": gc_content(mutant),
},
}
# ------------------------------------------------------------------
# HTTP 서버
# ------------------------------------------------------------------
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=FRONTEND_DIR, **kwargs)
def _send_json(self, data, status=200):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = urlparse(self.path).path
if path == "/report":
self.path = "/report.html"
super().do_GET()
elif path == "/analyzer":
self.path = "/analyzer.html"
super().do_GET()
elif path == "/api/samples":
self._send_json({"ok": True, "samples": SAMPLES,
"default": {"normal": DEFAULT_NORMAL, "mutant": DEFAULT_MUTANT}})
elif path == "/api/health":
self._send_json({"ok": True, "python": sys.version})
else:
super().do_GET()
def do_POST(self):
path = urlparse(self.path).path
if path == "/api/analyze":
try:
length = int(self.headers.get("Content-Length", 0))
data = json.loads(self.rfile.read(length).decode("utf-8"))
result = analyze(data.get("normal", ""), data.get("mutant", ""),
data.get("sample_id"))
self._send_json(result)
except (ValueError, json.JSONDecodeError):
self._send_json({"ok": False, "errors": ["잘못된 요청 형식입니다."]}, 400)
else:
self._send_json({"ok": False, "errors": ["존재하지 않는 API 경로입니다."]}, 404)
def log_message(self, fmt, *args):
sys.stdout.write("[서버] %s\n" % (fmt % args))
def main():
print("=" * 55)
print(" p53 유전자 돌연변이 분석 시스템 - 백엔드 서버")
print("=" * 55)
print(" Python : %s" % sys.version.split()[0])
print(" 주소 : http://localhost:%d" % PORT)
print(" 종료 : Ctrl + C")
print("=" * 55)
server = ThreadingHTTPServer((HOST, PORT), Handler)
if OPEN_BROWSER:
try:
webbrowser.open("http://localhost:%d" % PORT)
except Exception:
pass
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n서버를 종료합니다.")
server.shutdown()
if __name__ == "__main__":
main()
유전자 돌연변이 분석 화면. 샘플 카드, 서열 입력, 3D 이중나선, 서열 비교, 분석 결과, 임상 정보 카드 영역으로 구성됩니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p53 유전자 돌연변이 분석 시스템</title>
<link rel="stylesheet" href="css/style.css?v=20260907a">
</head>
<body>
<!-- NCBI 스타일 상단 헤더 -->
<header class="ncbi-header">
<div class="container header-inner">
<div class="logo-area">
<div class="dna-logo">🧬</div>
<div>
<h1>유전자 돌연변이 분석 시스템</h1>
<p class="subtitle">Gene Mutation Analysis · p53 종양억제 유전자</p>
</div>
</div>
<nav class="header-nav">
<a href="/" class="home-btn" data-home>🏠 홈</a>
<a href="#input-section">서열 입력</a>
<a href="#compare-section">서열 비교</a>
<a href="#result-section">분석 결과</a>
<a href="https://www.ncbi.nlm.nih.gov/" target="_blank" rel="noopener">NCBI ↗</a>
</nav>
</div>
</header>
<!-- NCBI 스타일 검색바 영역 -->
<div class="ncbi-searchbar">
<div class="container searchbar-inner">
<span class="db-label">데이터베이스 : 뉴클레오타이드 (Nucleotide)</span>
<span class="server-status" id="server-status">백엔드 연결 확인 중...</span>
</div>
</div>
<main class="container">
<!-- 공공데이터 연계 사이트 바로가기 -->
<a class="dna2-banner" href="/dna2/">
<div class="dna2-banner-text">
<span class="dna2-badge">NEW</span>
<strong>공공데이터 연계판 — 대장암 면역병리 HMSH2 검사 통계</strong>
<span class="dna2-desc">국립암센터 공공데이터포털 오픈API 실시간 연동 · 환자 26,000여 명 실제 집계 보기</span>
</div>
<span class="dna2-arrow">→</span>
</a>
<!-- 샘플 서열 (NCBI 참고) -->
<section class="panel" id="sample-section">
<div class="panel-head">
<h2>📚 NCBI 참고 샘플 서열</h2>
<span class="panel-note">카드를 클릭하면 서열이 입력창에 자동으로 채워집니다</span>
</div>
<div class="sample-grid" id="sample-grid">
<div class="loading">샘플 서열을 불러오는 중...</div>
</div>
</section>
<!-- 서열 입력 -->
<section class="panel" id="input-section">
<div class="panel-head">
<h2>🔬 염기서열 입력</h2>
<span class="panel-note">A, T, G, C 문자만 입력 가능 (공백·줄바꿈은 자동 제거)</span>
</div>
<div class="input-grid">
<div class="input-block">
<label for="normal-input">정상 서열 (Normal)</label>
<textarea id="normal-input" spellcheck="false"
placeholder="예) ATGGAGGAGCCGCAG..."></textarea>
<div class="input-meta"><span id="normal-count">0 bp</span></div>
</div>
<div class="input-block">
<label for="mutant-input">돌연변이 서열 (Mutant)</label>
<textarea id="mutant-input" spellcheck="false"
placeholder="예) ATGGAGGAGTCGCAG..."></textarea>
<div class="input-meta"><span id="mutant-count">0 bp</span></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="analyze-btn">🧪 돌연변이 분석 실행</button>
<button class="btn btn-secondary" id="default-btn">기본 p53 서열 불러오기</button>
<button class="btn btn-ghost" id="clear-btn">지우기</button>
</div>
</section>
<!-- 염기 색상 범례 -->
<section class="panel legend-panel">
<div class="legend">
<span class="legend-title">염기 색상 안내 :</span>
<span class="legend-item"><span class="base base-A">A</span> 아데닌 (Adenine)</span>
<span class="legend-item"><span class="base base-T">T</span> 티민 (Thymine)</span>
<span class="legend-item"><span class="base base-G">G</span> 구아닌 (Guanine)</span>
<span class="legend-item"><span class="base base-C">C</span> 사이토신 (Cytosine)</span>
<span class="legend-item"><span class="base base-mut">X</span> <strong class="red-text">돌연변이 (불일치 염기)</strong></span>
<span class="legend-item"><span class="base base-hotspot-mark">★</span> 핫스팟 위치</span>
</div>
<p class="hotspot-note"><strong>핫스팟(★)이란?</strong>
DNA에서 돌연변이가 유난히 자주 일어나는 "사고 다발 구역"입니다. 도로에 사고 다발 지점이 있듯이,
유전자에도 화학적 성질이나 단백질 기능상 중요도 때문에 변이가 몰리는 위치가 있습니다.
같은 유전자라도 핫스팟에서 생긴 변이는 질병 위험이 훨씬 크므로, 이 프로그램은 핫스팟 변이에 위험도 가중치(+20%)를 둡니다.
유전자별 실제 핫스팟 설명은 분석 결과의 임상 정보 카드에 표시됩니다.</p>
</section>
<!-- DNA 이중나선 3D 보기 -->
<section class="panel" id="helix-section" hidden>
<div class="panel-head">
<h2>🧬 DNA 이중나선 3D 보기</h2>
<span class="panel-note">마우스로 드래그 = 상하·좌우 회전 · 휠 = 확대/축소</span>
</div>
<div class="helix-style-picker">
<span class="picker-label">모델 선택 :</span>
<button type="button" class="helix-style-btn active" data-style="standard">기본 모델</button>
<button type="button" class="helix-style-btn" data-style="cartoon">카툰 모델</button>
<button type="button" class="helix-style-btn" data-style="neon">네온 홀로그램</button>
</div>
<div class="helix-info">
💡 입력한 서열은 DNA의 <strong>한쪽 가닥(5'→3')</strong>입니다.
반대쪽 <strong>상보 가닥(3'→5')</strong>은 염기쌍 규칙 <strong>A-T, G-C</strong>에 따라
자동으로 생성되어 이중나선으로 표시됩니다.
(NCBI 등 유전자 데이터베이스도 한쪽 가닥만 기록하는 것이 표준 방식입니다)
</div>
<div class="helix-grid">
<div class="helix-box">
<div class="helix-title title-normal">정상 DNA</div>
<canvas id="helix-normal"></canvas>
</div>
<div class="helix-box">
<div class="helix-title title-mutant">돌연변이 DNA</div>
<canvas id="helix-mutant"></canvas>
</div>
</div>
<div class="legend" style="margin-top:10px">
<span class="legend-title">염기쌍 :</span>
<span class="legend-item"><span class="base base-A">A</span>-<span class="base base-T">T</span> 아데닌-티민</span>
<span class="legend-item"><span class="base base-G">G</span>-<span class="base base-C">C</span> 구아닌-사이토신</span>
<span class="legend-item"><span class="base base-mut">X</span> <strong class="red-text">돌연변이 염기쌍</strong></span>
</div>
</section>
<!-- 서열 비교 -->
<section class="panel" id="compare-section" hidden>
<div class="panel-head">
<h2>🧬 정상 vs 돌연변이 서열 비교</h2>
<span class="panel-note">빨간색 = 서로 다른 염기 (돌연변이 위치)</span>
</div>
<div id="compare-view" class="compare-view"></div>
</section>
<!-- 분석 결과 -->
<section class="panel" id="result-section" hidden>
<div class="panel-head">
<h2>📊 p53 돌연변이 분석 결과</h2>
</div>
<div id="warnings-box"></div>
<div class="result-grid">
<div class="stat-card">
<div class="stat-label">발견된 변이 수</div>
<div class="stat-value" id="stat-count">-</div>
</div>
<div class="stat-card">
<div class="stat-label" id="risk-title">암 관련 위험도</div>
<div class="stat-value" id="stat-risk">-</div>
<div class="risk-bar"><div class="risk-fill" id="risk-fill"></div></div>
</div>
<div class="stat-card">
<div class="stat-label">위험 등급</div>
<div class="stat-value" id="stat-level">-</div>
</div>
<div class="stat-card">
<div class="stat-label">유전자 치료 우선순위</div>
<div class="stat-value" id="stat-treatment">-</div>
</div>
</div>
<!-- 임상 정보 -->
<div id="clinical-box" hidden>
<h3 class="sub-title">🏥 임상 정보 (NCBI ClinVar·문헌 기반)</h3>
<div class="clinical-card">
<div class="clinical-variant" id="clinical-variant"></div>
<p class="clinical-summary" id="clinical-summary"></p>
<table class="ncbi-table small clinical-table">
<tbody>
<tr><td class="th-cell">관련 암종</td><td id="clinical-cancers"></td></tr>
<tr><td class="th-cell">발병 위험</td><td id="clinical-risk"></td></tr>
<tr><td class="th-cell">인체에 미치는 영향</td><td id="clinical-mechanism"></td></tr>
<tr><td class="th-cell">핫스팟 설명</td><td id="clinical-hotspot"></td></tr>
<tr><td class="th-cell">임상 결과·치료</td><td id="clinical-outcomes"></td></tr>
<tr><td class="th-cell">생존율·예후</td><td id="clinical-survival"></td></tr>
</tbody>
</table>
<div class="clinical-sources" id="clinical-sources"></div>
</div>
</div>
<div class="two-col">
<div>
<h3 class="sub-title">📍 변이 위치 상세</h3>
<table class="ncbi-table" id="mutation-table">
<thead>
<tr><th>위치</th><th>정상 염기</th><th></th><th>변이 염기</th><th>핫스팟</th></tr>
</thead>
<tbody></tbody>
</table>
<p id="no-mutation-msg" class="ok-text" hidden>✅ 돌연변이가 발견되지 않았습니다.</p>
</div>
<div>
<h3 class="sub-title">💊 가능한 치료 접근법</h3>
<ol class="approach-list" id="approach-list"></ol>
<h3 class="sub-title">📈 서열 통계</h3>
<table class="ncbi-table small">
<tbody id="stats-body"></tbody>
</table>
</div>
</div>
</section>
</main>
<footer class="ncbi-footer">
<div class="container">
<p>본 프로그램은 교육·발표용 시뮬레이션입니다. 실제 임상 진단에 사용할 수 없습니다.</p>
<p>참고 : <a href="https://www.ncbi.nlm.nih.gov/" target="_blank" rel="noopener">NCBI (미국 국립생물공학정보센터)</a> · TP53 RefSeq NM_000546 · <a href="/report">작업 보고서 (전체 소스코드)</a></p>
</div>
</footer>
<script src="js/app.js?v=20260907a"></script>
<script>
/* 홈 버튼: 인터넷에서는 사이트 루트(/), 로컬에서는 메인 서버(localhost:8000)로 이동 */
(function () {
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
if (location.port && location.port !== "8000") {
var links = document.querySelectorAll("a[data-home]");
for (var i = 0; i < links.length; i++) links[i].href = "http://localhost:8000/";
}
}
})();
</script>
</body>
</html>
NCBI 색상 팔레트(남색 헤더)와 염기별 고유 색상(A 초록, T 주황, G 보라, C 파랑, 돌연변이 빨강)을 정의합니다.
/* ==========================================================
p53 유전자 돌연변이 분석 시스템 - NCBI 참고 스타일
========================================================== */
:root {
/* NCBI 색상 팔레트 참고 */
--ncbi-blue: #205493;
--ncbi-blue-dark: #14315c;
--ncbi-blue-light: #e8f0fa;
--ncbi-gray-bg: #f5f6f7;
--ncbi-border: #d1d5da;
--text-main: #212121;
--text-sub: #5b616b;
/* 염기별 색상 (빨간색은 돌연변이 전용) */
--color-A: #1a9850; /* 아데닌 - 초록 */
--color-T: #e69f00; /* 티민 - 주황 */
--color-G: #7b3fa0; /* 구아닌 - 보라 */
--color-C: #0072b2; /* 사이토신 - 파랑 */
--color-mut: #d32f2f; /* 돌연변이 - 빨강 */
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Malgun Gothic", "맑은 고딕", "Segoe UI", Arial, sans-serif;
background: var(--ncbi-gray-bg);
color: var(--text-main);
line-height: 1.6;
}
.container { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
/* ---------------- 헤더 (NCBI 상단 바 스타일) ---------------- */
.ncbi-header {
background: linear-gradient(180deg, var(--ncbi-blue) 0%, var(--ncbi-blue-dark) 100%);
color: #fff;
border-bottom: 4px solid #0b1f3a;
}
.header-inner {
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px; padding: 14px 16px;
}
.logo-area { display: flex; align-items: center; gap: 14px; }
.dna-logo {
font-size: 34px; background: rgba(255,255,255,.15);
border-radius: 8px; padding: 6px 10px;
}
.ncbi-header h1 { font-size: 21px; font-weight: 700; letter-spacing: -0.5px; }
.subtitle { font-size: 12.5px; color: #cfe0f5; }
.header-nav { display: flex; gap: 4px; flex-wrap: wrap; }
.header-nav a {
color: #eaf2fc; text-decoration: none; font-size: 13.5px;
padding: 7px 12px; border-radius: 4px; transition: background .15s;
}
.header-nav a:hover { background: rgba(255,255,255,.18); }
/* 홈 버튼 (메인으로 이동) */
.header-nav a.home-btn {
background: rgba(255,255,255,.18);
border: 1px solid rgba(255,255,255,.45);
font-weight: 700; margin-right: 4px;
}
.header-nav a.home-btn:hover { background: rgba(255,255,255,.32); }
/* ---------------- 검색바 영역 (NCBI 회색 보조 바) ---------------- */
.ncbi-searchbar { background: #e9edf2; border-bottom: 1px solid var(--ncbi-border); }
.searchbar-inner {
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 8px; padding: 8px 16px; font-size: 13px;
}
.db-label { color: var(--ncbi-blue-dark); font-weight: 600; }
.server-status { color: var(--text-sub); }
.server-status.ok { color: #1a7f37; font-weight: 600; }
.server-status.fail { color: var(--color-mut); font-weight: 600; }
/* ---------------- 공공데이터 연계 사이트 배너 ---------------- */
.dna2-banner {
display: flex; align-items: center; justify-content: space-between; gap: 14px;
background: linear-gradient(90deg, var(--ncbi-blue-dark), var(--ncbi-blue));
border: 1px solid #0b1f3a; border-radius: 8px;
padding: 16px 22px; margin: 18px 0 0; text-decoration: none; color: #fff;
box-shadow: 0 2px 8px rgba(20,49,92,.25);
transition: transform .12s, box-shadow .12s;
}
.dna2-banner:hover {
transform: translateY(-2px);
box-shadow: 0 5px 14px rgba(20,49,92,.35);
}
.dna2-banner-text { display: flex; flex-direction: column; gap: 3px; }
.dna2-banner-text strong { font-size: 16px; }
.dna2-badge {
align-self: flex-start; background: #ffcf40; color: #5c4400;
font-size: 11px; font-weight: 700; border-radius: 9px; padding: 1px 9px;
}
.dna2-desc { font-size: 12.5px; color: #cfe0f5; }
.dna2-arrow { font-size: 26px; font-weight: 700; color: #9ec5f0; }
/* ---------------- 패널 공통 ---------------- */
.panel {
background: #fff; border: 1px solid var(--ncbi-border);
border-radius: 6px; margin: 18px 0; padding: 18px 20px;
box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
.panel-head {
display: flex; align-items: baseline; justify-content: space-between;
flex-wrap: wrap; gap: 6px;
border-bottom: 2px solid var(--ncbi-blue-light);
padding-bottom: 10px; margin-bottom: 14px;
}
.panel-head h2 { font-size: 17px; color: var(--ncbi-blue-dark); }
.panel-note { font-size: 12.5px; color: var(--text-sub); }
.sub-title { font-size: 15px; color: var(--ncbi-blue-dark); margin: 14px 0 8px; }
/* ---------------- 샘플 카드 ---------------- */
.sample-grid {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
@media (max-width: 900px) { .sample-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .sample-grid { grid-template-columns: 1fr; } }
.sample-card {
border: 1px solid var(--ncbi-border); border-radius: 6px;
padding: 12px 14px; cursor: pointer; background: #fbfcfd;
transition: border-color .15s, box-shadow .15s, transform .1s;
}
.sample-card:hover {
border-color: var(--ncbi-blue); box-shadow: 0 2px 8px rgba(32,84,147,.18);
transform: translateY(-2px);
}
.sample-card.active { border-color: var(--ncbi-blue); background: var(--ncbi-blue-light); }
.sample-gene { font-weight: 700; color: var(--ncbi-blue); font-size: 14.5px; }
.sample-badge {
display: inline-block; margin-left: 6px; vertical-align: middle;
background: #1a7f37; color: #fff; border-radius: 10px;
font-size: 10.5px; font-weight: 700; padding: 2px 9px; letter-spacing: -0.3px;
}
.sample-refseq {
font-family: Consolas, monospace; font-size: 11.5px; color: var(--text-sub);
background: #eef1f4; border-radius: 3px; padding: 1px 6px; display: inline-block; margin: 4px 0;
}
.sample-desc { font-size: 12px; color: var(--text-sub); line-height: 1.45; }
.loading { color: var(--text-sub); font-size: 13.5px; }
/* ---------------- 입력 영역 ---------------- */
.input-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 760px) { .input-grid { grid-template-columns: 1fr; } }
.input-block label {
display: block; font-weight: 700; font-size: 13.5px;
color: var(--ncbi-blue-dark); margin-bottom: 6px;
}
.input-block textarea {
width: 100%; height: 96px; resize: vertical;
font-family: Consolas, "Courier New", monospace; font-size: 14px;
letter-spacing: 1px; padding: 10px 12px;
border: 1px solid var(--ncbi-border); border-radius: 5px;
outline: none; transition: border-color .15s, box-shadow .15s;
text-transform: uppercase;
}
.input-block textarea:focus {
border-color: var(--ncbi-blue); box-shadow: 0 0 0 3px rgba(32,84,147,.15);
}
.input-meta { text-align: right; font-size: 12px; color: var(--text-sub); margin-top: 4px; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.btn {
border: none; border-radius: 5px; padding: 10px 20px;
font-size: 14px; font-weight: 700; cursor: pointer;
font-family: inherit; transition: filter .15s, transform .1s;
}
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--ncbi-blue); color: #fff; }
.btn-secondary { background: #e8f0fa; color: var(--ncbi-blue-dark); border: 1px solid #b7cbe4; }
.btn-ghost { background: transparent; color: var(--text-sub); border: 1px solid var(--ncbi-border); }
/* ---------------- 염기 색상 ---------------- */
.legend-panel { padding: 12px 20px; }
.legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 13px; }
.legend-title { font-weight: 700; color: var(--ncbi-blue-dark); }
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.red-text { color: var(--color-mut); }
.base {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 26px; border-radius: 4px;
font-family: Consolas, "Courier New", monospace;
font-weight: 700; font-size: 15px; color: #fff;
}
.base-A { background: var(--color-A); }
.base-T { background: var(--color-T); }
.base-G { background: var(--color-G); }
.base-C { background: var(--color-C); }
.base-N { background: #9aa0a6; }
.base-mut {
background: var(--color-mut);
box-shadow: 0 0 0 2px #ffd7d7;
animation: mut-pulse 1.6s ease-in-out infinite;
}
@keyframes mut-pulse {
0%, 100% { box-shadow: 0 0 0 2px #ffd7d7; }
50% { box-shadow: 0 0 0 4px #ffb3b3; }
}
.base-hotspot-mark { background: #fff3cd; color: #8a6d00; box-shadow: 0 0 0 1px #e0c869; }
.hotspot-note {
margin-top: 10px; padding: 10px 14px; font-size: 12.5px; line-height: 1.6;
background: #fffaf0; border: 1px solid #e0c869; border-radius: 6px; color: #5c4a00;
}
/* ---------------- DNA 이중나선 3D ---------------- */
.helix-info {
background: var(--ncbi-blue-light); border: 1px solid #b7cbe4;
border-radius: 5px; padding: 10px 14px; margin-bottom: 14px;
font-size: 13px; color: var(--ncbi-blue-dark); line-height: 1.55;
}
.helix-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
@media (max-width: 760px) { .helix-grid { grid-template-columns: 1fr; } }
.helix-box {
border: 1px solid var(--ncbi-border); border-radius: 6px;
background: linear-gradient(180deg, #f8fbff 0%, #eef3fa 100%);
overflow: hidden;
}
.helix-title {
text-align: center; font-weight: 700; font-size: 14px;
padding: 8px; border-bottom: 1px solid var(--ncbi-border); background: #fff;
}
.helix-title.title-normal { color: var(--ncbi-blue); }
.helix-title.title-mutant { color: #a04000; }
.helix-box canvas {
display: block; width: 100%; height: 460px;
cursor: grab; touch-action: none;
}
.helix-box canvas:active { cursor: grabbing; }
/* ---------------- 3D 모델 선택 버튼 ---------------- */
.helix-style-picker {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-bottom: 12px;
}
.picker-label { font-size: 13px; font-weight: 700; color: var(--ncbi-blue-dark); }
.helix-style-btn {
border: 1px solid var(--ncbi-border); background: #fff; color: var(--text-sub);
border-radius: 16px; padding: 6px 16px; font-size: 13px; font-weight: 700;
font-family: inherit; cursor: pointer; transition: all .15s;
}
.helix-style-btn:hover { border-color: var(--ncbi-blue); color: var(--ncbi-blue); }
.helix-style-btn.active {
background: var(--ncbi-blue); border-color: var(--ncbi-blue); color: #fff;
}
/* ---------------- 서열 비교 뷰 ---------------- */
.compare-view { overflow-x: auto; }
.seq-row-group {
border: 1px solid var(--ncbi-border); border-radius: 6px;
padding: 10px 14px; margin-bottom: 12px; background: #fbfcfd;
}
.seq-line { display: flex; align-items: center; gap: 3px; margin: 3px 0; white-space: nowrap; }
.seq-label {
width: 74px; min-width: 74px; font-size: 12px; font-weight: 700;
color: var(--text-sub);
}
.seq-label.normal-label { color: var(--ncbi-blue); }
.seq-label.mutant-label { color: #a04000; }
.pos-ruler {
display: flex; gap: 3px; margin-left: 77px; margin-bottom: 2px;
}
.pos-cell {
width: 24px; min-width: 24px; text-align: center;
font-size: 10px; color: var(--text-sub);
font-family: Consolas, monospace;
}
.pos-cell.hotspot-pos { color: #8a6d00; font-weight: 700; }
.match-line { margin-left: 77px; display: flex; gap: 3px; }
.match-cell {
width: 24px; min-width: 24px; text-align: center;
font-size: 12px; color: #b0b6bc; font-family: Consolas, monospace;
}
.match-cell.mismatch { color: var(--color-mut); font-weight: 700; }
/* ---------------- 결과 카드 ---------------- */
.result-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px; margin-bottom: 18px;
}
.stat-card {
border: 1px solid var(--ncbi-border); border-left: 4px solid var(--ncbi-blue);
border-radius: 6px; padding: 12px 16px; background: #fbfcfd;
}
.stat-label { font-size: 12.5px; color: var(--text-sub); margin-bottom: 4px; }
.stat-value { font-size: 22px; font-weight: 700; color: var(--ncbi-blue-dark); }
.stat-value.danger { color: var(--color-mut); }
.stat-value.warn { color: #b45f06; }
.stat-value.safe { color: #1a7f37; }
.risk-bar {
height: 8px; background: #e6e9ec; border-radius: 4px; margin-top: 8px; overflow: hidden;
}
.risk-fill {
height: 100%; width: 0; border-radius: 4px;
background: linear-gradient(90deg, #1a9850, #e69f00, #d32f2f);
transition: width .8s ease;
}
.two-col { display: grid; grid-template-columns: 1.2fr 1fr; gap: 24px; }
@media (max-width: 760px) { .two-col { grid-template-columns: 1fr; } }
/* ---------------- 테이블 (NCBI 스타일) ---------------- */
.ncbi-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.ncbi-table th {
background: var(--ncbi-blue-light); color: var(--ncbi-blue-dark);
text-align: left; padding: 8px 10px; border: 1px solid var(--ncbi-border);
}
.ncbi-table td { padding: 7px 10px; border: 1px solid var(--ncbi-border); }
.ncbi-table tbody tr:nth-child(even) { background: #f7f9fb; }
.ncbi-table.small td { font-size: 12.5px; }
.arrow-cell { color: var(--text-sub); font-weight: 700; text-align: center; }
.hotspot-badge {
background: #fff3cd; color: #8a6d00; border: 1px solid #e0c869;
border-radius: 10px; font-size: 11.5px; padding: 1px 8px; font-weight: 700;
}
/* ---------------- 임상 정보 카드 ---------------- */
.clinical-card {
border: 1px solid #b7cbe4; border-left: 4px solid var(--ncbi-blue);
border-radius: 6px; background: #f6faff;
padding: 14px 16px; margin-bottom: 18px;
}
.clinical-variant {
font-weight: 700; font-size: 14.5px; color: var(--ncbi-blue-dark);
margin-bottom: 6px;
}
.clinical-variant .detected-badge {
background: #d32f2f; color: #fff; border-radius: 10px;
font-size: 11.5px; padding: 2px 9px; margin-left: 8px; vertical-align: middle;
}
.clinical-summary { font-size: 13px; color: #333; margin-bottom: 10px; line-height: 1.55; }
.clinical-table { background: #fff; margin-bottom: 10px; }
.clinical-table .th-cell {
background: var(--ncbi-blue-light); font-weight: 700;
color: var(--ncbi-blue-dark); width: 110px; white-space: nowrap;
}
.clinical-sources { font-size: 12.5px; }
.clinical-sources a {
color: var(--ncbi-blue); margin-right: 14px; text-decoration: none;
}
.clinical-sources a:hover { text-decoration: underline; }
.approach-list { padding-left: 20px; font-size: 13.5px; }
.approach-list li { margin-bottom: 7px; }
.approach-list .approach-name { font-weight: 700; color: var(--ncbi-blue-dark); }
.approach-list .approach-desc { color: var(--text-sub); font-size: 12.5px; }
.ok-text { color: #1a7f37; font-weight: 700; font-size: 14px; margin-top: 8px; }
/* ---------------- 경고 박스 ---------------- */
.warning-box {
background: #fff8e6; border: 1px solid #e0c869; border-left: 4px solid #d4a017;
border-radius: 5px; padding: 10px 14px; margin-bottom: 12px;
font-size: 13px; color: #6d5400;
}
.error-box {
background: #fdecea; border: 1px solid #f0b3ae; border-left: 4px solid var(--color-mut);
border-radius: 5px; padding: 10px 14px; margin-bottom: 12px;
font-size: 13px; color: #8f1f1a;
}
.hotspot-alert {
background: #fff3cd; border: 1px solid #e0c869; border-radius: 5px;
padding: 10px 14px; margin-bottom: 12px; font-size: 13.5px; color: #6d5400;
font-weight: 600;
}
/* ---------------- 푸터 ---------------- */
.ncbi-footer {
background: var(--ncbi-blue-dark); color: #cfe0f5;
font-size: 12.5px; padding: 18px 0; margin-top: 30px; text-align: center;
}
.ncbi-footer a { color: #9ec5f0; }
서열 비교 렌더링, 순수 캔버스로 구현한 3D 이중나선(3종 모델·드래그 회전·휠 줌), 백엔드 API 호출과 결과 표시를 담당합니다.
/* ==========================================================
p53 유전자 돌연변이 분석 시스템 - 프론트엔드 로직
백엔드(Python) API: /api/samples, /api/analyze
========================================================== */
const $ = (sel) => document.querySelector(sel);
const normalInput = $("#normal-input");
const mutantInput = $("#mutant-input");
const PER_ROW = 30; // 한 줄에 표시할 염기 수
let DEFAULT_SEQ = { normal: "", mutant: "" };
let HOTSPOT = [];
let currentSampleId = null; // 선택된 샘플 (임상 데이터·핫스팟·c.좌표에 사용)
/* ---------------- 유틸 ---------------- */
function cleanSeq(text) {
return (text || "").replace(/\s+/g, "").toUpperCase();
}
function baseClass(ch) {
if ("ATGC".includes(ch)) return "base base-" + ch;
return "base base-N";
}
function updateCounts() {
$("#normal-count").textContent = cleanSeq(normalInput.value).length + " bp";
$("#mutant-count").textContent = cleanSeq(mutantInput.value).length + " bp";
}
/* ---------------- 서버 상태 확인 ---------------- */
async function checkServer() {
const el = $("#server-status");
try {
const res = await fetch("/api/health");
const data = await res.json();
el.textContent = "✅ 백엔드 연결됨 (Python " + data.python.split(" ")[0] + ")";
el.className = "server-status ok";
} catch (e) {
el.textContent = "❌ 백엔드에 연결할 수 없습니다. run.bat 으로 실행해 주세요.";
el.className = "server-status fail";
}
}
/* ---------------- 샘플 서열 불러오기 ---------------- */
async function loadSamples() {
const grid = $("#sample-grid");
try {
const res = await fetch("/api/samples");
const data = await res.json();
DEFAULT_SEQ = data.default;
grid.innerHTML = "";
data.samples.forEach((s) => {
const card = document.createElement("div");
card.className = "sample-card";
card.innerHTML =
'<div class="sample-gene">' + s.gene +
(s.badge ? '<span class="sample-badge">' + s.badge + "</span>" : "") + "</div>" +
'<span class="sample-refseq">' + s.refseq + "</span>" +
'<div class="sample-desc">' + s.desc + "</div>";
card.title = "클릭하면 " + s.gene + " 서열이 입력됩니다";
card.addEventListener("click", () => {
document.querySelectorAll(".sample-card").forEach((c) => c.classList.remove("active"));
card.classList.add("active");
currentSampleId = s.id;
normalInput.value = s.normal;
mutantInput.value = s.mutant;
updateCounts();
runAnalyze();
});
grid.appendChild(card);
});
} catch (e) {
grid.innerHTML = '<div class="loading">⚠️ 샘플을 불러오지 못했습니다. 백엔드 서버가 실행 중인지 확인해 주세요.</div>';
}
}
/* ---------------- 서열 비교 뷰 렌더링 ---------------- */
function renderCompare(normal, mutant) {
const view = $("#compare-view");
view.innerHTML = "";
const maxLen = Math.max(normal.length, mutant.length);
for (let start = 0; start < maxLen; start += PER_ROW) {
const end = Math.min(start + PER_ROW, maxLen);
const group = document.createElement("div");
group.className = "seq-row-group";
// 위치 눈금
let ruler = '<div class="pos-ruler">';
for (let i = start; i < end; i++) {
const pos = i + 1;
const isHot = HOTSPOT.includes(pos);
ruler += '<span class="pos-cell' + (isHot ? " hotspot-pos" : "") + '">' +
(isHot ? "★" : (pos % 5 === 0 || pos === 1 ? pos : "·")) + "</span>";
}
ruler += "</div>";
// 정상 서열 줄
let normalLine = '<div class="seq-line"><span class="seq-label normal-label">정상</span>';
for (let i = start; i < end; i++) {
const ch = normal[i] || "-";
const mismatch = normal[i] !== undefined && mutant[i] !== undefined && normal[i] !== mutant[i];
normalLine += '<span class="' + (mismatch ? "base base-mut" : baseClass(ch)) + '" title="위치 ' + (i + 1) + '">' + ch + "</span>";
}
normalLine += "</div>";
// 일치 표시 줄
let matchLine = '<div class="match-line">';
for (let i = start; i < end; i++) {
if (normal[i] === undefined || mutant[i] === undefined) {
matchLine += '<span class="match-cell"> </span>';
} else if (normal[i] === mutant[i]) {
matchLine += '<span class="match-cell">|</span>';
} else {
matchLine += '<span class="match-cell mismatch">✕</span>';
}
}
matchLine += "</div>";
// 돌연변이 서열 줄
let mutantLine = '<div class="seq-line"><span class="seq-label mutant-label">돌연변이</span>';
for (let i = start; i < end; i++) {
const ch = mutant[i] || "-";
const mismatch = normal[i] !== undefined && mutant[i] !== undefined && normal[i] !== mutant[i];
mutantLine += '<span class="' + (mismatch ? "base base-mut" : baseClass(ch)) + '" title="위치 ' + (i + 1) + '">' + ch + "</span>";
}
mutantLine += "</div>";
group.innerHTML = ruler + normalLine + matchLine + mutantLine;
view.appendChild(group);
}
}
/* ==========================================================
DNA 이중나선 3D 렌더러 (순수 캔버스, 외부 라이브러리 없음)
- 드래그 : 좌우(나선 회전) · 상하(기울이기)
- 휠 : 확대 / 축소
========================================================== */
const BASE_COLORS = { A: "#1a9850", T: "#e69f00", G: "#7b3fa0", C: "#0072b2", N: "#9aa0a6" };
const MUT_COLOR = "#d32f2f";
const COMPLEMENT = { A: "T", T: "A", G: "C", C: "G" };
function complementBase(ch) { return COMPLEMENT[ch] || "N"; }
let CURRENT_HELIX_STYLE = "standard"; // standard | cartoon | neon
class DnaHelix3D {
constructor(canvas, seq, mutSet) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.seq = seq;
this.mutSet = mutSet; // 0-기반 돌연변이 위치 Set
this.style = CURRENT_HELIX_STYLE;
this.rotY = 0.6; // 좌우 회전각
this.tilt = 0.35; // 상하 기울기
this.zoom = 1;
this.autoSpin = true;
this.dragging = false;
this.lastX = 0; this.lastY = 0;
this._resumeTimer = null;
this._bindEvents();
this._resize();
this._draw();
this._loop = this._loop.bind(this);
requestAnimationFrame(this._loop);
}
setSequence(seq, mutSet) {
this.seq = seq;
this.mutSet = mutSet;
this._draw(); // 화면 갱신이 멈춰 있어도 즉시 1회 그리기
}
setStyle(style) {
this.style = style;
this._draw();
}
_bindEvents() {
const c = this.canvas;
const start = (x, y) => {
this.dragging = true; this.autoSpin = false;
this.lastX = x; this.lastY = y;
clearTimeout(this._resumeTimer);
};
const move = (x, y) => {
if (!this.dragging) return;
this.rotY += (x - this.lastX) * 0.012; // 좌우 → 나선 회전
this.tilt += (y - this.lastY) * 0.008; // 상하 → 기울이기
this.tilt = Math.max(-1.4, Math.min(1.4, this.tilt));
this.lastX = x; this.lastY = y;
};
const end = () => {
this.dragging = false;
// 3초 후 자동 회전 재개
this._resumeTimer = setTimeout(() => { this.autoSpin = true; }, 3000);
};
c.addEventListener("mousedown", (e) => start(e.clientX, e.clientY));
window.addEventListener("mousemove", (e) => move(e.clientX, e.clientY));
window.addEventListener("mouseup", end);
c.addEventListener("touchstart", (e) => { e.preventDefault(); const t = e.touches[0]; start(t.clientX, t.clientY); }, { passive: false });
c.addEventListener("touchmove", (e) => { e.preventDefault(); const t = e.touches[0]; move(t.clientX, t.clientY); }, { passive: false });
c.addEventListener("touchend", end);
c.addEventListener("wheel", (e) => {
e.preventDefault();
this.zoom *= e.deltaY < 0 ? 1.1 : 0.9;
this.zoom = Math.max(0.4, Math.min(3.5, this.zoom));
}, { passive: false });
window.addEventListener("resize", () => this._resize());
}
_resize() {
const dpr = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = Math.max(1, rect.width * dpr);
this.canvas.height = Math.max(1, (rect.height || 460) * dpr);
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.W = rect.width;
this.H = rect.height || 460;
this._draw(); // 크기 변경은 캔버스를 비우므로 즉시 다시 그린다
}
_loop() {
if (this.autoSpin) this.rotY += 0.008;
this._draw();
requestAnimationFrame(this._loop);
}
_draw() {
const { ctx, W, H, seq } = this;
const style = this.style || "standard";
ctx.clearRect(0, 0, W, H);
if (!seq || !seq.length) return;
// 네온 홀로그램 모델: 어두운 우주 배경
if (style === "neon") {
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, "#0a1226"); bg.addColorStop(1, "#101c3a");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
}
// 모델별 색상표
const NEON_COLORS = { A: "#3dff8b", T: "#ffc14d", G: "#c77dff", C: "#4dc9ff", N: "#9aa0a6" };
const NEON_MUT = "#ff4d6d";
const baseColorOf = (ch, isMut) => {
if (style === "neon") return isMut ? NEON_MUT : (NEON_COLORS[ch] || NEON_COLORS.N);
return isMut ? MUT_COLOR : (BASE_COLORS[ch] || BASE_COLORS.N);
};
const n = seq.length;
const TWIST = (2 * Math.PI) / 10.5; // 10.5 bp당 1회전 (B-DNA)
const rise = Math.min(15, (H - 70) / Math.max(n - 1, 1)) * this.zoom;
const R = Math.min(52, W * 0.16) * this.zoom; // 나선 반지름
const cx = W / 2, cy = H / 2;
const f = 620; // 원근 거리
const cosT = Math.cos(this.tilt), sinT = Math.sin(this.tilt);
const project = (x, y, z) => {
const y2 = y * cosT - z * sinT;
const z2 = y * sinT + z * cosT;
const s = f / (f + z2 + R * 2);
return { x: cx + x * s, y: cy + y2 * s, z: z2, s: s };
};
const items = []; // {z, draw}
const pts1 = [], pts2 = [];
for (let i = 0; i < n; i++) {
const a = i * TWIST + this.rotY;
const y = (i - (n - 1) / 2) * rise;
const p1 = project(R * Math.cos(a), y, R * Math.sin(a));
const p2 = project(R * Math.cos(a + Math.PI), y, R * Math.sin(a + Math.PI));
pts1.push(p1); pts2.push(p2);
const isMut = this.mutSet.has(i);
const b1 = seq[i];
const b2 = complementBase(b1);
const c1 = baseColorOf(b1, isMut);
const c2 = baseColorOf(b2, isMut);
const mid = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
const zAvg = (p1.z + p2.z) / 2;
const showLetters = rise >= 8.5;
// 염기쌍 가로대 (rung)
items.push({ z: zAvg, draw: () => {
const lw = Math.max(2, (style === "cartoon" ? 7 : 4.5) * ((p1.s + p2.s) / 2));
ctx.lineCap = "round";
if (style === "cartoon") {
// 카툰: 두꺼운 검정 외곽선 먼저
ctx.strokeStyle = "#222";
ctx.lineWidth = lw + 4;
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
}
if (style === "neon") {
ctx.shadowBlur = isMut ? 18 : 12;
} else if (isMut) {
ctx.shadowColor = "rgba(211,47,47,.75)";
ctx.shadowBlur = 10;
}
if (style === "neon") ctx.shadowColor = c1;
ctx.strokeStyle = c1; ctx.lineWidth = lw;
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(mid.x, mid.y); ctx.stroke();
if (style === "neon") ctx.shadowColor = c2;
ctx.strokeStyle = c2;
ctx.beginPath(); ctx.moveTo(mid.x, mid.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
ctx.shadowBlur = 0;
if (showLetters) {
const fs = Math.max(8, (style === "cartoon" ? 11 : 10) * p1.s);
ctx.font = "bold " + fs + "px Consolas, monospace";
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillStyle = style === "cartoon" ? "#222" : "#fff";
const q1 = { x: p1.x + (mid.x - p1.x) * 0.45, y: p1.y + (mid.y - p1.y) * 0.45 };
const q2 = { x: p2.x + (mid.x - p2.x) * 0.45, y: p2.y + (mid.y - p2.y) * 0.45 };
if (style === "cartoon") {
// 카툰: 글자 뒤 흰 원 배지
[q1, q2].forEach((q) => {
ctx.beginPath(); ctx.arc(q.x, q.y, fs * 0.72, 0, Math.PI * 2);
ctx.fillStyle = "#fff"; ctx.fill();
ctx.lineWidth = 1.6; ctx.strokeStyle = "#222"; ctx.stroke();
});
ctx.fillStyle = "#222";
}
ctx.fillText(b1, q1.x, q1.y);
ctx.fillText(b2, q2.x, q2.y);
}
}});
// 골격(백본) 구슬
const beadCols = style === "neon" ? ["#59e6ff", "#3f8fff"] : ["#3b5f8a", "#7d97b5"];
[[p1, beadCols[0]], [p2, beadCols[1]]].forEach(([p, col]) => {
items.push({ z: p.z, draw: () => {
const r = Math.max(2.5, (style === "cartoon" ? 7 : 5) * p.s);
if (style === "cartoon") {
ctx.fillStyle = "#5b7ea8";
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
ctx.lineWidth = 2.2; ctx.strokeStyle = "#222"; ctx.stroke();
} else if (style === "neon") {
ctx.shadowColor = col; ctx.shadowBlur = 10;
ctx.fillStyle = col;
ctx.beginPath(); ctx.arc(p.x, p.y, r * 0.7, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
} else {
const g = ctx.createRadialGradient(p.x - r * .3, p.y - r * .3, r * .2, p.x, p.y, r);
g.addColorStop(0, "#dce8f5"); g.addColorStop(1, col);
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
}
}});
});
}
// 골격 연결선 (리본)
const ribbonCols = style === "neon" ? ["#2ec9e8", "#2a6fd6"] : ["#4a6f9d", "#8fa9c6"];
for (let i = 0; i < n - 1; i++) {
[[pts1, ribbonCols[0]], [pts2, ribbonCols[1]]].forEach(([pts, col]) => {
const p = pts[i], q = pts[i + 1];
items.push({ z: (p.z + q.z) / 2 - 0.01, draw: () => {
if (style === "cartoon") {
ctx.strokeStyle = "#222";
ctx.lineWidth = Math.max(3, 5.5 * ((p.s + q.s) / 2)) + 2.5;
ctx.lineCap = "round";
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke();
ctx.strokeStyle = "#5b7ea8";
ctx.lineWidth = Math.max(3, 5.5 * ((p.s + q.s) / 2));
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke();
} else {
if (style === "neon") { ctx.shadowColor = col; ctx.shadowBlur = 8; }
ctx.strokeStyle = col;
ctx.lineWidth = Math.max(1.5, 3 * ((p.s + q.s) / 2));
ctx.lineCap = "round";
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke();
ctx.shadowBlur = 0;
}
}});
});
}
// 멀리 있는 것부터 그리기 (painter's algorithm)
items.sort((a, b) => b.z - a.z);
items.forEach((it) => it.draw());
// 5' / 3' 표시
ctx.font = "bold 12px Consolas, monospace";
ctx.fillStyle = style === "neon" ? "#7fd6ff" : "#5b616b";
ctx.textAlign = "left";
ctx.fillText("5'", 10, 18);
ctx.fillText("3'", 10, H - 10);
}
}
let helixNormal = null, helixMutant = null;
// 3D 모델 스타일 선택 (기본 / 카툰 / 네온 홀로그램)
function setHelixStyle(style) {
CURRENT_HELIX_STYLE = style;
document.querySelectorAll(".helix-style-btn").forEach((b) => {
b.classList.toggle("active", b.dataset.style === style);
});
if (helixNormal) helixNormal.setStyle(style);
if (helixMutant) helixMutant.setStyle(style);
}
document.querySelectorAll(".helix-style-btn").forEach((b) => {
b.addEventListener("click", () => setHelixStyle(b.dataset.style));
});
function renderHelix(r) {
$("#helix-section").hidden = false;
const mutIdx = new Set(r.mutations.map((m) => m.position - 1));
const cn = $("#helix-normal"), cm = $("#helix-mutant");
if (!helixNormal) {
helixNormal = new DnaHelix3D(cn, r.normal, mutIdx);
helixMutant = new DnaHelix3D(cm, r.mutant, mutIdx);
} else {
helixNormal.setSequence(r.normal, mutIdx);
helixMutant.setSequence(r.mutant, mutIdx);
helixNormal._resize();
helixMutant._resize();
}
}
/* ---------------- 분석 결과 렌더링 ---------------- */
function renderResult(r) {
HOTSPOT = r.hotspot || [];
// 서열 비교
$("#compare-section").hidden = false;
renderCompare(r.normal, r.mutant);
// DNA 이중나선 3D
renderHelix(r);
// 경고/핫스팟 안내
const wbox = $("#warnings-box");
wbox.innerHTML = "";
(r.warnings || []).forEach((w) => {
wbox.innerHTML += '<div class="warning-box">⚠️ ' + w + "</div>";
});
if (r.hotspot_found) {
const hotPositions = r.mutations.filter((m) => m.is_hotspot).map((m) => m.position + "번");
wbox.innerHTML +=
'<div class="hotspot-alert">★ ' + hotPositions.join(", ") +
" 위치는 중요 변이 구간(핫스팟)입니다! 핫스팟 변이가 발견되어 위험도가 추가 반영되었습니다.</div>";
}
// 통계 카드
$("#stat-count").textContent = r.mutation_count + "개";
$("#risk-title").textContent = r.risk_title || "암 관련 위험도";
$("#stat-risk").textContent = r.risk + "%";
$("#stat-level").textContent = r.risk_level;
$("#stat-treatment").textContent = r.treatment_priority;
const riskEl = $("#stat-risk");
const levelEl = $("#stat-level");
const cls = r.risk >= 50 ? "danger" : r.risk >= 20 ? "warn" : "safe";
riskEl.className = "stat-value " + cls;
levelEl.className = "stat-value " + cls;
$("#risk-fill").style.width = r.risk + "%";
// 변이 테이블
const tbody = $("#mutation-table tbody");
tbody.innerHTML = "";
if (r.mutation_count === 0) {
$("#mutation-table").hidden = true;
$("#no-mutation-msg").hidden = false;
} else {
$("#mutation-table").hidden = false;
$("#no-mutation-msg").hidden = true;
r.mutations.forEach((m) => {
const posLabel = (r.offset && r.offset > 1)
? m.position + "번 <small>(c." + m.c_position + ")</small>"
: m.position + "번";
const tr = document.createElement("tr");
tr.innerHTML =
"<td><strong>" + posLabel + "</strong></td>" +
'<td><span class="' + baseClass(m.normal_base) + '">' + m.normal_base + "</span></td>" +
'<td class="arrow-cell">→</td>' +
'<td><span class="base base-mut">' + m.mutant_base + "</span></td>" +
"<td>" + (m.is_hotspot ? '<span class="hotspot-badge">★ 핫스팟</span>' : "-") + "</td>";
tbody.appendChild(tr);
});
}
// 임상 정보 (NCBI ClinVar·문헌 기반)
const cbox = $("#clinical-box");
if (r.clinical && r.clinical.variant_name) {
const c = r.clinical;
cbox.hidden = false;
$("#clinical-variant").innerHTML =
"🧾 " + c.variant_name +
(c.variant_detected ? ' <span class="detected-badge">이 변이가 검출되었습니다</span>' : "");
$("#clinical-summary").textContent = c.summary || "";
$("#clinical-cancers").textContent = c.cancers || "-";
$("#clinical-risk").textContent = c.risk_info || "-";
$("#clinical-mechanism").textContent = c.mechanism || "-";
$("#clinical-hotspot").textContent = c.hotspot_info || "-";
$("#clinical-outcomes").textContent = c.outcomes || "-";
$("#clinical-survival").textContent = c.survival || "-";
$("#clinical-sources").innerHTML = (c.sources || []).map(
(s) => '<a href="' + s.url + '" target="_blank" rel="noopener">🔗 ' + s.label + "</a>"
).join("");
} else {
cbox.hidden = true;
}
// 치료 접근법
const list = $("#approach-list");
list.innerHTML = "";
(r.approaches || []).forEach((a) => {
list.innerHTML +=
'<li><span class="approach-name">' + a.name + "</span><br>" +
'<span class="approach-desc">' + a.desc + "</span></li>";
});
// 서열 통계
$("#stats-body").innerHTML =
"<tr><td>정상 서열 길이</td><td>" + r.stats.normal_length + " bp</td></tr>" +
"<tr><td>돌연변이 서열 길이</td><td>" + r.stats.mutant_length + " bp</td></tr>" +
"<tr><td>정상 서열 GC 함량</td><td>" + r.stats.normal_gc + " %</td></tr>" +
"<tr><td>돌연변이 서열 GC 함량</td><td>" + r.stats.mutant_gc + " %</td></tr>";
$("#result-section").hidden = false;
$("#helix-section").scrollIntoView({ behavior: "smooth", block: "start" });
}
/* ---------------- 분석 실행 ---------------- */
async function runAnalyze() {
const normal = cleanSeq(normalInput.value);
const mutant = cleanSeq(mutantInput.value);
const wbox = $("#warnings-box");
if (!normal || !mutant) {
alert("정상 서열과 돌연변이 서열을 모두 입력해 주세요.");
return;
}
try {
const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ normal, mutant, sample_id: currentSampleId }),
});
const data = await res.json();
if (!data.ok) {
$("#result-section").hidden = false;
$("#compare-section").hidden = true;
wbox.innerHTML = (data.errors || ["알 수 없는 오류"]).map(
(e) => '<div class="error-box">❌ ' + e + "</div>"
).join("");
return;
}
renderResult(data);
} catch (e) {
alert("백엔드 서버에 연결할 수 없습니다.\nrun.bat 파일을 실행하거나 터미널에서\npython backend/server.py 를 실행해 주세요.");
}
}
/* ---------------- 이벤트 연결 ---------------- */
$("#analyze-btn").addEventListener("click", runAnalyze);
$("#default-btn").addEventListener("click", () => {
currentSampleId = "tp53";
normalInput.value = DEFAULT_SEQ.normal;
mutantInput.value = DEFAULT_SEQ.mutant;
updateCounts();
runAnalyze();
});
$("#clear-btn").addEventListener("click", () => {
currentSampleId = null;
normalInput.value = "";
mutantInput.value = "";
updateCounts();
$("#compare-section").hidden = true;
$("#result-section").hidden = true;
$("#helix-section").hidden = true;
document.querySelectorAll(".sample-card").forEach((c) => c.classList.remove("active"));
});
// 사용자가 직접 서열을 수정하면 샘플 연결 해제 (임상 데이터 오적용 방지)
function onManualEdit() {
updateCounts();
currentSampleId = null;
document.querySelectorAll(".sample-card").forEach((c) => c.classList.remove("active"));
}
normalInput.addEventListener("input", onManualEdit);
mutantInput.addEventListener("input", onManualEdit);
/* ---------------- 초기화 ---------------- */
checkServer();
loadSamples();
더블클릭 한 번으로 서버를 시작합니다. 폴더에 내장된 Python 3.14를 우선 사용하므로 설치도 인터넷도 필요 없습니다.
@echo off
title p53 DNA Mutation Analysis System
cd /d "%~dp0"
echo =====================================================
echo p53 DNA Mutation Analysis System
echo =====================================================
echo.
rem 1) Bundled Python 3.14 (no install / no internet needed)
if exist "%~dp0python\python.exe" (
echo [INFO] Running with bundled Python 3.14
echo [INFO] Browser will open: http://localhost:8000
echo [INFO] Press Ctrl+C to stop the server
echo.
"%~dp0python\python.exe" "%~dp0backend\server.py"
goto end
)
rem 2) Python installed on this PC
py -3 --version >nul 2>&1
if %errorlevel%==0 (
echo [INFO] Running with installed Python 3
py -3 "%~dp0backend\server.py"
goto end
)
python --version >nul 2>&1
if %errorlevel%==0 (
echo [INFO] Running with python command
python "%~dp0backend\server.py"
goto end
)
echo [ERROR] Python not found.
echo Make sure the "python" folder is inside the dna folder.
:end
echo.
pause
클라우드 서버가 재부팅되어도 프로그램이 자동으로 다시 시작되도록 등록한 서비스 정의입니다.
[Unit]
Description=AsTec hyunah p53 gene mutation analyzer (Python stdlib, port 3300)
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/hyunah
Environment=HOST=127.0.0.1
Environment=NO_BROWSER=1
ExecStart=/usr/bin/python3 /home/ubuntu/hyunah/backend/server.py 3300
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
hyunah.astec-mobility.com 접속을 내부 파이썬 서버로 전달하고, HTTPS 인증서를 자동 발급합니다. /dna2 경로는 공공데이터 연계판 서버(3301)로 전달됩니다.
hyunah.astec-mobility.com {
encode gzip
redir /dna2 /dna2/ 301
handle_path /dna2/* {
reverse_proxy localhost:3301
}
reverse_proxy localhost:3300
}
| 방법 | 절차 | 필요한 것 |
|---|---|---|
| 인터넷 접속 | hyunah.astec-mobility.com/analyzer | 인터넷만 있으면 어떤 기기든 가능 |
| 오프라인 실행 | D:\dna un.bat 더블클릭 → localhost:8000/analyzer | 없음 (파이썬 3.14 내장, 인터넷 불필요) |
| 터미널 실행 | python backend\server.py 8000 | Python 3.8 이상 아무 버전 |