빌드 깨짐 수정

This commit is contained in:
2025-08-05 11:28:44 +09:00
parent c2fa1ec589
commit 9527b7d385
12 changed files with 181 additions and 51 deletions

12
TODO.md
View File

@@ -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 오류 해결.
- **접근성(a11y) 수정**: `DynamicTable`의 컬럼 리사이저에 `slider` 역할을 부여하여 웹 접근성 lint 오류 해결.

41
bff-server.js Normal file
View File

@@ -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}`);
});

10
docker-entrypoint.sh Executable file
View File

@@ -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 "$@"

36
nginx.conf Normal file
View File

@@ -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;
}
}
}

View File

@@ -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 (
<Routes>
{/* 기본 경로 리디렉션 */}
<Route
path="/"
element={
<Navigate
to={`/projects/${defaultProjectId}/channels/${defaultChannelId}/feedbacks`}
<Suspense fallback={<div className="text-center p-8"> ...</div>}>
<Routes>
{/* 기본 경로 리디렉션 */}
<Route
path="/"
element={
<Navigate
to={`/projects/${defaultProjectId}/channels/${defaultChannelId}/feedbacks`}
/>
}
/>
{/* 프로젝트 기반 레이아웃 */}
<Route path="/projects/:projectId" element={<MainLayout />}>
<Route
path="channels/:channelId/feedbacks"
element={<FeedbackListPage />}
/>
<Route
path="channels/:channelId/feedbacks/new"
element={<FeedbackCreatePage />}
/>
<Route
path="channels/:channelId/feedbacks/:feedbackId"
element={<FeedbackDetailPage />}
/>
}
/>
{/* 프로젝트 기반 레이아웃 */}
<Route path="/projects/:projectId" element={<MainLayout />}>
<Route
path="channels/:channelId/feedbacks"
element={<FeedbackListPage />}
/>
<Route
path="channels/:channelId/feedbacks/new"
element={<FeedbackCreatePage />}
/>
<Route
path="channels/:channelId/feedbacks/:feedbackId"
element={<FeedbackDetailPage />}
/>
{/* 채널 비종속 페이지 */}
<Route path="issues" element={<IssueListPage />} />
<Route path="issues/:issueId" element={<IssueDetailPage />} />
</Route>
{/* 채널 비종속 페이지 */}
<Route path="issues" element={<IssueListPage />} />
<Route path="issues/:issueId" element={<IssueDetailPage />} />
</Route>
{/* 전체 레이아웃 */}
<Route element={<MainLayout />}>
<Route path="/profile" element={<ProfilePage />} />
</Route>
{/* 전체 레이아웃 */}
<Route element={<MainLayout />}>
<Route path="/profile" element={<ProfilePage />} />
</Route>
{/* 잘못된 접근을 위한 리디렉션 */}
<Route path="*" element={<Navigate to="/" />} />
</Routes>
{/* 잘못된 접근을 위한 리디렉션 */}
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
);
}

View File

@@ -70,6 +70,7 @@ export function DynamicForm({
return (
<Textarea
{...commonProps}
value={String(commonProps.value)}
onChange={(e) => handleFormChange(field.id, e.target.value)}
placeholder={field.readOnly ? "" : `${field.name}...`}
rows={5}
@@ -80,6 +81,7 @@ export function DynamicForm({
return (
<Input
{...commonProps}
value={String(commonProps.value)}
type={field.type}
onChange={(e) => handleFormChange(field.id, e.target.value)}
placeholder={field.readOnly ? "" : field.name}
@@ -88,7 +90,7 @@ export function DynamicForm({
case "select":
return (
<Select
value={commonProps.value}
value={String(commonProps.value)}
onValueChange={(value) => handleFormChange(field.id, value)}
disabled={field.readOnly}
>

View File

@@ -375,8 +375,8 @@ export function DynamicTable<TData extends BaseData>({
<div
role="slider"
aria-label={`컬럼 너비 조절: ${header.id}`}
aria-valuemin={header.column.minSize}
aria-valuemax={header.column.maxSize}
aria-valuemin={header.column.columnDef.minSize}
aria-valuemax={header.column.columnDef.maxSize}
aria-valuenow={header.column.getSize()}
tabIndex={0}
onMouseDown={header.getResizeHandler()}

View File

@@ -50,8 +50,6 @@ export function Header() {
return `/projects/${projectId}/channels/${channelId}${basePath}`;
};
const homePath = projectId ? `/projects/${projectId}` : "/";
return (
<header className="border-b">
<div className="container mx-auto flex h-16 items-center">

View File

@@ -1,6 +1,9 @@
import React from "react";
interface ThemeProviderProps {
children: React.ReactNode;
}
export declare function ThemeProvider({
children,
}: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
}: ThemeProviderProps): React.ReactElement;

View File

@@ -15,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider projectId={projectId}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<ThemeProvider>
<App />
</ThemeProvider>
</AuthProvider>

View File

@@ -59,8 +59,8 @@ export function FeedbackListPage() {
const renderExpandedRow = (row: Row<Feedback>) => (
<div className="p-4 bg-muted rounded-md">
<h4 className="font-bold text-lg mb-2">{row.original.title}</h4>
<p className="whitespace-pre-wrap">{row.original.contents}</p>
<h4 className="font-bold text-lg mb-2">{String(row.original.title ?? '')}</h4>
<p className="whitespace-pre-wrap">{String(row.original.contents ?? '')}</p>
</div>
);

View File

@@ -13,6 +13,7 @@ interface ApiField {
export interface Feedback {
id: string;
content: string;
updatedAt: string;
[key: string]: unknown; // 동적 필드를 위해 인덱스 시그니처 사용
}