빌드 깨짐 수정
This commit is contained in:
10
TODO.md
10
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 연동 완료
|
2025-08-05 11:00:00 KST - Descope 연동 완료
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
41
bff-server.js
Normal file
41
bff-server.js
Normal 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
10
docker-entrypoint.sh
Executable 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
36
nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,46 @@
|
|||||||
// src/App.tsx
|
// src/App.tsx
|
||||||
|
import { Suspense, lazy } from "react";
|
||||||
import { Routes, Route, Navigate } from "react-router-dom";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { MainLayout } from "@/components/MainLayout";
|
import { MainLayout } from "@/components/MainLayout";
|
||||||
import { FeedbackCreatePage } from "@/pages/FeedbackCreatePage";
|
|
||||||
import { FeedbackListPage } from "@/pages/FeedbackListPage";
|
// 페이지 컴포넌트를 동적으로 import
|
||||||
import { FeedbackDetailPage } from "@/pages/FeedbackDetailPage";
|
const FeedbackListPage = lazy(() =>
|
||||||
import { IssueListPage } from "@/pages/IssueListPage";
|
import("@/pages/FeedbackListPage").then((module) => ({
|
||||||
import { IssueDetailPage } from "@/pages/IssueDetailPage";
|
default: module.FeedbackListPage,
|
||||||
import { ProfilePage } from "@/pages/ProfilePage";
|
})),
|
||||||
|
);
|
||||||
|
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() {
|
function App() {
|
||||||
const defaultProjectId = import.meta.env.VITE_DEFAULT_PROJECT_ID || "1";
|
const defaultProjectId = import.meta.env.VITE_DEFAULT_PROJECT_ID || "1";
|
||||||
const defaultChannelId = import.meta.env.VITE_DEFAULT_CHANNEL_ID || "4";
|
const defaultChannelId = import.meta.env.VITE_DEFAULT_CHANNEL_ID || "4";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Suspense fallback={<div className="text-center p-8">로딩 중...</div>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* 기본 경로 리디렉션 */}
|
{/* 기본 경로 리디렉션 */}
|
||||||
<Route
|
<Route
|
||||||
@@ -52,6 +80,7 @@ function App() {
|
|||||||
{/* 잘못된 접근을 위한 리디렉션 */}
|
{/* 잘못된 접근을 위한 리디렉션 */}
|
||||||
<Route path="*" element={<Navigate to="/" />} />
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function DynamicForm({
|
|||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
value={String(commonProps.value)}
|
||||||
onChange={(e) => handleFormChange(field.id, e.target.value)}
|
onChange={(e) => handleFormChange(field.id, e.target.value)}
|
||||||
placeholder={field.readOnly ? "" : `${field.name}...`}
|
placeholder={field.readOnly ? "" : `${field.name}...`}
|
||||||
rows={5}
|
rows={5}
|
||||||
@@ -80,6 +81,7 @@ export function DynamicForm({
|
|||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
value={String(commonProps.value)}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
onChange={(e) => handleFormChange(field.id, e.target.value)}
|
onChange={(e) => handleFormChange(field.id, e.target.value)}
|
||||||
placeholder={field.readOnly ? "" : field.name}
|
placeholder={field.readOnly ? "" : field.name}
|
||||||
@@ -88,7 +90,7 @@ export function DynamicForm({
|
|||||||
case "select":
|
case "select":
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={commonProps.value}
|
value={String(commonProps.value)}
|
||||||
onValueChange={(value) => handleFormChange(field.id, value)}
|
onValueChange={(value) => handleFormChange(field.id, value)}
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -375,8 +375,8 @@ export function DynamicTable<TData extends BaseData>({
|
|||||||
<div
|
<div
|
||||||
role="slider"
|
role="slider"
|
||||||
aria-label={`컬럼 너비 조절: ${header.id}`}
|
aria-label={`컬럼 너비 조절: ${header.id}`}
|
||||||
aria-valuemin={header.column.minSize}
|
aria-valuemin={header.column.columnDef.minSize}
|
||||||
aria-valuemax={header.column.maxSize}
|
aria-valuemax={header.column.columnDef.maxSize}
|
||||||
aria-valuenow={header.column.getSize()}
|
aria-valuenow={header.column.getSize()}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseDown={header.getResizeHandler()}
|
onMouseDown={header.getResizeHandler()}
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ export function Header() {
|
|||||||
return `/projects/${projectId}/channels/${channelId}${basePath}`;
|
return `/projects/${projectId}/channels/${channelId}${basePath}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const homePath = projectId ? `/projects/${projectId}` : "/";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b">
|
<header className="border-b">
|
||||||
<div className="container mx-auto flex h-16 items-center">
|
<div className="container mx-auto flex h-16 items-center">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
interface ThemeProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ThemeProvider({
|
export declare function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
}: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
}: ThemeProviderProps): React.ReactElement;
|
||||||
@@ -15,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider projectId={projectId}>
|
<AuthProvider projectId={projectId}>
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider>
|
||||||
<App />
|
<App />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export function FeedbackListPage() {
|
|||||||
|
|
||||||
const renderExpandedRow = (row: Row<Feedback>) => (
|
const renderExpandedRow = (row: Row<Feedback>) => (
|
||||||
<div className="p-4 bg-muted rounded-md">
|
<div className="p-4 bg-muted rounded-md">
|
||||||
<h4 className="font-bold text-lg mb-2">{row.original.title}</h4>
|
<h4 className="font-bold text-lg mb-2">{String(row.original.title ?? '')}</h4>
|
||||||
<p className="whitespace-pre-wrap">{row.original.contents}</p>
|
<p className="whitespace-pre-wrap">{String(row.original.contents ?? '')}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface ApiField {
|
|||||||
export interface Feedback {
|
export interface Feedback {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
updatedAt: string;
|
||||||
[key: string]: unknown; // 동적 필드를 위해 인덱스 시그니처 사용
|
[key: string]: unknown; // 동적 필드를 위해 인덱스 시그니처 사용
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user