CTO 과제 평가 리포트 기반 개선 사항 목록.
현재 문제: 메인 페이지 전체가 Client Component로, Next.js 서버 렌더링 이점(초기 로딩, SEO)을 활용하지 못함.
현재 코드 (src/app/page.tsx):
"use client";
import { useState } from "react";
import { executeQuery } from "./actions";
// ...
export default function Home() {
const [result, setResult] = useState<QueryResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (question: string) => { /* ... */ };
return (
<div className="mx-auto min-h-screen max-w-4xl px-4 py-8">
<header>...</header>
<main>
<QueryInput onSubmit={handleSubmit} isLoading={isLoading} />
{result?.error && <ErrorMessage message={result.error} />}
{result?.success && (
<>
<SqlViewer ... />
<ChartView ... />
<ResultsTable ... />
</>
)}
</main>
</div>
);
}권장 코드:
src/app/page.tsx (Server Component):
import { QueryContainer } from "@/components/query-container";
export default function Home() {
return (
<div className="mx-auto min-h-screen max-w-4xl px-4 py-8">
<header className="mb-8 text-center">
<h1 className="text-3xl font-bold tracking-tight">Text-to-SQL</h1>
<p className="mt-2 text-muted-foreground">
자연어로 데이터를 조회하고 시각화하세요
</p>
</header>
<main className="space-y-6">
<QueryContainer />
</main>
</div>
);
}src/components/query-container.tsx (Client Component):
"use client";
import { useState } from "react";
import { executeQuery } from "@/app/actions";
import { QueryInput } from "@/components/query-input";
import { SqlViewer } from "@/components/sql-viewer";
import { ResultsTable } from "@/components/results-table";
import { ChartView } from "@/components/chart-view";
import { ErrorMessage } from "@/components/error-message";
import type { QueryResult } from "@/types";
export function QueryContainer() {
const [result, setResult] = useState<QueryResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (question: string) => {
setIsLoading(true);
setResult(null);
try {
const data = await executeQuery(question);
setResult(data);
} catch {
setResult({
success: false,
sql: "",
rows: [],
columns: [],
chartConfig: null,
explanation: "",
error: "요청 처리 중 오류가 발생했습니다.",
});
} finally {
setIsLoading(false);
}
};
return (
<>
<QueryInput onSubmit={handleSubmit} isLoading={isLoading} />
{result?.error && <ErrorMessage message={result.error} />}
{result?.success && (
<>
<SqlViewer sql={result.sql} explanation={result.explanation} />
{result.chartConfig && (
<ChartView config={result.chartConfig} rows={result.rows} />
)}
<ResultsTable columns={result.columns} rows={result.rows} />
</>
)}
</>
);
}현재 문제: ! 단언으로 환경변수 누락 시 cryptic 런타임 에러 발생.
현재 코드 (src/lib/supabase.ts):
const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
export const supabase = createClient(supabaseUrl, supabaseServiceRoleKey);권장 코드:
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl) throw new Error("SUPABASE_URL 환경변수가 설정되지 않았습니다.");
if (!supabaseServiceRoleKey) throw new Error("SUPABASE_SERVICE_ROLE_KEY 환경변수가 설정되지 않았습니다.");
export const supabase = createClient(supabaseUrl, supabaseServiceRoleKey);현재 문제: AI가 LIMIT을 빼먹을 경우 수천 행이 한 번에 DOM에 렌더링될 수 있음.
현재 코드 (src/components/results-table.tsx):
<TableBody>
{rows.map((row, i) => (
<TableRow key={i}>
{columns.map((col) => (
<TableCell key={col}>{String(row[col] ?? "")}</TableCell>
))}
</TableRow>
))}
</TableBody>권장 코드:
const MAX_DISPLAY_ROWS = 100;
const displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
const hasMore = rows.length > MAX_DISPLAY_ROWS;
// ...
<TableBody>
{displayRows.map((row, i) => (
<TableRow key={i}>
{columns.map((col) => (
<TableCell key={col}>{String(row[col] ?? "")}</TableCell>
))}
</TableRow>
))}
</TableBody>
{hasMore && (
<p className="mt-2 text-sm text-muted-foreground">
전체 {rows.length}건 중 {MAX_DISPLAY_ROWS}건만 표시합니다.
</p>
)}현재 문제: 모든 에러가 동일한 형태로 표시됨. 사용자가 "재시도해야 하는지" vs "질문을 바꿔야 하는지" 판단 불가.
현재 코드 (src/app/actions.ts):
} catch (err) {
return {
success: false,
sql: "",
rows: [],
columns: [],
chartConfig: null,
explanation: "",
error: err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.",
};
}권장 코드:
} catch (err) {
const message = err instanceof Error ? err.message : "";
let userMessage: string;
if (message.includes("rate limit") || message.includes("quota")) {
userMessage = "AI API 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.";
} else if (message.includes("fetch") || message.includes("network")) {
userMessage = "네트워크 오류가 발생했습니다. 인터넷 연결을 확인해주세요.";
} else if (message.includes("timeout")) {
userMessage = "요청 시간이 초과되었습니다. 더 간단한 질문으로 시도해주세요.";
} else {
userMessage = "요청 처리 중 오류가 발생했습니다. 질문을 바꿔서 다시 시도해주세요.";
}
return {
success: false,
sql: "",
rows: [],
columns: [],
chartConfig: null,
explanation: "",
error: userMessage,
};
}현재 문제: <pre><code> 태그만 사용하여 SQL이 단색 텍스트로 표시됨.
현재 코드 (src/components/sql-viewer.tsx):
<pre className="overflow-x-auto rounded-md bg-muted p-4 text-sm">
<code>{sql}</code>
</pre>권장 방안: 경량 하이라이터 사용 또는 CSS 기반 키워드 강조.
pnpm add shikiimport { codeToHtml } from "shiki";
// Server Component에서 사용 시:
const html = await codeToHtml(sql, { lang: "sql", theme: "github-light" });
return <div dangerouslySetInnerHTML={{ __html: html }} />;또는 외부 라이브러리 없이 간단한 정규식 하이라이팅:
function highlightSQL(sql: string) {
const keywords = /\b(SELECT|FROM|WHERE|JOIN|ON|GROUP BY|ORDER BY|LIMIT|AS|AND|OR|NOT|IN|BETWEEN|LIKE|IS|NULL|COUNT|SUM|AVG|MIN|MAX|DISTINCT|HAVING|UNION|WITH|CASE|WHEN|THEN|ELSE|END|LEFT|RIGHT|INNER|OUTER|DESC|ASC)\b/gi;
return sql.replace(keywords, '<span class="font-bold text-blue-600">$1</span>');
}
// 사용
<pre className="overflow-x-auto rounded-md bg-muted p-4 text-sm">
<code dangerouslySetInnerHTML={{ __html: highlightSQL(sql) }} />
</pre>