Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -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=
33 changes: 31 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- master
- develop

concurrency:
Expand All @@ -19,7 +20,7 @@ jobs:
runs-on: ubuntu-latest

env:
NEXT_TELEMETRY_DISABLED: '1'
NEXT_TELEMETRY_DISABLED: "1"

steps:
- name: Checkout
Expand All @@ -34,11 +35,39 @@ 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

- name: Test
run: npm test

- name: Build
run: npm run build
run: npm run build
84 changes: 0 additions & 84 deletions app/api/v1/primer/design/route.ts

This file was deleted.

35 changes: 28 additions & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Comment on lines +155 to +158
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 사용자에게 노출되는 에러 메시지가 영어로 표시되고 있는데, 같은 파일의 다른 검증/경고 메시지는 한국어로 제공되고 있습니다. 사용자 경험 일관성을 위해 한국어 메시지로 통일하거나(또는 i18n/상수로 분리) 기존 메시지 스타일에 맞춰 주세요.

Copilot uses AI. Check for mistakes.
}

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,
Expand Down
6 changes: 3 additions & 3 deletions components/steps/Step1TemplateEssential.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export default function Step1TemplateEssential({
<p className="mt-2 text-[11px] text-slate-500">
A, T, G, C 이외 문자는 입력 시 자동으로 제거됩니다.
<br />
Paste 버튼, Ctrl+V, Upload FASTA에서는 제거 전에 확인을 요청합니다.
Paste 버튼, Ctrl+V, Upload as file에서는 제거 전에 확인을 요청합니다.
</p>
{validationMessage && (
<p className="mt-2 text-xs text-red-300">{validationMessage}</p>
Expand All @@ -334,7 +334,7 @@ export default function Step1TemplateEssential({
<input
ref={fileInputRef}
type="file"
accept=".fa,.fasta,.txt"
accept=".fa,.fasta,.fna,.txt"
className="hidden"
onChange={handleFileChange}
/>
Expand All @@ -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
</button>
<button
type="button"
Expand Down
33 changes: 21 additions & 12 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
// ... 기타 설정들 ...
const DEFAULT_LOCAL_BACKEND_URL = "http://127.0.0.1:8000";
const configuredBackendUrl = process.env.BACKEND_URL?.trim();

if (!configuredBackendUrl && process.env.NODE_ENV === "production") {
throw new Error(
"BACKEND_URL must be set in production. Set BACKEND_URL to your backend origin.",
);
}

async rewrites() {
// BACKEND_URL 값이 없으면 로컬(8000)로 연결
const backendUrl = process.env.BACKEND_URL || "http://127.0.0.1:8000";
const backendUrl = (configuredBackendUrl || DEFAULT_LOCAL_BACKEND_URL).replace(
/\/+$/,
"",
);

return [
{
source: "/api/v1/:path*",
destination: `${backendUrl}/:path*`, // 여기를 변수로 변경
},
];
},
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${backendUrl}/:path*`,
},
];
},
};

export default nextConfig;
9 changes: 5 additions & 4 deletions src/lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import axios from "axios";

const DEFAULT_API_BASE_URL = "/api";
const apiBaseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL?.trim() || DEFAULT_API_BASE_URL;

export const apiClient = axios.create({
// 모든 API 호출을 /api/v1 이하 상대 경로로 강제합니다.
baseURL: "/api/v1",
baseURL: apiBaseUrl,
headers: {
"Content-Type": "application/json",
},
// 필요한 경우 쿠키 공유를 위해 아래 옵션을 사용하세요.
// withCredentials: true,
});

export const api = apiClient;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/api/primer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const postDesignPrimers = async (
payload: PrimerDesignRequest,
): Promise<PrimerDesignResponse> => {
const response: AxiosResponse<PrimerDesignResponse> = await apiClient.post(
"/primer/design",
"/design",
payload,
);

Expand Down
25 changes: 0 additions & 25 deletions src/lib/mocks/demoGenome.ts

This file was deleted.

Loading