feat(frontend): Phase 1 - 리팩토링을 위한 frontend-temp 프로젝트 셋업

- frontend-temp/ 폴더 생성 (Strangler Fig Pattern)
- package.json: clsx 추가, 버전 2.0.0
- vite.config.js: @ path alias 추가
- 기본 폴더 구조 생성 (api, components, hooks, pages, stores, utils, constants)
- docker-compose.yml: fromis9-frontend-dev 서비스 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 17:04:18 +09:00
parent 9d365dcadb
commit 4ec368c936
22 changed files with 3461 additions and 0 deletions

View file

@ -12,6 +12,19 @@ services:
- app - app
restart: unless-stopped restart: unless-stopped
fromis9-frontend-dev:
build: ./frontend-temp
container_name: fromis9-frontend-dev
labels:
- "com.centurylinklabs.watchtower.enable=false"
volumes:
- ./frontend-temp:/app
depends_on:
- fromis9-backend
networks:
- app
restart: unless-stopped
fromis9-backend: fromis9-backend:
build: ./backend build: ./backend
container_name: fromis9-backend container_name: fromis9-backend

4
frontend-temp/Dockerfile Normal file
View file

@ -0,0 +1,4 @@
# 개발 모드
FROM node:20-alpine
WORKDIR /app
CMD ["sh", "-c", "npm install --include=dev && npm run dev -- --host 0.0.0.0"]

22
frontend-temp/index.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>fromis_9 - 프로미스나인</title>
<link
rel="stylesheet"
as="style"
crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3175
frontend-temp/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
{
"name": "fromis9-frontend",
"private": true,
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@babel/runtime": "^7.28.6",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-virtual": "^3.13.18",
"canvas-confetti": "^1.9.4",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"framer-motion": "^11.0.8",
"lucide-react": "^0.344.0",
"react": "^18.2.0",
"react-calendar": "^6.0.0",
"react-colorful": "^5.6.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-infinite-scroll-component": "^6.1.1",
"react-intersection-observer": "^10.0.0",
"react-ios-time-picker": "^0.2.2",
"react-linkify": "^1.0.0-alpha",
"react-photo-album": "^3.4.0",
"react-router-dom": "^6.22.3",
"react-window": "^2.2.3",
"swiper": "^12.0.3",
"zustand": "^5.0.9"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"vite": "^5.4.1"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

41
frontend-temp/src/App.jsx Normal file
View file

@ -0,0 +1,41 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { isMobile } from "react-device-detect";
/**
* 프로미스나인 팬사이트 메인
*
* Phase 1: 프로젝트 셋업 완료
* - 기본 구조 설정 파일 생성
* - React Query, React Router 설정
* - Tailwind CSS 설정
*
* 다음 단계에서 유틸리티, 스토어, API, 컴포넌트들이 추가될 예정
*/
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-2xl font-bold text-primary mb-2">
fromis_9 Frontend Refactoring
</h1>
<p className="text-gray-600">
Phase 1 완료 - 프로젝트 셋업
</p>
<p className="text-sm text-gray-500 mt-2">
디바이스: {isMobile ? "모바일" : "PC"}
</p>
</div>
</div>
}
/>
</Routes>
</BrowserRouter>
);
}
export default App;

View file

View file

View file

View file

@ -0,0 +1,85 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 기본 스타일 */
body {
background-color: #fafafa;
color: #1a1a1a;
}
/* 스크롤바 스타일 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #548360;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #456e50;
}
/* View Transitions API - 앨범 커버 이미지 부드러운 전환 */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}
/* 앨범 커버 트랜지션 */
::view-transition-group(*) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(*) {
animation: fade-out 0.3s ease-out both;
}
::view-transition-new(*) {
animation: fade-in 0.3s ease-in both;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 라이트박스 스크롤바 숨기기 */
.lightbox-no-scrollbar::-webkit-scrollbar {
display: none;
}
/* 스크롤바 숨기기 유틸리티 */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Swiper autoHeight 지원 */
.swiper-slide {
height: auto !important;
}

View file

@ -0,0 +1,23 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./index.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5
retry: 1,
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);

View file

View file

View file

View file

View file

@ -0,0 +1,21 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
primary: {
DEFAULT: "#548360",
dark: "#456E50",
light: "#6A9A75",
},
secondary: "#F5F5F5",
accent: "#FFD700",
},
fontFamily: {
sans: ["Pretendard", "Inter", "sans-serif"],
},
},
},
plugins: [],
};

View file

@ -0,0 +1,27 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
host: true,
port: 80,
allowedHosts: true,
proxy: {
"/api": {
target: "http://fromis9-backend:80",
changeOrigin: true,
},
"/docs": {
target: "http://fromis9-backend:80",
changeOrigin: true,
},
},
},
});