- {/* 컨트롤 버튼 영역 */}
-
-
- Zoom: {viewState.scale.toFixed(2)}x
+ {step === 1 && (
+
+
+
+
+ *
+
PCR Template Sequence
+
+
+ FASTA / Raw
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tune
+
+ Essential Settings
+
+
+
+
+
+
+
+
+ v
+
+
+
+
+
+
+
+ {[
+ { label: "Min", value: 57.0, tone: "text-slate-200", border: "border-slate-800" },
+ { label: "Opt", value: 60.0, tone: "text-blue-100", border: "border-blue-500/50" },
+ { label: "Max", value: 63.0, tone: "text-slate-200", border: "border-slate-800" },
+ ].map((item) => (
+
+
+ {item.label}
+
+
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ {step === 2 && (
+
+
+ Step 2. 프라이머 물성(*는 이전 단계)과 클램프/폴리X/농도를 설정하세요.
+
+
+
+ science
+
+ Primer Properties
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ GC Clamp Requirement
+ Enforce G/C at 3' end
+
+
+
+
+
+
+ )}
+
+ {step === 3 && (
+
+
+
+ location_on
+
+ Binding Location & Structure
+
+
+
+
+
+
+
+
+ From
+
+
+
+
-
+
+
+ To
+
+
+
+
+
+
+
+
+
+
+ v
+
+
+
+
+
+
+
+
+
+
+ {["EcoRI", "BamHI", "HindIII"].map((site) => (
+
+ {site}
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ {step === 4 && (
+
+
+
+
+
+ verified_user
+
+ Specificity
+
+
+
+
+
+
+
+ {[
+ { label: "Splice Variant Handling", desc: "Avoid non-specific variants" },
+ { label: "SNP Exclusion", desc: "Avoid primers on known SNPs" },
+ { label: "Mispriming Library", desc: "Check against repeat library" },
+ ].map((item) => (
+
+ ))}
+
+
+
+
+ Number of mismatches allowed within 5bp of 3' end.
+
+
+
+
+
+
+
+
+
+
+
+ Genome timeline
+
+
+ Canvas preview with live zoom
+
+
+
+
+ Zoom {viewState.scale.toFixed(2)}x
+
+
+
+
+
+
+
+
+
{
+ const { data, viewport, viewState } = renderState;
+ if (!data) return;
+
+ const dpr = viewport.devicePixelRatio || 1;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+ ctx.fillStyle = "#0c1222";
+ ctx.fillRect(0, 0, viewport.width, viewport.height);
+
+ const paddingX = 20;
+ const layoutScale = Math.min(
+ 1.4,
+ Math.max(1, viewport.height / 400),
+ );
+
+ const headerY = 28 * layoutScale;
+ const trackStartY = 64 * layoutScale;
+ const trackGap = 28 * layoutScale;
+
+ const bpScale = createBpScale(
+ data.length,
+ viewport.width - paddingX * 2,
+ 0,
+ );
+
+ const toScreenX = (bp: number) =>
+ paddingX +
+ viewState.offsetX +
+ bpScale.bpToX(bp) * viewState.scale;
+
+ const toScreenWidth = (start: number, end: number) => {
+ const rawWidth =
+ bpScale.spanToWidth(start, end, 0) *
+ viewState.scale;
+ return Math.max(2, rawWidth);
+ };
+
+ ctx.fillStyle = "#e2e8f0";
+ ctx.font = `600 ${14 * layoutScale}px ui-sans-serif, system-ui`;
+ ctx.fillText(
+ `Genome length`,
+ paddingX,
+ headerY - 6 * layoutScale,
+ );
+
+ ctx.fillStyle = "#9fb3d4";
+ ctx.font = `${12 * layoutScale}px ui-sans-serif, system-ui`;
+ ctx.fillText(
+ `${data.length.toLocaleString()} bp`,
+ paddingX,
+ headerY + 10 * layoutScale,
+ );
+
+ ctx.strokeStyle = "#1f2b3f";
+ ctx.lineWidth = 1;
+ for (let i = 0; i <= 10; i += 1) {
+ const x =
+ paddingX +
+ i * ((viewport.width - paddingX * 2) / 10);
+ ctx.beginPath();
+ ctx.moveTo(x, trackStartY - 16 * layoutScale);
+ ctx.lineTo(x, viewport.height - 20);
+ ctx.stroke();
+ }
+
+ let y = trackStartY + viewState.offsetY;
+
+ data.tracks.forEach((track) => {
+ const trackHeight = (track.height ?? 18) * layoutScale;
+
+ ctx.fillStyle = "#a5b4d8";
+ ctx.font = `${12 * layoutScale}px ui-sans-serif, system-ui`;
+ ctx.fillText(
+ track.name ?? track.id,
+ paddingX,
+ y - 10 * layoutScale,
+ );
+
+ ctx.strokeStyle = "#23324a";
+ ctx.beginPath();
+ ctx.moveTo(paddingX, y + trackHeight / 2);
+ ctx.lineTo(
+ viewport.width - paddingX,
+ y + trackHeight / 2,
+ );
+ ctx.stroke();
+
+ track.features.forEach((feature) => {
+ const x = toScreenX(feature.start);
+ const width = toScreenWidth(
+ feature.start,
+ feature.end,
+ );
+ const radius = Math.min(6, trackHeight / 2);
+
+ ctx.fillStyle = feature.color ?? "#38bdf8";
+ drawRoundedRect(ctx, x, y, width, trackHeight, radius);
+ ctx.fill();
+
+ if (feature.label) {
+ const labelPaddingX = 6 * layoutScale;
+ const labelPaddingY = 3 * layoutScale;
+ ctx.font = `600 ${11 * layoutScale}px ui-sans-serif, system-ui`;
+ const metrics = ctx.measureText(feature.label);
+ const labelWidth =
+ metrics.width + labelPaddingX * 2;
+ const labelHeight =
+ 16 * layoutScale + labelPaddingY;
+ const labelX = x + 6 * layoutScale;
+ const labelY = y + trackHeight + 6 * layoutScale;
+
+ ctx.fillStyle = "rgba(15,23,42,0.9)";
+ ctx.strokeStyle = "#1f2b3f";
+ drawRoundedRect(
+ ctx,
+ labelX,
+ labelY,
+ labelWidth,
+ labelHeight,
+ 6 * layoutScale,
+ );
+ ctx.fill();
+ ctx.stroke();
+
+ ctx.fillStyle = "#e2e8f0";
+ ctx.fillText(
+ feature.label,
+ labelX + labelPaddingX,
+ labelY + 12 * layoutScale,
+ );
+ }
+ });
+
+ y += trackHeight + trackGap;
+ });
+ }}
+ />
+
+
+
+
+
+
+
+
+ Quality notes
+
+
+ Quick health check
+
+
+
+ Stable
+
+
+
+ -
+
+ Primer candidates are spaced to avoid overlap with target amplicon.
+
+ -
+
+ Zoom 상태와 오프셋은 결과 단계에서만 노출됩니다.
+
+ -
+
+ 단계 전환은 상단 스텝 원형 인디케이터와 하단 버튼으로 진행합니다.
+
+
+
+
+ )}
+
+
+
+ {!isLastStep && (
-
+ )}
+ {isLastStep && (
-
-
- {/* 캔버스 컴포넌트 */}
-
{
- const { data, viewport, viewState } = renderState;
- if (!data) return;
-
- // ✅ [핵심 수정] DPI를 고려한 Transform 초기화
- // GenomeCanvas에서 설정한 dpr 값을 그대로 가져와야 함 (안 그러면 1/dpr 배로 작아짐)
- const dpr = viewport.devicePixelRatio || 1;
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
-
- // 1. 배경 지우기 및 그리기
- ctx.fillStyle = "#f8fafc";
- ctx.fillRect(0, 0, viewport.width, viewport.height);
-
- // 2. 레이아웃 상수 계산
- const paddingX = 20;
- // 높이가 너무 작으면 글씨 크기도 줄이는 반응형 레이아웃 스케일
- const layoutScale = Math.min(1.4, Math.max(1, viewport.height / 400));
-
- const headerY = 28 * layoutScale;
- const trackStartY = 64 * layoutScale;
- const trackGap = 28 * layoutScale;
-
- // 3. 좌표 변환 함수
- const bpScale = createBpScale(data.length, viewport.width - paddingX * 2, 0);
-
- const toScreenX = (bp: number) =>
- paddingX + viewState.offsetX + bpScale.bpToX(bp) * viewState.scale;
-
- const toScreenWidth = (start: number, end: number) => {
- const rawWidth = bpScale.spanToWidth(start, end, 0) * viewState.scale;
- return Math.max(2, rawWidth); // 최소 2px 보장
- };
-
- // 4. 헤더 텍스트 그리기
- ctx.fillStyle = "#0f172a";
- ctx.font = `600 ${14 * layoutScale}px ui-sans-serif, system-ui`;
- ctx.fillText(`Genome length`, paddingX, headerY - 6 * layoutScale);
-
- ctx.fillStyle = "#475569";
- ctx.font = `${12 * layoutScale}px ui-sans-serif, system-ui`;
- ctx.fillText(
- `${data.length.toLocaleString()} bp`,
- paddingX,
- headerY + 10 * layoutScale,
- );
-
- // 5. 그리드 라인 그리기
- ctx.strokeStyle = "#e2e8f0";
- ctx.lineWidth = 1;
- for (let i = 0; i <= 10; i += 1) {
- const x = paddingX + i * ((viewport.width - paddingX * 2) / 10);
- ctx.beginPath();
- ctx.moveTo(x, trackStartY - 16 * layoutScale);
- ctx.lineTo(x, viewport.height - 20);
- ctx.stroke();
- }
-
- // 6. 트랙 데이터 그리기
- let y = trackStartY + viewState.offsetY;
-
- data.tracks.forEach((track) => {
- const trackHeight = (track.height ?? 18) * layoutScale;
-
- // 트랙 이름
- ctx.fillStyle = "#64748b";
- ctx.font = `${12 * layoutScale}px ui-sans-serif, system-ui`;
- ctx.fillText(track.name ?? track.id, paddingX, y - 10 * layoutScale);
-
- // 트랙 가이드라인
- ctx.strokeStyle = "#e5e7eb";
- ctx.beginPath();
- ctx.moveTo(paddingX, y + trackHeight / 2);
- ctx.lineTo(viewport.width - paddingX, y + trackHeight / 2);
- ctx.stroke();
-
- // 피처 그리기
- track.features.forEach((feature) => {
- const x = toScreenX(feature.start);
- const width = toScreenWidth(feature.start, feature.end);
- const radius = Math.min(6, trackHeight / 2);
-
- // 막대 그리기
- ctx.fillStyle = feature.color ?? "#38bdf8";
- drawRoundedRect(ctx, x, y, width, trackHeight, radius);
- ctx.fill();
-
- // 라벨 그리기 (옵션)
- if (feature.label) {
- const labelPaddingX = 6 * layoutScale;
- const labelPaddingY = 3 * layoutScale;
- ctx.font = `600 ${11 * layoutScale}px ui-sans-serif, system-ui`;
- const metrics = ctx.measureText(feature.label);
- const labelWidth = metrics.width + labelPaddingX * 2;
- const labelHeight = 16 * layoutScale + labelPaddingY;
- const labelX = x + 6 * layoutScale;
- const labelY = y + trackHeight + 6 * layoutScale;
-
- // 라벨 배경
- ctx.fillStyle = "#ffffff";
- ctx.strokeStyle = "#e2e8f0";
- drawRoundedRect(
- ctx,
- labelX,
- labelY,
- labelWidth,
- labelHeight,
- 6 * layoutScale,
- );
- ctx.fill();
- ctx.stroke();
-
- // 라벨 텍스트
- ctx.fillStyle = "#0f172a";
- ctx.fillText(
- feature.label,
- labelX + labelPaddingX,
- labelY + 12 * layoutScale,
- );
- }
- });
-
- y += trackHeight + trackGap;
- });
- }}
- />
+ )}
diff --git a/package-lock.json b/package-lock.json
index b03cfd7..15594ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"next": "16.1.1",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "zustand": "^5.0.9"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -1170,7 +1171,6 @@
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1560,7 +1560,7 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -2634,7 +2634,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -3217,6 +3217,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -6573,21 +6574,6 @@
"optional": true
}
}
- },
- "node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.1",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
- "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
}
}
}
diff --git a/package.json b/package.json
index 87fd894..32375b2 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"dependencies": {
"next": "16.1.1",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "zustand": "^5.0.9"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
From 716fc71a0e2fcfeecae055c5aeb14be0a8a468ac Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Tue, 6 Jan 2026 22:07:04 +0900
Subject: [PATCH 02/83] =?UTF-8?q?docs:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20?=
=?UTF-8?q?=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=A0=95=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../primer_design_ui_rework.md" | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 "docs/prompts/3\354\243\274\354\260\250/primer_design_ui_rework.md"
diff --git "a/docs/prompts/3\354\243\274\354\260\250/primer_design_ui_rework.md" "b/docs/prompts/3\354\243\274\354\260\250/primer_design_ui_rework.md"
new file mode 100644
index 0000000..fd00391
--- /dev/null
+++ "b/docs/prompts/3\354\243\274\354\260\250/primer_design_ui_rework.md"
@@ -0,0 +1,29 @@
+# Primer Design UI Rework
+
+## 1. 배경 및 목적
+
+- `test/demodesign.html`에 맞춰 Next.js 앱 메인 화면의 프라이머 디자인 UI를 다크 테마로 재구성하고, 단계별 입력 흐름(시퀀스 입력 → 조건 설정 → 결과 확인/미리보기)을 구현하기 위함.
+
+## 2. 프롬프트 (User Input)
+
+- pcr을 위한 프라이머를 자동으로 디자인해서 시각화해주는 툴을 개발할 것. input 원하는 시퀀스 (필수) 프라이머 조건: 길이(18-30), TM(60C), GC%(40-60%) (optional) 제한효소서열(optional) Core: Next.js 16 (App Router), TypeScript Graphics: HTML5 Canvas API (2D Context) Styling: Tailwind CSS State Management: Zustand Data Fetching: SWR / TanStack Query Deployment: Vercel 목 데이터는 따로 알아보기 편하게 폴더로 분리, 컴포넌트 분리
+
+- `test/demodesign.html`을 참고해 메인 페이지 디자인을 동일 톤으로 맞춰 달라는 요청.
+- UI를 “시퀀스 입력 → 조건 설정 → 결과 확인” 순서로 단계화하고, 진행 중인 단계 외엔 가리도록 요구.
+- 수정된 `demodesign.html` 느낌을 다시 반영해 달라는 리디자인 요청.
+
+## 3. AI 응답 요약 (AI Output)
+
+- `app/page.tsx`를 demodesign과 동일한 다크 톤으로 재설계: 헤더/배지/배경 그라데이션, Space Grotesk/Noto Sans/JetBrains Mono 폰트 유지.
+- 단계형 스텝퍼와 가드 로직 추가(Template & Essential → Primer Properties → Binding Location → Specificity & Preview). 현재 단계만 노출되며 Back/Next/Generate 버튼과 스티키 네비게이션 제공.
+- Step 1: PCR Template 입력 카드(FASTA/Raws, 업로드/붙여넣기/정리 버튼)와 필수 Essential 설정(Organism DB, Product Size Min-Max, Primer Tm Min/Opt/Max).
+- Step 2: Primer 물성 카드(GC 범위, Max Tm diff, GC Clamp 토글, Poly-X, 농도 등) 배치.
+- Step 3: Binding 위치 카드(Search range, Exon junction 옵션, Intron 포함 여부, Intron size range, Restriction enzyme 리스트) 배치.
+- Step 4: Specificity 토글/체크박스와 3' mismatch strictness 입력, 우측에 GenomeCanvas 미리보기(줌/리셋 컨트롤) 및 품질 노트 섹션 구성.
+- 하단 스티키 CTA 영역에서 단계 이동/프라이머 생성 버튼 제공. 화살표/텍스트 인코딩 문제를 escaping으로 해결.
+
+## 4. 결과 및 적용 (Result)
+
+- 적용 파일: `app/page.tsx` (전체 레이아웃, 스텝 UI, 입력 카드, 캔버스 섹션 재구성), `app/layout.tsx`, `app/globals.css` (이전 단계에서 테마/폰트 설정).
+- 결과: demodesign 스타일을 반영한 4단계 시퀀스 UI로 리디자인 완료. 단계별 카드/폼과 GenomeCanvas 프리뷰가 분리되어 UX 가이드라인 충족.
+- 테스트: 실행 테스트 미수행. `npm run dev` 후 스텝 1→4 순서로 화면 전환 및 입력/줌 컨트롤 확인 필요.
From 7ee8f1d770ec8dae0453f4f38aec96d1e6ba59c3 Mon Sep 17 00:00:00 2001
From: DoltHi58
Date: Thu, 8 Jan 2026 03:07:45 +0900
Subject: [PATCH 03/83] =?UTF-8?q?docs:=20=EB=8B=A4=EC=9D=B4=EC=96=B4?=
=?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/spec_diagram.md | 258 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 258 insertions(+)
create mode 100644 docs/spec_diagram.md
diff --git a/docs/spec_diagram.md b/docs/spec_diagram.md
new file mode 100644
index 0000000..dc358f0
--- /dev/null
+++ b/docs/spec_diagram.md
@@ -0,0 +1,258 @@
+# PrimerFlow 시스템 설계 및 아키텍처
+이 문서는 PrimerFlow의 핵심 데이터 구조(Class Diagram)와 시스템 동작 흐름(Sequence Diagram)을 정의합니다.
+테스트 케이스 작성 및 기능 구현 시 본 문서를 기준으로 로직을 검증합니다.
+
+## 1. Class Diagram
+Backend의 주요 데이터 모델과 알고리즘 처리 클래스 구조입니다.
+
+```mermaid
+classDiagram
+ %% ==========================================================
+ %% 1. Request Schema (UI Input Structure)
+ %% ==========================================================
+ class PrimerDesignRequest {
+ +BasicInput basic
+ +PropertyInput properties
+ +SpecificityInput specificity
+ +PositionInput position
+ }
+ class BasicInput {
+ +String templateSequence
+ +String targetOrganism
+ +Range productSize
+ +TmSettings primerTm
+ }
+ class PropertyInput {
+ +Range gcContent
+ +float maxTmDifference
+ +bool gcClamp
+ +int maxPolyX
+ +float concentration
+ }
+ class SpecificityInput {
+ +bool checkEnabled
+ +bool spliceVariantHandling
+ +bool snpExclusion
+ +MismatchSettings endMismatchStrictness
+ +bool misprimingLibrary
+ }
+ class PositionInput {
+ +Range searchRange
+ +String exonJunctionSpan
+ +bool intronInclusion
+ +Range intronSize
+ +List~String~ restrictionEnzymes
+ }
+ %% ==========================================================
+ %% 2. Entity & Response Schema
+ %% ==========================================================
+ class GenomeSequence {
+ %% 5.1.1 GenomeSequence
+ +String id
+ +String name
+ +String sequence
+ +int length_bp
+ }
+ class PrimerCandidate {
+ +String id
+ +String sequence
+ +int start_bp
+ +int end_bp
+ +String strand
+ +Metrics metrics
+ }
+ class Metrics {
+ %% PrimerCandidate.metrics
+ +float tm_c
+ +float gc_percent
+ +dict penalties
+ }
+ class PrimerDesignResponse {
+ +GenomeSequence genome
+ +List~PrimerCandidate~ candidates
+ +Meta meta
+ }
+ class Meta {
+ +PrimerDesignRequest params
+ +String timestamp
+ +float execution_time_ms
+ }
+ %% ==========================================================
+ %% 3. Helper Classes (Value Objects)
+ %% ==========================================================
+ class Range {
+ +float min
+ +float max
+ }
+ class TmSettings {
+ +float min
+ +float opt
+ +float max
+ }
+ class MismatchSettings {
+ +int regionSize
+ +int minMismatch
+ }
+ %% ==========================================================
+ %% 4. Frontend State Management
+ %% ==========================================================
+ class ViewStore {
+ <>
+ +int viewportStartBp
+ +int viewportEndBp
+ +float zoomLevel
+ +String selectedPrimerId
+ +String hoveredPrimerId
+ +Object filterSortState
+
+ +setViewport(start, end)
+ +setZoom(level)
+ +selectPrimer(id)
+ }
+ %% ==========================================================
+ %% Relationships
+ %% ==========================================================
+ %% Request Composition
+ PrimerDesignRequest *-- BasicInput
+ PrimerDesignRequest *-- PropertyInput
+ PrimerDesignRequest *-- SpecificityInput
+ PrimerDesignRequest *-- PositionInput
+ %% Response Composition
+ PrimerDesignResponse *-- GenomeSequence
+ PrimerDesignResponse *-- Meta
+ PrimerDesignResponse "1" o-- "many" PrimerCandidate
+ PrimerCandidate *-- Metrics
+ %% Helpers
+ BasicInput ..> Range
+ BasicInput ..> TmSettings
+ PropertyInput ..> Range
+ PositionInput ..> Range
+ SpecificityInput ..> MismatchSettings
+```
+## 2. Sequence Diagram
+Backend의 주요 시스템 동작 흐름 구조입니다.
+
+```mermaid
+sequenceDiagram
+ autonumber
+
+ participant User as 사용자
+ participant Client as Client (Next.js)
+ participant API as API (FastAPI)
+ participant Algo as Algorithm Engine
+
+ Note over User, Client: 1. 초기 설정 및 입력
+ User->>Client: FASTA 데이터 입력 & 파라미터 설정
+ User->>Client: "프라이머 찾기" 버튼 클릭
+
+ Note over Client, API: 2. 데이터 전송 및 검증
+ Client->>Client: 입력값 유효성 검사 (길이, 형식)
+ Client->>API: POST /api/v1/primer/design (JSON)
+ activate API
+
+ API->>API: Request Body 검증 (Pydantic)
+ alt 유효하지 않은 데이터
+ API-->>Client: 400 Bad Request (Error Msg)
+ Client-->>User: 에러 알림 표시
+ else 유효한 데이터
+ Note over API, Algo: 3. 알고리즘 수행
+ API->>Algo: design_primers(sequence, params)
+ activate Algo
+
+ Algo->>Algo: 후보군 탐색 (Sliding Window)
+ Algo->>Algo: 필터링
+ Algo->>Algo: 최적 페어링 및 정렬
+
+ Algo-->>API: List[PrimerPair] 반환
+ deactivate Algo
+
+ API-->>Client: 200 OK (Result JSON)
+ end
+ deactivate API
+
+ Note over Client, User: 4. 결과 시각화
+ Client->>Client: GenomeCanvas 렌더링
+ Client->>User: 최적 프라이머 목록 및 위치 표시
+```
+
+# Data Flow Diagram and Structure Chart
+
+이 문서는 사용자의 유전체 데이터(FASTA)가 시스템 내부에서 어떤 변환 과정을 거쳐 최종 프라이머 쌍(Primer Pair)으로 도출되는지를 시각화한 DFD Level 1입니다.
+
+## DFD Level 1: Primer Design Process
+
+```mermaid
+graph LR
+ classDef entity fill:#f9f,stroke:#333,stroke-width:2px, color:#000;
+ classDef process fill:#e1f5fe,stroke:#0277bd,stroke-width:2px,rx:10,ry:10, color:#000;
+ classDef store fill:#fff9c4,stroke:#fbc02d,stroke-width:2px, color:#000;
+ %% 1. External Entities (외부 엔티티)
+ User[사용자/Researcher]:::entity
+ Client[Frontend UI]:::entity
+ %% 2. Processes (처리 과정)
+ P1(입력 데이터 파싱 및 검증):::process
+ P2(후보군 생성):::process
+ P3(물성 계산-Tm/GC):::process
+ P4(필터링 및 페어링):::process
+ %% 3. Data Stores (데이터 저장소/설정값)
+ Config[(파라미터 설정값\nMin/Max Tm, Length)]:::store
+ RefData[(생물학적 상수\nSalt Correction 등)]:::store
+ %% 4. Data Flows (데이터 흐름)
+ User -- "1. FASTA 파일 & 설정" --> Client
+ Client -- "2. Raw Sequence JSON" --> P1
+
+ P1 -- "3. 정제된 서열 (Clean Sequence)" --> P2
+ P1 -- "유효하지 않은 서열 에러" --> Client
+
+ Config -.-> P2
+ P2 -- "4. 모든 가능한 20mer 조각들" --> P3
+
+ RefData -.-> P3
+ P3 -- "5. 후보군 리스트 (with Properties)" --> P4
+
+ Config -.-> P4
+ P4 -- "6. 최적 프라이머 쌍 (Ranked Pairs)" --> Client
+ Client -- "7. 시각화된 결과" --> User
+```
+# PrimerFlow 구조 도표 (Structure Chart)
+
+이 문서는 DFD에서 정의된 데이터 처리 과정을 수행하기 위한 소프트웨어 모듈의 계층 구조를 정의합니다.
+상위 모듈이 하위 모듈을 제어하며, 화살표는 호출 관계를 나타냅니다.
+
+## Structure Chart
+
+```mermaid
+graph TD
+ classDef root fill:#1a237e,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef mainModule fill:#0277bd,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef subModule fill:#e1f5fe,stroke:#0277bd,stroke-width:1px,color:#000;
+ classDef library fill:#fff9c4,stroke:#fbc02d,stroke-width:1px,stroke-dasharray: 5 5, color:#000;
+ %% Level 0: Main Controller
+ Main["Main Controller
(API Endpoint)"]:::root
+ %% Level 1: Primary Modules
+ InputMod["Input Processing Module"]:::mainModule
+ AlgoMod["Core Algorithm Module"]:::mainModule
+ ResultMod["Result Formatting Module"]:::mainModule
+ %% Level 2: Sub Modules
+ %% Input Branch
+ Parser["FASTA Parser"]:::subModule
+ Validator["Input Validator"]:::subModule
+ %% Algorithm Branch
+ Gen["Candidate Generator
(Sliding Window)"]:::subModule
+ PropFilt["Property filtering"]:::subModule
+ SpecFilt["Specificity filtering"]:::subModule
+ BindFilt["Binding filtering"]:::subModule
+ %% Result Branch
+ JSON["JSON Serializer"]:::subModule
+ %% 호출 관계 연결
+ Main --> InputMod
+ Main --> AlgoMod
+ Main --> ResultMod
+ InputMod --> Parser
+ InputMod --> Validator
+ AlgoMod --> Gen
+ AlgoMod --> PropFilt
+ AlgoMod --> BindFilt
+ AlgoMod --> SpecFilt
+ ResultMod --> JSON
+```
\ No newline at end of file
From fbad9857dad98d08d17a1c891117a46e51a44088 Mon Sep 17 00:00:00 2001
From: SeongHyeon Mun <7sonicx@gmail.com>
Date: Thu, 8 Jan 2026 18:46:20 +0900
Subject: [PATCH 04/83] =?UTF-8?q?chore:=20Copilot=20=EB=A6=AC=EB=B7=B0=20?=
=?UTF-8?q?=ED=95=9C=EA=B5=AD=EC=96=B4=EB=A1=9C=20=EB=B0=9B=EB=8F=84?=
=?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/copilot-instructions.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 .github/copilot-instructions.md
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..db8ab7e
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,4 @@
+# Code Review Guidelines
+
+1. **Language**: Please write ALL comments, summaries, and reviews in **Korean (한국어)**. (모든 리뷰와 답변은 반드시 한국어로 작성해 주세요.)
+2. **Tone**: Be polite and constructive. (정중하고 건설적인 톤을 유지해 주세요.)
\ No newline at end of file
From 8d6e74aec84b4b84a439915ad69d30947da6c76e Mon Sep 17 00:00:00 2001
From: DoltHi58
Date: Thu, 8 Jan 2026 23:57:11 +0900
Subject: [PATCH 05/83] =?UTF-8?q?docs:=20diagram=20=EC=88=98=EC=A0=95=20?=
=?UTF-8?q?=EB=B0=8F=20UI=20=EC=9E=85=EB=A0=A5=20=ED=8C=8C=EB=9D=BC?=
=?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A0=95=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/spec_UI_input.md | 61 +++++++++++++++++++++++++++++++++++++++++++
docs/spec_diagram.md | 6 ++---
2 files changed, 64 insertions(+), 3 deletions(-)
create mode 100644 docs/spec_UI_input.md
diff --git a/docs/spec_UI_input.md b/docs/spec_UI_input.md
new file mode 100644
index 0000000..7766f10
--- /dev/null
+++ b/docs/spec_UI_input.md
@@ -0,0 +1,61 @@
+# Primer design 시 필요한 UI_input들
+##### 본 문서에서는 생물학적 단어와 내용은 최대한 배제하여 개발 시 숙지하여야 한다고 판단되는 단어만 첨언하여 작성하였습니다.
+
+# 1. Introduction
+Primer 설계는 주형 DNA(template DNA)라는 거대한 문자열 텍스트 안에서, 우리가 원하는 특정 구간만을 복사하기 위해 "정확한 위치"를 찾아내는 "검색 패턴(Search Pattern)"을 만드는 과정과 유사합니다. Primer가 주형 DNA에 얼마나 "정확하고(Specificity)", "빠르게(Efficiency)" 붙느냐가 핵심입니다. 아래는 Primer 설계 시 고려해야 하는 변수들을 정리한 내용입니다.
+
+## 2. UI 입력 파라미터 (Input Parameters)
+
+사용자 인터페이스(UI)에서 입력받아야 할 항목들은 알고리즘 내 역할에 따라 다음과 같이 분류된다.
+
+### 2.1 기본 입력 (Basic Input)
+알고리즘 구동을 위한 필수 데이터이다.
+
+| 항목명 (Label) | 필수 | 입력 타입 | 설명 및 로직 |
+| :--- | :---: | :--- | :--- |
+| **PCR Template Sequence** | ✅ | String / File | 프라이머를 설계할 표적 DNA 서열 (FASTA 포맷). |
+| **Target Organism (DB)** | ✅ | Dropdown | 특이성 검증(Specificity Check)을 수행할 대상 유전체 DB 선택 (예: hg38, mm10). |
+| **PCR Product Size** | ✅ | Range (Min-Max) | 원하는 증폭 산물의 크기 범위 (예: $100 \sim 300 \text{bp}$). |
+| **Primer Tm** | ✅ | 3 Inputs | 프라이머의 목표 융해 온도 범위 (Min, Opt, Max).
*(일반적으로 $57 \sim 63^\circ\text{C}$)* |
+
+### 2.2 프라이머 물성 설정 (Primer Property)
+프라이머 자체의 물리화학적 성질 및 열역학적 안정성을 제어한다.
+
+| 항목명 (Label) | 필수 | 입력 타입 | 알고리즘 처리 로직 |
+| :--- | :---: | :--- | :--- |
+| **Primer GC Content (%)** | | Range (Min-Max) | 프라이머 서열 내 G/C 비율 검사 (보통 40~60%). |
+| **Max Tm Difference** | | Number | Forward/Reverse 프라이머 간 $T_m$ 차이 허용치 (권장 $\le 3^\circ\text{C}$). |
+| **GC Clamp Requirement** | | Checkbox | 3' 말단 1~2bp가 G 또는 C로 끝나는지 확인. |
+| **Max Poly-X Run** | | Number | 단일 염기 반복(예: AAAAA) 허용 횟수 제한. |
+| **Concentration (nM)** | | Number | $T_m$ 및 $\Delta G$ 계산(Nearest-Neighbor 모델) 시 농도 보정 상수로 사용. |
+
+### 2.3 결합 위치 및 구조 제어 (Binding Position)
+유전자 구조(Exon/Intron), 변이(SNP), 제한효소 정보를 기반으로 프라이머 위치를 필터링한다.
+
+| 항목명 (Label) | 필수 | 입력 타입 | 알고리즘 처리 로직 |
+| :--- | :---: | :--- | :--- |
+| **Search Range** | | 2 Inputs (From-To) | 템플릿 내에서 프라이머 탐색 구간 제한. |
+| **Exon Junction Span** | | Dropdown | Annotation DB(SQLite) 조회. 프라이머가 Exon 경계에 걸치는지 확인 (mRNA 타겟 시 gDNA 증폭 방지). |
+| **Intron Inclusion** | | Checkbox | Annotation DB 조회. Fwd/Rev 프라이머 사이에 인트론이 포함되는지 확인. |
+| **Intron Size Range** | | Range (Min-Max) | 포함될 인트론의 크기 범위 지정. |
+| **Restriction Enzymes** | | List / Search | **[사용자 정의]** 템플릿 내 해당 효소 절단 부위와 프라이머가 겹치면 제거(Overlap check). |
+
+### 2.4 특이성 검증 (Specificity)
+Genome DB(pysam)를 활용하여 비특이적 결합을 차단한다.
+
+| 항목명 (Label) | 필수 | 입력 타입 | 알고리즘 처리 로직 |
+| :--- | :---: | :--- | :--- |
+| **Specificity Check Enable** | ✅ | Checkbox | Genome DB 전수 검색 수행 여부 결정 (OFF 시 속도 향상, 위험 증가). |
+| **Splice Variant Handling** | | Checkbox | Annotation DB 조회. 동일 유전자의 다른 전사체(Variant)에 결합하는 경우 허용(Pass). |
+| **SNP Exclusion** | | Checkbox | SNP DB 조회. 프라이머 위치(특히 3' 말단)에 SNP 존재 시 제거. |
+| **3' End Mismatch Strictness** | | Dropdown / Number | 비특이적 타겟 발견 시, 3' 말단 $N$ bp 내에 미스매치가 $M$개 미만이면 '위험'으로 간주하고 탈락시킴. |
+| **Mispriming Library** | | Checkbox | 반복 서열(Repeat DB) 유사도 검사. |
+
+---
+
+## References
+* Altschul, S. F., et al. (1990). Basic local alignment search tool. *Journal of Molecular Biology*.
+* Altschul, S. F., et al. (1997). Gapped BLAST and PSI-BLAST. *Nucleic Acids Research*.
+* Qu, W., et al. (2012). MFEprimer-2.0: a fast thermodynamics-based program for checking PCR primer specificity. *Nucleic Acids Research*.
+* SantaLucia, J. (1998). A unified view of polymer, dumbbell, and oligonucleotide DNA nearest-neighbor thermodynamics. *PNAS*.
+* Ye, J., et al. (2012). Primer-BLAST: A tool to design target-specific primers for polymerase chain reaction. *BMC Bioinformatics*.
\ No newline at end of file
diff --git a/docs/spec_diagram.md b/docs/spec_diagram.md
index dc358f0..06e11e4 100644
--- a/docs/spec_diagram.md
+++ b/docs/spec_diagram.md
@@ -239,9 +239,9 @@ graph TD
Validator["Input Validator"]:::subModule
%% Algorithm Branch
Gen["Candidate Generator
(Sliding Window)"]:::subModule
- PropFilt["Property filtering"]:::subModule
- SpecFilt["Specificity filtering"]:::subModule
- BindFilt["Binding filtering"]:::subModule
+ PropFilt["Property Filter"]:::subModule
+ SpecFilt["Specificity Filter"]:::subModule
+ BindFilt["Binding Filter"]:::subModule
%% Result Branch
JSON["JSON Serializer"]:::subModule
%% 호출 관계 연결
From 1a9153132039d46f2f4c359a107665c174843584 Mon Sep 17 00:00:00 2001
From: DoItHi58
Date: Fri, 9 Jan 2026 00:28:20 +0900
Subject: [PATCH 06/83] Update spec_diagram.md
---
docs/spec_diagram.md | 54 ++++++++++++++++++++++----------------------
1 file changed, 27 insertions(+), 27 deletions(-)
diff --git a/docs/spec_diagram.md b/docs/spec_diagram.md
index 06e11e4..bdb2b1e 100644
--- a/docs/spec_diagram.md
+++ b/docs/spec_diagram.md
@@ -17,31 +17,31 @@ classDiagram
+PositionInput position
}
class BasicInput {
- +String templateSequence
- +String targetOrganism
- +Range productSize
- +TmSettings primerTm
+ +String template_sequence
+ +String target_organism
+ +Range product_size
+ +TmSettings primer_tm
}
class PropertyInput {
- +Range gcContent
- +float maxTmDifference
- +bool gcClamp
- +int maxPolyX
+ +Range gc_content
+ +float max_tm_difference
+ +bool gc_clamp
+ +int max_poly_x
+float concentration
}
class SpecificityInput {
- +bool checkEnabled
- +bool spliceVariantHandling
- +bool snpExclusion
- +MismatchSettings endMismatchStrictness
- +bool misprimingLibrary
+ +bool check_enabled
+ +bool splice_variant_handling
+ +bool snp_exclusion
+ +MismatchSettings end_mismatch_strictness
+ +bool mispriming_library
}
class PositionInput {
- +Range searchRange
- +String exonJunctionSpan
- +bool intronInclusion
- +Range intronSize
- +List~String~ restrictionEnzymes
+ +Range search_range
+ +String exon_junction_span
+ +bool intron_inclusion
+ +Range intron_size
+ +List~String~ restriction_enzymes
}
%% ==========================================================
%% 2. Entity & Response Schema
@@ -90,20 +90,20 @@ classDiagram
+float max
}
class MismatchSettings {
- +int regionSize
- +int minMismatch
+ +int region_size
+ +int min_mismatch
}
%% ==========================================================
%% 4. Frontend State Management
%% ==========================================================
class ViewStore {
<>
- +int viewportStartBp
- +int viewportEndBp
- +float zoomLevel
- +String selectedPrimerId
- +String hoveredPrimerId
- +Object filterSortState
+ +int viewport_startBp
+ +int viewport_endBp
+ +float zoom_level
+ +String selected_primer_id
+ +String hovered_primer_id
+ +Object filter_sort_state
+setViewport(start, end)
+setZoom(level)
@@ -255,4 +255,4 @@ graph TD
AlgoMod --> BindFilt
AlgoMod --> SpecFilt
ResultMod --> JSON
-```
\ No newline at end of file
+```
From 9d6e610593e665ed752d1e6123e182fbed6e2939 Mon Sep 17 00:00:00 2001
From: DoltHi58
Date: Fri, 9 Jan 2026 13:19:48 +0900
Subject: [PATCH 07/83] =?UTF-8?q?docs:=20spec=5FAPI&Data.md=20=EC=97=85?=
=?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/spec_API&Data.md | 65 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 59 insertions(+), 6 deletions(-)
diff --git a/docs/spec_API&Data.md b/docs/spec_API&Data.md
index 9e939b7..d54424c 100644
--- a/docs/spec_API&Data.md
+++ b/docs/spec_API&Data.md
@@ -21,7 +21,7 @@ interface GenomeSequence {
interface PrimerCandidate {
id: string; // 후보 ID
sequence: string; // 프라이머 서열
- start_bp: number; // 시작 위치 (TBD: 0-based vs 1-based 기준 확정 필요)
+ start_bp: number; // 시작 위치 (1-based 기준)
end_bp: number; // 종료 위치
strand: "forward" | "reverse"; // 방향
metrics: { // (TBD) 제공 필드 확정 필요
@@ -33,12 +33,65 @@ interface PrimerCandidate {
```
### 3) PrimerDesignResponse
```typescript
+export interface Range {
+ min: number;
+ max: number;
+}
+
+export interface PrimerDesignRequest {
+ // 1. Basic Input (필수)
+ basic: {
+ templateSequence: string; // ★ PCR Template Sequence
+ targetOrganism: string; // ★ Target Organism (DB ID)
+ productSize: Range; // ★ PCR Product Size
+ primerTm: { // ★ Primer Tm
+ min: number;
+ opt: number;
+ max: number;
+ };
+ };
+
+ // 2. Primer Property
+ properties: {
+ gcContent: Range; // Primer GC Content (%)
+ maxTmDifference: number; // Max Tm Difference
+ gcClamp: boolean; // GC Clamp Requirement
+ maxPolyX: number; // Max Poly-X Run
+ concentration: number; // Concentration (nM)
+ };
+
+ // 3. Primer Specificity
+ specificity: {
+ checkEnabled: boolean; // ★ Specificity Check Enable
+ spliceVariantHandling: boolean; // Splice Variant Handling
+ snpExclusion: boolean; // SNP Exclusion
+ endMismatchStrictness?: { // 3' End Mismatch (Dropdown/Number)
+ regionSize: number; // 3' 말단 N bp
+ minMismatch: number; // M 개 이상의 미스매치
+ };
+ misprimingLibrary: boolean; // Mispriming Library Check
+ };
+
+ // 4. Primer Binding Position
+ position: {
+ searchRange: { // Search Range (From-To)
+ from: number;
+ to: number;
+ };
+ exonJunctionSpan: 'none' | 'flanking' | 'spanning'; // Dropdown
+ intronInclusion: boolean; // Intron Inclusion
+ intronSize?: Range; // Intron Size Range
+ restrictionEnzymes: string[]; // Restriction Enzymes List
+ };
+}
+
interface PrimerDesignResponse {
genome: GenomeSequence; // 분석된 게놈 정보 (요약)
candidates: PrimerCandidate[]; // 생성된 후보 목록
- meta: { // (TBD) 메타 데이터
- params?: any; // 요청 시 사용된 파라미터
- timestamp?: string; // 생성 시간
+ meta: {
+ params: PrimerDesignRequest; // 요청 시 사용된 파라미터 (검증용)
+ timestamp: string; // 생성 시간 (ISO 8601)
+ execution_time_ms?: number; // 실행 시간
};
}
```
@@ -46,8 +99,8 @@ interface PrimerDesignResponse {
Note: 백엔드는 FastAPI를 사용하며 OpenAPI(/docs)를 제공합니다. 정확한 경로는 개발 착수 시 확정(TBD)됩니다.
1) 프라이머 설계 요청 (Design Primers)
- Endpoint: POST /api/design (예상, TBD)
-- Request:서열 데이터 (Sequence String)
-설계 파라미터 (TBD: Target Product Size, Tm Range 등)
+- Request: 서열 데이터 (Sequence String)
+설계 파라미터 (PrimerDesignRequest)
- Response: PrimerDesignResponse
- Status Codes & Errors:
- 200 OK: 성공
From 959f10b31b6dd3acb1d599973700391687f96f82 Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Sun, 11 Jan 2026 17:27:52 +0900
Subject: [PATCH 08/83] fix: correct z-index stacking in Stepper component
---
app/page.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/page.tsx b/app/page.tsx
index 562c3f9..c29687b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,9 +1,10 @@
"use client";
import { useState } from "react";
-import GenomeCanvas, { type GenomeData } from "@/components/canvas/GenomeCanvas";
+import GenomeCanvas from "@/components/canvas/GenomeCanvas";
import { createBpScale } from "@/lib/math/coords"; // 만약 이 파일이 없다면 아래 주석 참고
import { useViewStore } from "@/store/useViewStore";
+import type { GenomeData } from "@/lib/types/Genome";
/**
* 둥근 사각형 그리기 유틸리티
@@ -133,7 +134,7 @@ export default function Home() {
-
+
{steps.map((item) => {
const status =
@@ -150,7 +151,7 @@ export default function Home() {
);
-}
\ No newline at end of file
+}
diff --git a/src/lib/types/Genome.ts b/src/lib/types/Genome.ts
new file mode 100644
index 0000000..7e409e8
--- /dev/null
+++ b/src/lib/types/Genome.ts
@@ -0,0 +1,53 @@
+import type { CSSProperties } from "react";
+
+export type GenomeFeature = {
+ id?: string;
+ start: number;
+ end: number;
+ label?: string;
+ color?: string;
+};
+
+export type GenomeTrack = {
+ id: string;
+ name?: string;
+ height?: number;
+ features: GenomeFeature[];
+};
+
+export type GenomeData = {
+ length: number;
+ tracks: GenomeTrack[];
+};
+
+export type GenomeCanvasViewState = {
+ scale: number;
+ offsetX: number;
+ offsetY: number;
+};
+
+export type GenomeCanvasRenderState = {
+ data?: GenomeData;
+ viewState: GenomeCanvasViewState;
+ viewport: {
+ width: number;
+ height: number;
+ devicePixelRatio: number;
+ };
+};
+
+export type GenomeCanvasProps = {
+ className?: string;
+ style?: CSSProperties;
+ genome?: GenomeData;
+ viewState?: GenomeCanvasViewState;
+ initialViewState?: GenomeCanvasViewState;
+ minScale?: number;
+ maxScale?: number;
+ onViewStateChange?: (nextViewState: GenomeCanvasViewState) => void;
+ onDraw?: (
+ ctx: CanvasRenderingContext2D,
+ canvas: HTMLCanvasElement,
+ renderState: GenomeCanvasRenderState,
+ ) => void;
+};
diff --git a/store/useViewStore.ts b/store/useViewStore.ts
index 8ee6b74..f4a7dbc 100644
--- a/store/useViewStore.ts
+++ b/store/useViewStore.ts
@@ -1,7 +1,7 @@
"use client";
import { create } from "zustand";
-import type { GenomeCanvasViewState } from "@/components/canvas/GenomeCanvas";
+import type { GenomeCanvasViewState } from "@/lib/types/Genome";
type ViewStore = {
viewState: GenomeCanvasViewState;
diff --git a/tsconfig.json b/tsconfig.json
index 3a13f90..7d5eedd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,7 +19,7 @@
}
],
"paths": {
- "@/*": ["./*"]
+ "@/*": ["./src/*", "./*"]
}
},
"include": [
From 7fdefad3381464d95bd21df0e40b30d1f99d8ce4 Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Sun, 11 Jan 2026 17:42:20 +0900
Subject: [PATCH 10/83] =?UTF-8?q?docs:=203=EC=A3=BC=EC=B0=A8=20=ED=94=84?=
=?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=A0=95=EB=A6=AC+@?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../genome_types_refactor.md" | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 "docs/prompts/3\354\243\274\354\260\250/genome_types_refactor.md"
diff --git "a/docs/prompts/3\354\243\274\354\260\250/genome_types_refactor.md" "b/docs/prompts/3\354\243\274\354\260\250/genome_types_refactor.md"
new file mode 100644
index 0000000..f3af57c
--- /dev/null
+++ "b/docs/prompts/3\354\243\274\354\260\250/genome_types_refactor.md"
@@ -0,0 +1,28 @@
+# Genome Types Extraction & Stepper Layering Fix
+
+## 1. 배경 및 목적
+
+- `GenomeCanvas` 내에 흩어져 있던 타입 정의를 분리해 재사용성과 유지보수성을 높이고, 관련 소비자들이 일관된 타입 소스를 사용하도록 정리.
+- 스텝퍼 UI에서 진행선이 원형 스텝 아이콘 위로 겹치는 문제를 해결해 시각적 위계를 바로잡음.
+
+## 2. 프롬프트 (User Input)
+
+- `src/components/canvas/GenomeCanvas.tsx`에 있는 `GenomeFeature`, `GenomeTrack`, `GenomeData`, `GenomeCanvasViewState`, `GenomeCanvasRenderState`, `GenomeCanvasProps`를 `src/lib/types/Genome.ts`로 이동하고, 관련 파일들이 새 경로에서 가져오도록 수정 요청.
+- 스텝퍼의 가로 라인이 원형 스텝 위에 렌더링되는 문제를 고쳐 라인이 뒤로 가도록 z-index/Tailwind 클래스 조정 요청.
+
+- 프로젝트의 타입 공유 규칙을 따르기 위해 타입을 컴포넌트 로직과 분리하고, 경로 별칭(`@/`)이 새 타입 위치를 가리키도록 보완.
+- 레이아웃 겹침 문제는 원형 스텝에 `relative`/`z-10`을 주고 라인에 낮은 z-index를 부여하는 방식으로 해결 지시.
+
+## 3. AI 응답 요약 (AI Output)
+
+- `src/lib/types/Genome.ts`를 신설해 Genome 관련 타입 6종을 집약하고, `GenomeCanvas`에서 선언하던 타입을 제거 후 새 모듈에서 임포트하도록 수정.
+- 타입 소비 파일(`app/page.tsx`, `store/useViewStore.ts`)도 모두 새 타입 경로(`@/lib/types/Genome`)로 교체.
+- `tsconfig.json`의 `paths`를 `@/* -> ./src/*` 우선으로 확장해 새 타입 위치를 해석하도록 조정.
+- 스텝퍼 진행선에 `z-0`, 스텝 버튼에 `relative z-10`을 적용해 라인이 원 위로 올라오지 않도록 레이어링 수정.
+- 정리 후 `npm run lint` 수행, 경고/에러 없이 통과 확인.
+
+## 4. 결과 및 적용 (Result)
+
+- 적용 파일: `src/lib/types/Genome.ts`, `components/canvas/GenomeCanvas.tsx`, `app/page.tsx`, `store/useViewStore.ts`, `tsconfig.json`.
+- 효과: Genome 타입이 단일 모듈로 분리되어 재사용성 확보 및 경로 혼선 제거. 스텝퍼 시각 계층 개선으로 진행선이 원형 스텝 뒤에 배치됨.
+- 테스트: `npm run lint` 성공. 추가로 `npm run build`로 전체 빌드를 확인하면 완전성을 보강할 수 있음.
From a7b18068631af930de7c433c0731a81a8287884e Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Sun, 11 Jan 2026 18:28:36 +0900
Subject: [PATCH 11/83] fix: resolve broken Material Icons rendering
---
app/layout.tsx | 6 ++++++
app/page.tsx | 8 ++++----
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/app/layout.tsx b/app/layout.tsx
index 0ca5c62..c320058 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -29,6 +29,12 @@ export default function RootLayout({
}>) {
return (
+
+
+
diff --git a/app/page.tsx b/app/page.tsx
index c29687b..2a3b9e8 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -217,7 +217,7 @@ ATGCGT..."
- tune
+ tune
Essential Settings
@@ -299,7 +299,7 @@ ATGCGT..."
-
science
+
science
Primer Properties
@@ -375,7 +375,7 @@ ATGCGT..."
-
location_on
+
location_on
Binding Location & Structure
@@ -484,7 +484,7 @@ ATGCGT..."
-
verified_user
+
verified_user
Specificity
From 372b3c3a05c98e0896965b2bd30c84b7a0fac7b7 Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Sun, 11 Jan 2026 18:33:35 +0900
Subject: [PATCH 12/83] fix: replace text arrow with icon in Next, Back button
---
app/page.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/app/page.tsx b/app/page.tsx
index 2a3b9e8..539ef3b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -776,7 +776,8 @@ ATGCGT..."
: "bg-slate-800/80 text-white hover:bg-slate-800"
}`}
>
- {"<- Back"}
+
arrow_back
+
Back
{!isLastStep && (
From 99c6ddb793f2a230e922c51b1145132ed4644519 Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Sun, 11 Jan 2026 18:37:11 +0900
Subject: [PATCH 13/83] =?UTF-8?q?docs:=203=EC=A3=BC=EC=B0=A8=20=ED=94=84?=
=?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4?=
=?UTF-8?q?=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../material_icons_fix.md" | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 "docs/prompts/3\354\243\274\354\260\250/material_icons_fix.md"
diff --git "a/docs/prompts/3\354\243\274\354\260\250/material_icons_fix.md" "b/docs/prompts/3\354\243\274\354\260\250/material_icons_fix.md"
new file mode 100644
index 0000000..b5492e3
--- /dev/null
+++ "b/docs/prompts/3\354\243\274\354\260\250/material_icons_fix.md"
@@ -0,0 +1,30 @@
+# Material Icons Loading & Navigation Icon Fix
+
+## 1. 배경 및 목적
+
+- Material Icons 폰트가 로드되지 않아 카드 헤더의 아이콘 위치에 ligature 텍스트(`tune` 등)만 보이던 문제를 해결.
+- 스텝 이동 CTA 버튼의 화살표를 텍스트(`->`, `<-`) 대신 아이콘으로 교체해 일관된 UI/아이콘 세트를 유지.
+
+## 2. 프롬프트 (User Input)
+
+- ESSENTIAL SETTINGS 헤더 옆에 `tune` 문자열이 보이는 것을 Material Icons로 바꿔 달라는 요청.
+- Material Icons가 제대로 렌더링되도록 CDN `` 또는 패키지 임포트 안내 요청.
+- Next 버튼의 `->` 텍스트를 `arrow_forward`/`chevron_right` 아이콘으로 교체하고, Flex 정렬로 간격을 두라는 요청.
+- 동일한 처리를 Back 버튼에도 적용해 달라는 후속 요청.
+
+- 전역 ``에 Material Icons CDN을 추가하고, 해당 스팬에 `material-icons` 클래스를 부여해 ligature를 아이콘으로 렌더링.
+- Next/Back CTA를 텍스트 화살표 대신 Material Icons로 치환하고 기존 flex 정렬과 gap을 활용해 수직 정렬/간격을 유지.
+
+## 3. AI 응답 요약 (AI Output)
+
+- `app/layout.tsx` ``에 Material Icons CDN 추가:
+ ``.
+- 카드 헤더 아이콘 스팬에 `material-icons` 클래스를 적용해 `tune`, `science`, `location_on`, `verified_user`가 폰트 아이콘으로 표시되도록 수정.
+- 하단 스티키 CTA에서 Next 화살표를 `material-icons` `arrow_forward` 아이콘으로 교체, 기존 `flex items-center gap-3`로 정렬/간격 유지.
+- Back 버튼도 `arrow_back` 아이콘 + “Back” 텍스트 조합으로 교체해 일관성 확보.
+
+## 4. 결과 및 적용 (Result)
+
+- 적용 파일: `app/layout.tsx`(Material Icons CDN), `app/page.tsx`(카드 헤더 아이콘 `material-icons` 클래스 적용, Next/Back 버튼 아이콘 교체).
+- 효과: Material Icons 폰트가 정상 로드되어 카드 헤더/CTA에서 의도한 아이콘이 표시되고, 스텝 이동 버튼의 화살표 표현이 통일됨.
+- 테스트: 별도 테스트 미수행. 브라우저에서 단계별 카드 헤더와 하단 CTA의 아이콘 표시 여부를 확인하면 됨.
From edcb349e917df3a00a09e7d19917325a3ecc972e Mon Sep 17 00:00:00 2001
From: hyeonK <157864988+okmac03@users.noreply.github.com>
Date: Mon, 12 Jan 2026 00:03:20 +0900
Subject: [PATCH 14/83] =?UTF-8?q?docs:=201~3=EC=A3=BC=EC=B0=A8=20=EC=A3=BC?=
=?UTF-8?q?=EA=B0=84=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EC=97=85=EB=8D=B0?=
=?UTF-8?q?=EC=9D=B4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 54 ++++++++++++++++++++++++
docs/screenshots/week2_screenshot.png | Bin 0 -> 108767 bytes
docs/screenshots/week3_screenshot#1.png | Bin 0 -> 228450 bytes
docs/screenshots/week3_screenshot#2.png | Bin 0 -> 212363 bytes
docs/screenshots/week3_screenshot#3.png | Bin 0 -> 204803 bytes
docs/screenshots/week3_screenshot#4.png | Bin 0 -> 338144 bytes
6 files changed, 54 insertions(+)
create mode 100644 docs/screenshots/week2_screenshot.png
create mode 100644 docs/screenshots/week3_screenshot#1.png
create mode 100644 docs/screenshots/week3_screenshot#2.png
create mode 100644 docs/screenshots/week3_screenshot#3.png
create mode 100644 docs/screenshots/week3_screenshot#4.png
diff --git a/README.md b/README.md
index 5d0d9e6..63dd575 100644
--- a/README.md
+++ b/README.md
@@ -82,3 +82,57 @@ npm install
# 5. 개발 서버 실행
npm run dev
```
+
+
+## 주간 진행 상황
+### Week 1 (25.12.22 - 25.12.28)
+- 작업 내역:
+ - 기술스택 선정
+ - Next.js: 메인 페이지(app/page.tsx)와 전역 레이아웃을 구성, 정적 리소스 관리·헤드 설정
+ - Typescript: 컴포넌트·전역 스토어·유틸 타입을 명시
+ - Canvas API: 캔버스 컨텍스트를 직접 처리; 대용량 서열 랜더링, 줌/패닝 변환, 텍스트/바 도형 그리기
+ - Zustand: 캔버스 뷰 상태, 리셋/업데이트 액션의 전역 관리
+ - Vercel: Next.js앱 배포
+ - 프로젝트 기본 아키텍처 및 스켈레톤 코드 구성
+- AI 활용: Gemini로 자세한 내용 프롬프트로 작성, codex로 프로젝트 아키텍처 및 스켈레톤 코드 작성.
+- 다음 주 계획: page.tsx, layout.tsx 구현, 목 데이터 출력 해보기
+
+### Week 2 (25.12.29 - 25.01.04)
+- 작업 내역:
+ - 더미 데이터로 페이지에 연결
+ - 뷰 상태(Zustand)와 줌·패닝 동작을 정돈
+- AI 활용:
+ - codex 활용하여 layout.tsx, page.tsx 세부 구현 및 디버깅
+- 완료 기능:
+ - 목 데이터의 출력 상태 확인
+- 테스트 결과:
+ - 
+- 다음 주 계획: 스펙 기반 초기 입력 폼과 검증 로직 착수.
+
+### Week 3 (26.01.05 ~ 26.01.11)
+- 작업 내역:
+ - PCR 프라이머 디자인 스펙을 작성해 1-based 규칙·IUPAC 제한·성능 목표를 포함한 요구사항과 시나리오를 정리
+ - 메인 UI 디자인 결정; 다크 톤 4단계 스텝 플로우로 리워크
+ - Genome 타입 분리
+- AI 활용:
+ - stitch, figma에 같은 프롬프트를 넣고 디자인을 비교, 채택.
+- 완료 기능:
+ - 단계별 UI 구현 완료
+ - 1단계: 시퀀스 입력(FASTA/raw textarea)
+ - 2단계: Primer Properties: GC% 범위, 최대 Tm 차이, GC Clamp 온/오프, Poly-X 제한, 농도/염 조건 입력
+ - 3단계: Binding Location: Search range 시작/끝, Exon junction 고려 여부, Intron 포함 여부와 Intron size 범위, Restriction enzyme 목록/선택.
+ - 4단계: 결과물 출력
+- 테스트 결과:
+ - 1단계
+
+ 
+ - 2단계
+
+ 
+ - 3단계
+
+ 
+ - 4단계
+
+ 
+- 다음 주 계획: 실제 데이터 연동, GenomeCanvas 미리보기·컨트롤 마무리.
diff --git a/docs/screenshots/week2_screenshot.png b/docs/screenshots/week2_screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..b17f7c53bf23ca2dc96a649255a74ab4c1bb9fd3
GIT binary patch
literal 108767
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