diff --git a/.env.example b/.env.example index 40935da..15f6ad2 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,11 @@ # Copy this file to .env.local and set variables as needed. -# If unset, Next.js rewrites fall back to http://127.0.0.1:8000. +# +# Backend origin used by Next.js rewrite proxy. +# - Local development default: http://127.0.0.1:8000 +# - Production: BACKEND_URL must be explicitly set. BACKEND_URL= +# +# Frontend axios base URL. +# Keep this unset to use "/api" (same-origin via Next.js rewrite). +# Set only when you intentionally bypass the rewrite proxy. +NEXT_PUBLIC_API_BASE_URL= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88a8375..06ec980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - master - develop concurrency: @@ -19,7 +20,7 @@ jobs: runs-on: ubuntu-latest env: - NEXT_TELEMETRY_DISABLED: '1' + NEXT_TELEMETRY_DISABLED: "1" steps: - name: Checkout @@ -34,6 +35,34 @@ jobs: - name: Install dependencies run: npm ci + - name: Inject BACKEND_URL + shell: bash + env: + BACKEND_URL_PRODUCTION: ${{ secrets.BACKEND_URL_PRODUCTION }} + BACKEND_URL_STAGING: ${{ secrets.BACKEND_URL_STAGING }} + run: | + # fork PR은 보안상 secrets가 전달되지 않음 → 공개 URL(또는 더미)로 빌드만 통과 + if [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then + echo "BACKEND_URL=https://primerflow-be.onrender.com" >> "$GITHUB_ENV" + exit 0 + fi + + if [ "${{ github.base_ref }}" = "main" ] || [ "${{ github.base_ref }}" = "master" ]; then + selected="$BACKEND_URL_PRODUCTION" + key="BACKEND_URL_PRODUCTION" + else + selected="$BACKEND_URL_STAGING" + key="BACKEND_URL_STAGING" + fi + + if [ -z "$selected" ]; then + echo "::error::Missing secret $key" + echo "::error::Set it in Settings -> Secrets and variables -> Actions -> Repository secrets" + exit 1 + fi + + echo "BACKEND_URL=$selected" >> "$GITHUB_ENV" + - name: Lint run: npm run lint @@ -41,4 +70,4 @@ jobs: run: npm test - name: Build - run: npm run build + run: npm run build \ No newline at end of file diff --git a/app/api/v1/primer/design/route.ts b/app/api/v1/primer/design/route.ts deleted file mode 100644 index 3b1268c..0000000 --- a/app/api/v1/primer/design/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; - -// Mock primer design endpoint that echoes user input while staying lightweight. -export async function POST(request: Request) { - const body = await request.json(); - console.log("프론트에서 받은 요청:", body); - - const templateSequence: string = - (body?.basic?.templateSequence ?? "") as string; - const cleanSequence = templateSequence.replace(/\s+/g, ""); - const length = cleanSequence.length || 1; // keep non-zero for calculations - - // Simple relative feature windows - const region = (startPct: number, endPct: number) => ({ - start_bp: Math.max(1, Math.floor(length * startPct)), - end_bp: Math.max(1, Math.floor(length * endPct)), - }); - - const featureA = region(0.1, 0.2); - const featureB = region(0.35, 0.45); - - // Dummy primer candidates positioned inside the sequence - const primerLen = Math.max(18, Math.min(28, Math.floor(length * 0.02) || 20)); - const fStart = Math.max(1, Math.floor(length * 0.15)); - const fEnd = Math.min(length, fStart + primerLen); - const rEnd = Math.max(1, length - Math.floor(length * 0.1)); - const rStart = Math.max(1, rEnd - primerLen); - - const mockResponse = { - genome: { - id: "gene_mock", - name: body?.basic?.targetOrganism ?? "Mock Gene", - sequence: cleanSequence || "N/A", - length_bp: length, - tracks: [ - { - id: "regions", - name: "Relative Windows", - features: [ - { - id: "window_A", - start: featureA.start_bp, - end: featureA.end_bp, - color: "#38bdf8", - }, - { - id: "window_B", - start: featureB.start_bp, - end: featureB.end_bp, - color: "#c084fc", - }, - ], - }, - ], - }, - candidates: [ - { - id: "primer_fwd", - sequence: cleanSequence.slice(fStart - 1, fEnd) || "ATGCATGCATGC", - start_bp: fStart, - end_bp: fEnd, - strand: "forward", - metrics: { tm_c: 60.5, gc_percent: 50 }, - }, - { - id: "primer_rev", - sequence: cleanSequence.slice(rStart - 1, rEnd) || "CGTACGTACGTA", - start_bp: rStart, - end_bp: rEnd, - strand: "reverse", - metrics: { tm_c: 59.8, gc_percent: 48 }, - }, - ], - meta: { - timestamp: new Date().toISOString(), - params: body, - }, - }; - - // 1초 뒤에 응답 (네트워크 지연 시뮬레이션) - await new Promise((resolve) => setTimeout(resolve, 1000)); - - return NextResponse.json(mockResponse); -} diff --git a/app/page.tsx b/app/page.tsx index e7cbd98..22158e1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,7 +6,6 @@ import { analyzeGenome, type AnalyzeRequestInput, } from "@/services/analysisService"; -import { demoGenome } from "@/lib/mocks/demoGenome"; import type { GenomeData, PrimerDesignResponseUI } from "@/types"; import { useViewStore } from "@/store/useViewStore"; import { @@ -31,6 +30,28 @@ const toGenomeDataFromResponse = (response: PrimerDesignResponseUI | null): Geno return { length, tracks }; }; +const DEFAULT_PREVIEW_GENOME: GenomeData = { + length: 12000, + tracks: [ + { + id: "preview-primer-candidates", + name: "Primer 후보군", + height: 28, + features: [ + { id: "p-01", start: 400, end: 1200, label: "P-01", color: "#2563eb" }, + { id: "p-02", start: 1800, end: 2600, label: "P-02", color: "#0ea5e9" }, + { id: "p-03", start: 3200, end: 4300, label: "P-03", color: "#22c55e" }, + ], + }, + { + id: "preview-target-region", + name: "Target 구간", + height: 18, + features: [{ id: "amplicon", start: 1500, end: 5200, label: "Amplicon", color: "#f97316" }], + }, + ], +}; + export default function Home() { const viewState = useViewStore((state) => state.viewState); const setViewState = useViewStore((state) => state.setViewState); @@ -118,7 +139,7 @@ export default function Home() { const handleBack = () => handleStepChange(step - 1); const isLastStep = step === totalSteps; - const previewGenome = demoGenome; + const previewGenome = DEFAULT_PREVIEW_GENOME; const trackCount = previewGenome.tracks.length; const featureCount = previewGenome.tracks.reduce( (count, track) => count + track.features.length, @@ -131,16 +152,16 @@ export default function Home() { return; } - const targetSeq = - validation.normalizedSequence && validation.normalizedSequence.trim().length > 0 - ? validation.normalizedSequence.trim() - : "ATGCGTACGTAGCTAGCTAGCTAGCTAATGCGTACGTAGCTAGCTAGCTAGCTA"; + const targetSeq = validation.normalizedSequence?.trim() ?? ""; + if (!targetSeq) { + setErrorMessage("템플릿 시퀀스를 입력해 주세요."); + return; + } const payload: AnalyzeRequestInput = { target_sequence: targetSeq, species: "Homo sapiens", analysis_type: "primer_generation", - notes: "UI mock request while backend is offline", product_size_min: 100, product_size_max: 300, tm_min: 57, diff --git a/components/steps/Step1TemplateEssential.tsx b/components/steps/Step1TemplateEssential.tsx index 8af7381..3930178 100644 --- a/components/steps/Step1TemplateEssential.tsx +++ b/components/steps/Step1TemplateEssential.tsx @@ -324,7 +324,7 @@ export default function Step1TemplateEssential({
A, T, G, C 이외 문자는 입력 시 자동으로 제거됩니다.
- Paste 버튼, Ctrl+V, Upload FASTA에서는 제거 전에 확인을 요청합니다.
+ Paste 버튼, Ctrl+V, Upload as file에서는 제거 전에 확인을 요청합니다.
{validationMessage}
@@ -334,7 +334,7 @@ export default function Step1TemplateEssential({ @@ -343,7 +343,7 @@ export default function Step1TemplateEssential({ className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-bold text-slate-300 hover:text-white hover:bg-slate-800 transition-colors border border-transparent hover:border-slate-700" onClick={handleUploadClick} > - Upload FASTA + Upload as file