diff --git a/src/App.jsx b/src/App.jsx
index a722a4b3..11431e97 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,14 +1,20 @@
-import { Routes, Route, Navigate } from 'react-router-dom';
+import { Routes, Route } from 'react-router-dom';
+import { lazy, Suspense } from 'react';
// 구현 영역 - 레이아웃
import MainLayout from './components/layout/MainLayout';
+import { ToastContainer } from './components/common/Toast';
-// 퍼블리시 영역
-import PublishLayout from './publish/layouts/PublishLayout';
-import PublishRoutes from './publish/PublishRoutes';
+// 퍼블리시 영역 (개발 환경에서만 동적 로드)
+// 프로덕션 빌드 시 tree-shaking으로 제외됨
+const PublishRouter = import.meta.env.DEV
+ ? lazy(() => import('./publish'))
+ : null;
export default function App() {
return (
+ <>
+
{/* =====================
구현 영역 (메인)
@@ -17,12 +23,21 @@ export default function App() {
} />
{/* =====================
- 퍼블리시 영역
+ 퍼블리시 영역 (개발 환경 전용)
/publish/* 로 접근하여 퍼블리시 결과물 미리보기
+ 프로덕션 빌드 시 이 라우트와 관련 모듈이 제외됨
===================== */}
- }>
- {PublishRoutes}
-
+ {import.meta.env.DEV && PublishRouter && (
+ Loading publish...}>
+
+
+ }
+ />
+ )}
+ >
);
}
diff --git a/src/components/common/Toast.jsx b/src/components/common/Toast.jsx
new file mode 100644
index 00000000..61bb66c4
--- /dev/null
+++ b/src/components/common/Toast.jsx
@@ -0,0 +1,91 @@
+/**
+ * 토스트 알림 컴포넌트
+ * 간단한 메시지를 일정 시간 후 자동으로 사라지게 표시
+ */
+import { useEffect, useState } from 'react';
+import { createPortal } from 'react-dom';
+import './Toast.scss';
+
+// 토스트 메시지 표시 함수 (외부에서 호출용)
+let showToastFn = null;
+
+export function showToast(message, duration = 3000) {
+ if (showToastFn) {
+ showToastFn(message, duration);
+ }
+}
+
+/**
+ * 토스트 컨테이너 컴포넌트
+ * App 최상위에 한 번만 마운트
+ */
+export function ToastContainer() {
+ const [toasts, setToasts] = useState([]);
+
+ useEffect(() => {
+ showToastFn = (message, duration) => {
+ const id = Date.now();
+ setToasts((prev) => [...prev, { id, message, duration }]);
+ };
+
+ return () => {
+ showToastFn = null;
+ };
+ }, []);
+
+ const removeToast = (id) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ };
+
+ return createPortal(
+
+ {toasts.map((toast) => (
+ removeToast(toast.id)}
+ />
+ ))}
+
,
+ document.body
+ );
+}
+
+/**
+ * 개별 토스트 아이템
+ */
+function ToastItem({ message, duration, onClose }) {
+ const [isVisible, setIsVisible] = useState(false);
+ const [isExiting, setIsExiting] = useState(false);
+
+ useEffect(() => {
+ // 마운트 후 바로 표시
+ requestAnimationFrame(() => {
+ setIsVisible(true);
+ });
+
+ // duration 후 사라지기 시작
+ const hideTimer = setTimeout(() => {
+ setIsExiting(true);
+ }, duration);
+
+ // 애니메이션 후 제거
+ const removeTimer = setTimeout(() => {
+ onClose();
+ }, duration + 300);
+
+ return () => {
+ clearTimeout(hideTimer);
+ clearTimeout(removeTimer);
+ };
+ }, [duration, onClose]);
+
+ return (
+
+ {message}
+
+ );
+}
+
+export default ToastContainer;
diff --git a/src/components/common/Toast.scss b/src/components/common/Toast.scss
new file mode 100644
index 00000000..ad6d2b50
--- /dev/null
+++ b/src/components/common/Toast.scss
@@ -0,0 +1,36 @@
+.toast-container {
+ position: fixed;
+ top: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 10000;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ pointer-events: none;
+}
+
+.toast-item {
+ background: rgba(0, 0, 0, 0.85);
+ color: #fff;
+ padding: 12px 24px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ transform: translateY(-20px);
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ pointer-events: auto;
+
+ &.visible {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ &.exiting {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+}
diff --git a/src/utils/assetPath.js b/src/utils/assetPath.js
new file mode 100644
index 00000000..7b286ff7
--- /dev/null
+++ b/src/utils/assetPath.js
@@ -0,0 +1,39 @@
+/**
+ * Public 폴더 에셋 경로 유틸리티
+ *
+ * Vite의 base 설정을 적용하여 public 폴더의 에셋 경로를 반환합니다.
+ * 서브 경로 배포 시 (예: /kcgv/) 자동으로 경로를 조정합니다.
+ *
+ * @example
+ * // 개발 환경 (base: '/')
+ * assetPath('/images/icon.svg') // → '/images/icon.svg'
+ *
+ * // 프로덕션 환경 (base: '/kcgv/')
+ * assetPath('/images/icon.svg') // → '/kcgv/images/icon.svg'
+ */
+
+/**
+ * public 폴더 에셋의 전체 경로를 반환
+ * @param {string} path - '/'로 시작하는 에셋 경로 (예: '/images/icon.svg')
+ * @returns {string} base URL이 적용된 전체 경로
+ */
+export function assetPath(path) {
+ // import.meta.env.BASE_URL은 항상 '/'로 끝남 (예: '/', '/kcgv/')
+ const base = import.meta.env.BASE_URL;
+
+ // path가 '/'로 시작하면 제거하여 중복 방지
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
+
+ return base + cleanPath;
+}
+
+/**
+ * 이미지 경로를 위한 단축 함수
+ * @param {string} filename - 이미지 파일명 (예: 'icon.svg', 'photo.png')
+ * @returns {string} base URL이 적용된 전체 이미지 경로
+ */
+export function imagePath(filename) {
+ return assetPath(`/images/${filename}`);
+}
+
+export default assetPath;