DM Log

[AI 프로젝트 #1] PDF Q&A #3: React + Vite 프론트엔드 UI 구축 본문

PJT/AI PJT

[AI 프로젝트 #1] PDF Q&A #3: React + Vite 프론트엔드 UI 구축

Dev. Dong 2025. 10. 19. 21:05

서론

  • React + Vite 프론트엔드 UI를 구축하여 실제로 PDF를 업로드하고 AI에게 질문을 던져 응답을 확인할 수 있는 화면 설계

프론트엔드 디렉토리 구조

 frontend/
├── apps/
│   └── pdf/                 # PDF Q&A 프론트엔드
│       ├── src/
│       │   ├── components/
│       │   │   ├── UploadForm.tsx
│       │   │   └── ChatBox.tsx
│       │   ├── api/pdfApi.ts
│       │   └── main.tsx
│       ├── package.json
│       └── tsconfig.json
├── packages/
│   ├── ui/
│   ├── styles/
│   └── utils/
├── turbo.json
└── package.json

패키지 설치

  • axios와 emotion 라이브러리를 공용으로 설치
# frontend 루트에서 실행
cd frontend

# axios (API 통신)
npm install axios

# emotion (전역 스타일)
npm install @emotion/react @emotion/styled
 

axios 유틸 설정

  • /packages/utils/src/api/axiosInstance.ts
import axios from 'axios'

const api = axios.create({
  baseURL: "http://localhost:5000",
  headers: {
    "Content-Type": "multipart/form-data",
  },
});

api.interceptors.request.use((config) => {
  console.log(`${config.url}로 요청 중`)
  return config;
});

api.interceptors.response.use(
  (res) => res,
  (error) => {
    console.error("요청 실패", error);
    return Promise.reject(error);
  }
)

export default api;
  • /utils/src/index.ts
export {default as api} from "./api/axiosInstance"

PDF 업로드 UI

  • /apps/pdf/src/api/pdfApi.ts
import {api} from "utils";

export async function uploadPDF(file: File) {
  const formData = new FormData();
  formData.append("file", file);
  const {data} = await api.post("/upload", formData);
  return data
}

export async function askQuestion(question: string) {
  const formData = new FormData();
  formData.append("question", question);
  const { data } = await api.post("/ask", formData);
  return data;
}

PDF 업로드 UI

  • /apps/pdf/src/components/UploadForm.tsx
 
/** @jsxImportSource @emotion/react */
import { useState } from "react";
import { uploadPDF } from "../api/pdfApi";

const UploadForm = ({ onUpload }: { onUpload: (filename: string) => void }) => {
  const [file, setFile] = useState<File | null>(null);
  const [loading, setLoading] = useState(false);

  const handleUpload = async () => {
    if (!file) return alert("PDF 파일을 선택하세요.");
    setLoading(true);
    try {
      const res = await uploadPDF(file);
      onUpload(res.filename);
      alert("파일 업로드 완료!");
    } catch (err) {
      console.error(err);
      alert("업로드 실패");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <input type="file" accept="application/pdf" onChange={(e) => setFile(e.target.files?.[0] || null)} />
      <button onClick={handleUpload} disabled={loading}>
        {loading ? "업로드 중..." : "업로드"}
      </button>
    </div>
  );
};

export default UploadForm;

질문 입력 & 답변 표시 UI

  • /apps/pdf/src/components/ChatBox.tsx
/** @jsxImportSource @emotion/react */
import { useState } from "react";
import { askQuestion } from "../api/pdfApi";

const ChatBox = () => {
  const [question, setQuestion] = useState("");
  const [answer, setAnswer] = useState("");

  const handleAsk = async () => {
    if (!question) return;
    try {
      const res = await askQuestion(question);
      setAnswer(res.answer);
    } catch (err) {
      console.error(err);
      alert("질문 처리 중 오류 발생");
    }
  };

  return (
    <div>
      <textarea
        placeholder="질문을 입력하세요..."
        value={question}
        onChange={(e) => setQuestion(e.target.value)}
        rows={3}
        style={{ width: "100%", marginBottom: "8px" }}
      />
      <button onClick={handleAsk}>질문하기</button>

      {answer && (
        <div style={{ marginTop: "16px", whiteSpace: "pre-wrap" }}>
          <strong>답변:</strong> {answer}
        </div>
      )}
    </div>
  );
};

export default ChatBox;

전체 페이지 구성

  • /apps/pdf/src/main.tsx
/** @jsxImportSource @emotion/react */
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import UploadForm from "./components/UploadForm";
import ChatBox from "./components/ChatBox";
import { GlobalStyle } from "styles";

const App = () => {
  const [uploadedFile, setUploadedFile] = useState("");

  return (
    <>
      <GlobalStyle />
      <div style={{ padding: "24px" }}>
        <h1>📄 PDF Q&A</h1>
        <UploadForm onUpload={setUploadedFile} />
        {uploadedFile && (
          <>
            <hr style={{ margin: "24px 0" }} />
            <ChatBox />
          </>
        )}
      </div>
    </>
  );
};

ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

실행

cd frontend
npm install
npx turbo run dev --filter=pdf