-
Notifications
You must be signed in to change notification settings - Fork 4
ATGC 염기서열 입력 검증 및 자동 대문자 변환 기능 추가 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
7d84974
b695f65
4647dc7
eb7a67f
fa928e4
5b30ad5
cceb44c
68c6ccb
8b120d2
d8d8543
747f7c8
ce55768
15263d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,10 @@ import { | |
| import { demoGenome } from "@/lib/mocks/demoGenome"; | ||
| import type { GenomeData, PrimerDesignResponseUI } from "@/types"; | ||
| import { useViewStore } from "@/store/useViewStore"; | ||
| import { | ||
| getInvalidStep1TemplateSequenceChar, | ||
| normalizeStep1TemplateSequence, | ||
| } from "../src/lib/parsers/step1TemplateSequence"; | ||
| import Step1TemplateEssential from "@/components/steps/Step1TemplateEssential"; | ||
| import Step2PrimerProperties from "@/components/steps/Step2PrimerProperties"; | ||
| import Step3BindingLocation from "@/components/steps/Step3BindingLocation"; | ||
|
|
@@ -56,13 +60,37 @@ export default function Home() { | |
| const [isLoading, setIsLoading] = useState(false); | ||
| const [apiResult, setApiResult] = useState<PrimerDesignResponseUI | null>(null); | ||
| const [errorMessage, setErrorMessage] = useState<string | null>(null); | ||
| const [step1WarningMessage, setStep1WarningMessage] = useState<string | null>(null); | ||
| const [isModalOpen, setIsModalOpen] = useState(false); | ||
| const totalSteps = steps.length; | ||
| const handleStepChange = (next: number) => { | ||
| const clamped = Math.min(Math.max(next, 1), totalSteps) as 1 | 2 | 3 | 4; | ||
| setStep(clamped); | ||
| }; | ||
| const handleNext = () => handleStepChange(step + 1); | ||
| const clearStep1Warning = () => { | ||
| if (step1WarningMessage) { | ||
| setStep1WarningMessage(null); | ||
| } | ||
| }; | ||
| const validateStep1Sequence = () => { | ||
| const invalidChar = getInvalidStep1TemplateSequenceChar(sequenceInputRef.current); | ||
| if (!invalidChar) { | ||
| setStep1WarningMessage(null); | ||
| return true; | ||
| } | ||
|
|
||
| const warningMessage = `대소문자 구분 없이 A, T, G, C만 입력 가능합니다. 잘못된 문자를 제거해 주세요.`; | ||
| setStep1WarningMessage(warningMessage); | ||
| alert(warningMessage); | ||
| return false; | ||
| }; | ||
| const handleNext = () => { | ||
| if (step === 1 && !validateStep1Sequence()) { | ||
| return; | ||
| } | ||
|
|
||
| handleStepChange(step + 1); | ||
| }; | ||
| const handleBack = () => handleStepChange(step - 1); | ||
| const isLastStep = step === totalSteps; | ||
|
|
||
|
|
@@ -74,11 +102,22 @@ export default function Home() { | |
| ); | ||
|
|
||
| const handleGenerate = async () => { | ||
| const inputSequence = sequenceInputRef.current; | ||
| const rawInputSequence = sequenceInputRef.current; | ||
| const inputSequence = normalizeStep1TemplateSequence(rawInputSequence); | ||
| if (rawInputSequence.trim().length > 0 && inputSequence.length === 0) { | ||
| const warningMessage = | ||
| "전송할 수 있는 유효한 염기서열이 없습니다. A, T, G, C 문자만 입력해 주세요."; | ||
| setStep1WarningMessage(warningMessage); | ||
| setErrorMessage(warningMessage); | ||
| alert(warningMessage); | ||
| return; | ||
|
Comment on lines
111
to
+131
|
||
| } | ||
|
|
||
| const targetSeq = | ||
| inputSequence && inputSequence.trim().length > 0 | ||
| ? inputSequence.trim() | ||
| : "ATGCGTACGTAGCTAGCTAGCTAGCTAATGCGTACGTAGCTAGCTAGCTAGCTA"; | ||
|
|
||
| const payload: AnalyzeRequestInput = { | ||
| target_sequence: targetSeq, | ||
| species: "Homo sapiens", | ||
|
|
@@ -149,6 +188,8 @@ export default function Home() { | |
| {step === 1 && ( | ||
| <Step1TemplateEssential | ||
| sequenceRef={sequenceInputRef} | ||
| validationMessage={step1WarningMessage} | ||
| onSequenceChange={clearStep1Warning} | ||
| /> | ||
| )} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,15 +3,22 @@ | |||||||||||||||||
| import { | ||||||||||||||||||
| type ClipboardEvent, | ||||||||||||||||||
| type ChangeEvent, | ||||||||||||||||||
| type FormEvent, | ||||||||||||||||||
| type MutableRefObject, | ||||||||||||||||||
| useEffect, | ||||||||||||||||||
| useRef, | ||||||||||||||||||
| useState, | ||||||||||||||||||
| } from "react"; | ||||||||||||||||||
| import { SlidersHorizontal } from "lucide-react"; | ||||||||||||||||||
| import { | ||||||||||||||||||
| getInvalidStep1TemplateSequenceChars, | ||||||||||||||||||
| sanitizeStep1TemplateSequenceInput, | ||||||||||||||||||
| } from "../../src/lib/parsers/step1TemplateSequence"; | ||||||||||||||||||
|
|
||||||||||||||||||
| type Step1TemplateEssentialProps = { | ||||||||||||||||||
| sequenceRef: MutableRefObject<string>; | ||||||||||||||||||
| validationMessage?: string | null; | ||||||||||||||||||
| onSequenceChange?: (value: string) => void; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const countAlphabeticChars = (value: string) => { | ||||||||||||||||||
|
|
@@ -47,6 +54,8 @@ const getPreviewValue = (value: string) => { | |||||||||||||||||
|
|
||||||||||||||||||
| export default function Step1TemplateEssential({ | ||||||||||||||||||
| sequenceRef, | ||||||||||||||||||
| validationMessage, | ||||||||||||||||||
| onSequenceChange, | ||||||||||||||||||
| }: Step1TemplateEssentialProps) { | ||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement | null>(null); | ||||||||||||||||||
| const textareaRef = useRef<HTMLTextAreaElement | null>(null); | ||||||||||||||||||
|
|
@@ -77,6 +86,7 @@ export default function Step1TemplateEssential({ | |||||||||||||||||
|
|
||||||||||||||||||
| const updateSequence = (value: string, countMode: "defer" | "immediate" = "defer") => { | ||||||||||||||||||
| sequenceRef.current = value; | ||||||||||||||||||
| onSequenceChange?.(value); | ||||||||||||||||||
| const nextIsLargeSequence = value.length > MAX_EDITOR_CHARS; | ||||||||||||||||||
| setIsLargeSequenceMode(nextIsLargeSequence); | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -104,21 +114,63 @@ export default function Step1TemplateEssential({ | |||||||||||||||||
|
|
||||||||||||||||||
| const focusTextarea = () => textareaRef.current?.focus(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const appendSequence = (next: string) => { | ||||||||||||||||||
| const sanitized = next.trim(); | ||||||||||||||||||
| const confirmInvalidRemoval = (rawSequence: string) => { | ||||||||||||||||||
| const invalidChars = getInvalidStep1TemplateSequenceChars(rawSequence); | ||||||||||||||||||
| if (invalidChars.length === 0) return true; | ||||||||||||||||||
|
|
||||||||||||||||||
| const previewInvalidChars = invalidChars.slice(0, 8).join(", "); | ||||||||||||||||||
| const extraKinds = invalidChars.length > 8 ? ` 외 ${invalidChars.length - 8}종` : ""; | ||||||||||||||||||
| return window.confirm( | ||||||||||||||||||
| `붙여넣으려는 데이터에 A, T, G, C 이외 문자가 포함되어 있습니다 (${previewInvalidChars}${extraKinds}). 해당 문자를 제거하고 계속할까요?`, | ||||||||||||||||||
| ); | ||||||||||||||||||
|
||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const appendSequence = ( | ||||||||||||||||||
| next: string, | ||||||||||||||||||
| options: { requireInvalidRemovalConsent?: boolean } = {}, | ||||||||||||||||||
| ) => { | ||||||||||||||||||
| if (options.requireInvalidRemovalConsent && !confirmInvalidRemoval(next)) { | ||||||||||||||||||
| focusTextarea(); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!sanitized) { | ||||||||||||||||||
| const sanitizedChunk = sanitizeStep1TemplateSequenceInput(next); | ||||||||||||||||||
| if (!sanitizedChunk) { | ||||||||||||||||||
| focusTextarea(); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const currentValue = sequenceRef.current; | ||||||||||||||||||
| const appendedValue = currentValue ? `${currentValue}\n${sanitized}` : sanitized; | ||||||||||||||||||
| const appendedValue = `${currentValue}${sanitizedChunk}`; | ||||||||||||||||||
|
||||||||||||||||||
| const appendedValue = `${currentValue}${sanitizedChunk}`; | |
| const needsSeparator = | |
| currentValue.length > 0 && | |
| !currentValue.endsWith("\n") && | |
| !sanitizedChunk.startsWith("\n"); | |
| const appendedValue = needsSeparator | |
| ? `${currentValue}\n${sanitizedChunk}` | |
| : `${currentValue}${sanitizedChunk}`; |
Outdated
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
event.preventDefault()가 사용자 확인(confirm) 전에 호출되어 잘못된 동작이 발생합니다. 현재 211번 줄에서 기본 paste 동작을 차단한 후, 215번 줄에서 사용자에게 확인을 요청합니다. 사용자가 "취소"를 클릭하면 아무것도 붙여넣어지지 않습니다 (기본 동작도, 정제된 데이터도 아님). event.preventDefault()를 215번 줄 확인 이후로 이동하여, 사용자가 확인한 경우에만 기본 동작을 차단하고 정제된 데이터를 삽입하도록 수정해 주세요.
Outdated
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line 210에서 sanitizedChunk === rawChunk 조건이 역설적입니다. 이 조건이 true일 때 함수가 early return하므로, 실제로는 입력이 이미 유효할 때만 반환하고 무효한 입력만 처리합니다. 그러나 line 209에서 sanitizeStep1TemplateSequenceInput이 FASTA 헤더와 공백도 제거하므로, 유효한 ATGC 입력이더라도 공백이 포함되어 있으면 불필요하게 처리됩니다. 로직을 재검토하세요: 유효한 입력은 그대로 통과시키고, 무효한 입력만 sanitize해야 합니다.
| insertSanitizedChunkAtSelection(event.currentTarget, rawChunk); | |
| insertSanitizedChunkAtSelection(event.currentTarget, sanitizedChunk); |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||||||||||
| const FASTA_HEADER_PREFIX = ">"; | ||||||||||||||
| const ATGC_ONLY_PATTERN = /^[ATGC]*$/i; | ||||||||||||||
| const UPPERCASE_ATGC_ONLY_PATTERN = /^[ATGC]+$/; | ||||||||||||||
|
||||||||||||||
| const UPPERCASE_ATGC_ONLY_PATTERN = /^[ATGC]+$/; | |
| const UPPERCASE_ATGC_ONLY_PATTERN = /^[ATGC]*$/; |
Outdated
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 함수가 export 되어 있지만 코드베이스 어디에서도 사용되지 않습니다. 불필요한 export는 제거하거나, 실제로 사용할 계획이 있다면 해당 위치에서 사용하세요.
| export const toUpperCaseAtgcOnly = (value: string) => | |
| const toUpperCaseAtgcOnly = (value: string) => |
Outdated
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toUpperCaseAtgcOnly 함수가 정의되어 있지만 코드베이스에서 실제로 사용되지 않고 있습니다. 사용되지 않는 export를 제거하거나, 실제로 사용할 계획이 있다면 해당 위치에서 사용하도록 수정해 주세요.
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 함수는 단순히 sanitizeStep1TemplateSequenceInput을 호출하는 wrapper입니다. 중복 레이어를 제거하고 직접 sanitizeStep1TemplateSequenceInput을 사용하거나, 이 함수가 향후 다른 로직을 추가할 계획이 있다면 주석으로 명시하세요.
| // NOTE: 이 함수는 step1 템플릿 시퀀스에 대한 "정규화(normalize)" 공개 API입니다. | |
| // 현재 구현은 sanitize 로직에만 위임하지만, 향후 추가적인 정규화/검증 단계가 | |
| // 필요해질 수 있으므로 별도 함수로 유지합니다. |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isUppercaseAtgcOnlySequence 함수가 정의되어 있지만 코드베이스에서 사용되지 않고 있습니다. 사용하지 않는 export를 제거하거나, 향후 사용할 계획이 있다면 그 의도를 주석으로 명확히 해 주세요.
| // NOTE: 현재 코드베이스에서는 사용되지 않지만, 시퀀스 입력 검증/밸리데이션 로직에서 | |
| // 재사용하기 위해 의도적으로 export를 유지합니다. |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로 추가된 파서 함수들에 대한 단위 테스트가 없습니다. 코드베이스에 vitest를 사용한 테스트 인프라가 있으므로(tests/visibleRange.test.ts 참조), 다음과 같은 케이스를 테스트하는 파일을 추가하세요: 1) 소문자 입력의 대문자 변환, 2) 잘못된 문자 제거, 3) FASTA 헤더 제거, 4) 빈 문자열 처리, 5) 유효한 시퀀스 검증.
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 커버리지가 부족합니다. 새로 추가된 step1TemplateSequence.ts 파일의 함수들(sanitizeStep1TemplateSequenceInput, getInvalidStep1TemplateSequenceChars, getInvalidStep1TemplateSequenceChar 등)에 대한 단위 테스트를 추가해 주세요. 특히 FASTA 헤더 제거, 공백 처리, 잘못된 문자 감지, 대소문자 변환 등 핵심 기능에 대한 테스트가 필요합니다.
| export const getInvalidStep1TemplateSequenceChar = (rawSequence: string) => | |
| stripFastaHeadersAndWhitespace(rawSequence).match(NON_ATGC_PATTERN)?.[0] ?? null; | |
| export const getInvalidStep1TemplateSequenceChar = (rawSequence: string) => { | |
| const invalidChars = getInvalidStep1TemplateSequenceChars(rawSequence); | |
| return invalidChars.length > 0 ? invalidChars[0] : null; | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
동일한 경고 메시지를 여러 상태에 설정하고 있습니다 (110-112번 줄).
setStep1WarningMessage,setErrorMessage, 그리고alert로 세 번 표시되는데, 이는 사용자에게 혼란을 줄 수 있습니다. 각 메커니즘의 역할을 명확히 하거나, 불필요한 중복을 제거해 주세요.