diff --git a/TODO.md b/TODO.md index 102e1d3..e34a61b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,13 @@ +### 2025-08-05 11:27:58 KST +- **빌드 오류 수정**: `pnpm build` 시 발생하던 12개의 타입스크립트 오류를 모두 해결하여 빌드 프로세스를 안정화했습니다. + - `DynamicForm`: `value` prop의 타입 불일치 오류를 해결했습니다. + - `DynamicTable`: 존재하지 않는 속성(`minSize`, `maxSize`) 접근 오류를 수정했습니다. + - `Header`: 사용되지 않는 변수(`homePath`)를 제거했습니다. + - `main.tsx`: `ThemeProvider`에 잘못 전달된 prop을 제거했습니다. + - `FeedbackListPage`: 데이터 타입 불일치로 인해 발생하던 다양한 오류들을 해결했습니다. +- **빌드 성능 최적화**: `React.lazy`와 `Suspense`를 사용하여 페이지 컴포넌트를 동적으로 가져오도록(Code Splitting) 구현했습니다. 이를 통해 초기 로딩 시 번들 크기를 줄여 성능을 개선하고, 빌드 시 발생하던 청크 크기 경고를 해결했습니다. +- **개발 환경 개선**: `pnpm install` 또는 `biome` 실행 시 나타나던 `npm warn` 경고를 해결하기 위해 프로젝트 루트에 `.npmrc` 파일을 추가하여 전역 설정을 덮어쓰도록 조치했습니다. + 2025-08-05 11:00:00 KST - Descope 연동 완료 --- @@ -24,4 +34,4 @@ - **UI/UX 개선**: - `DynamicTable`의 검색창과 카드 간 여백 조정. - `IssueDetailCard`의 제목, 레이블, 컨텐츠 스타일을 개선하여 가독성 향상. - - **접근성(a11y) 수정**: `DynamicTable`의 컬럼 리사이저에 `slider` 역할을 부여하여 웹 접근성 lint 오류 해결. \ No newline at end of file + - **접근성(a11y) 수정**: `DynamicTable`의 컬럼 리사이저에 `slider` 역할을 부여하여 웹 접근성 lint 오류 해결. diff --git a/bff-server.js b/bff-server.js new file mode 100644 index 0000000..ecd09c5 --- /dev/null +++ b/bff-server.js @@ -0,0 +1,41 @@ +const express = require('express'); +const morgan = require('morgan'); +const { createProxyMiddleware } = require('http-proxy-middleware'); + +const app = express(); + +// Configuration +const PORT = process.env.PORT || 3001; +const API_SERVICE_URL = process.env.VITE_API_PROXY_TARGET; +const API_KEY = process.env.VITE_API_KEY; + +if (!API_SERVICE_URL) { + throw new Error('VITE_API_PROXY_TARGET environment variable is not set.'); +} +if (!API_KEY) { + throw new Error('VITE_API_KEY environment variable is not set.'); +} + +// Logging +app.use(morgan('dev')); + +// Proxy middleware +app.use('/api', createProxyMiddleware({ + target: API_SERVICE_URL, + changeOrigin: true, + pathRewrite: { + '^/api': '', // remove /api prefix when forwarding to the target + }, + onProxyReq: (proxyReq, req, res) => { + // Add the API key to the request header + proxyReq.setHeader('X-API-KEY', API_KEY); + }, + onError: (err, req, res) => { + console.error('Proxy error:', err); + res.status(500).send('Proxy error'); + } +})); + +app.listen(PORT, () => { + console.log(`BFF server listening on port ${PORT}`); +}); diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..79a6655 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Exit immediately if a command exits with a non-zero status. +set -e + +# Substitute environment variables in the nginx template +envsubst '${VITE_API_PROXY_TARGET}' < /etc/nginx/templates/nginx.conf.template > /etc/nginx/nginx.conf + +# Execute the command passed to this script, e.g., "nginx -g 'daemon off;'" +exec "$@" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..1b48470 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +user nginx; +worker_processes auto; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + gzip on; + + server { + listen 80; + server_name localhost; + root /app/dist; # React app's build output + index index.html; + + # Proxy API requests to the internal BFF server + location /api { + proxy_pass http://localhost:3001; # BFF server is running on port 3001 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Serve React app for all other requests + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 59ea32c..5a2ccf5 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -1,57 +1,86 @@ // src/App.tsx +import { Suspense, lazy } from "react"; import { Routes, Route, Navigate } from "react-router-dom"; import { MainLayout } from "@/components/MainLayout"; -import { FeedbackCreatePage } from "@/pages/FeedbackCreatePage"; -import { FeedbackListPage } from "@/pages/FeedbackListPage"; -import { FeedbackDetailPage } from "@/pages/FeedbackDetailPage"; -import { IssueListPage } from "@/pages/IssueListPage"; -import { IssueDetailPage } from "@/pages/IssueDetailPage"; -import { ProfilePage } from "@/pages/ProfilePage"; + +// 페이지 컴포넌트를 동적으로 import +const FeedbackListPage = lazy(() => + import("@/pages/FeedbackListPage").then((module) => ({ + default: module.FeedbackListPage, + })), +); +const FeedbackCreatePage = lazy(() => + import("@/pages/FeedbackCreatePage").then((module) => ({ + default: module.FeedbackCreatePage, + })), +); +const FeedbackDetailPage = lazy(() => + import("@/pages/FeedbackDetailPage").then((module) => ({ + default: module.FeedbackDetailPage, + })), +); +const IssueListPage = lazy(() => + import("@/pages/IssueListPage").then((module) => ({ + default: module.IssueListPage, + })), +); +const IssueDetailPage = lazy(() => + import("@/pages/IssueDetailPage").then((module) => ({ + default: module.IssueDetailPage, + })), +); +const ProfilePage = lazy(() => + import("@/pages/ProfilePage").then((module) => ({ + default: module.ProfilePage, + })), +); function App() { const defaultProjectId = import.meta.env.VITE_DEFAULT_PROJECT_ID || "1"; const defaultChannelId = import.meta.env.VITE_DEFAULT_CHANNEL_ID || "4"; return ( - - {/* 기본 경로 리디렉션 */} - 로딩 중...}> + + {/* 기본 경로 리디렉션 */} + + } + /> + + {/* 프로젝트 기반 레이아웃 */} + }> + } + /> + } + /> + } /> - } - /> - {/* 프로젝트 기반 레이아웃 */} - }> - } - /> - } - /> - } - /> + {/* 채널 비종속 페이지 */} + } /> + } /> + - {/* 채널 비종속 페이지 */} - } /> - } /> - + {/* 전체 레이아웃 */} + }> + } /> + - {/* 전체 레이아웃 */} - }> - } /> - - - {/* 잘못된 접근을 위한 리디렉션 */} - } /> - + {/* 잘못된 접근을 위한 리디렉션 */} + } /> + + ); } diff --git a/viewer/src/components/DynamicForm.tsx b/viewer/src/components/DynamicForm.tsx index 322eda0..3aacffc 100644 --- a/viewer/src/components/DynamicForm.tsx +++ b/viewer/src/components/DynamicForm.tsx @@ -70,6 +70,7 @@ export function DynamicForm({ return (