diff --git a/frontend/README.md b/frontend/README.md index 3c4fd2f..ec6de00 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,54 +1,10 @@ # React + TypeScript + Vite -```js -npm run dev -``` - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +### 설치 및 실행 +```bash +# 의존성 설치 +npm install -- Configure the top-level `parserOptions` property like this: - -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` - -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: { react: { version: '18.3' } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) -``` +# 개발 서버 실행 +npm run dev +``` \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5de2b5e..7b6d507 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -16,7 +16,7 @@ export const Navbar = ({ style, link = "/" }: NavbarProps) => { style={style} > {/* 내부 컨텐츠 컨테이너 */} -
+
{/* 로고 컨테이너 - 호버/클릭 애니메이션 적용 */} (null); + // Blob URL 메모리 정리 (cleanup) + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + // type 유효성 검사 const validateType = (param: string | undefined): DrawingType => { if (!param || !['house', 'tree', 'person'].includes(param)) { @@ -99,6 +108,8 @@ export default function Drawing() { // 파일 처리 함수 const handleFileChange = (file: File) => { setUploadedFile(file); + const previewURL = URL.createObjectURL(file); // Blob URL 생성 (미리보기) + setPreviewUrl(previewURL); }; const handleDragOver = (e: React.DragEvent) => { @@ -230,9 +241,9 @@ export default function Drawing() { {/* 그리기/업로드 영역 */} -
+
{mode === 'draw' ? ( -
+
{/* ToolBar 컴포넌트 */} +
❌ diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 71964c4..4a77cd5 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,73 +1,155 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { motion, useMotionValue, useSpring } from 'framer-motion'; import { Navbar } from '../components/Navbar'; import noiseImage from '../assets/images/noise.png'; +// 자석 효과 설정 +const SPRING_CONFIG = { damping: 100, stiffness: 400 }; +const MAX_DISTANCE = 0.5; -const Home: React.FC = () => { - const circleRef = useRef(null); - const innerCircleRef = useRef(null); - const textRef = useRef(null); - const buttonRef = useRef(null); +// 자석 효과 버튼 컴포넌트 +const MagneticButton: React.FC<{ onClick: () => void }> = ({ onClick }) => { + const [isHovered, setIsHovered] = useState(false); + const x = useMotionValue(0); + const y = useMotionValue(0); + const buttonRef = useRef(null); + const springX = useSpring(x, SPRING_CONFIG); + const springY = useSpring(y, SPRING_CONFIG); - // 'layer in view' 트리거 효과 (IntersectionObserver 사용) useEffect(() => { - const textElement = textRef.current; - const buttonElement = buttonRef.current; + const calculateDistance = (e: MouseEvent) => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const distanceX = e.clientX - centerX; + const distanceY = e.clientY - centerY; - if (textElement && buttonElement) { - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - textElement.classList.add('in-view'); // 애니메이션 트리거 - buttonElement.classList.add('in-view'); // 버튼 애니메이션 트리거 - } - }); - }, { threshold: 0.5 }); // 50% 보였을 때 트리거 + if (isHovered) { + x.set(distanceX * MAX_DISTANCE); + y.set(distanceY * MAX_DISTANCE); + } else { + x.set(0); + y.set(0); + } + } + }; - observer.observe(textElement); + document.addEventListener("mousemove", calculateDistance); + return () => document.removeEventListener("mousemove", calculateDistance); + }, [isHovered, x, y]); - return () => { - if (textElement) { - observer.unobserve(textElement); - } - }; - } - }, []); + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ + x: springX, + y: springY, + }} + > + + + ); +}; + +const Home: React.FC = () => { + const circleRef = useRef(null); + const innerCircleRef = useRef(null); return (
{/* 원형 배경 */} -
- {/* 배경 이미지 추가 */} -
-
+ {/* 전체 화면 밝기 조정 (배경 밝게 설정) */}
{ opacity: 1, // opacity 1로 설정 overflow: 'visible', // overflow visible 설정 }} - >
+ /> - {/* 새로운 원형 배경 */} -
+ {/* 상단 네비게이션 바 */} -
- -
+
+ +
- {/* Heading Text - 'HTP Test' */} -
HTP Test -
+ - {/* 추가 텍스트 - "집-나무-사람 그림으로 심리를 분석해보세요!" */} -
{ fontFamily: 'Satashi, sans-serif', // 폰트 설정 color: '#3B3B3B', fontSize: '24px', // 텍스트 크기 설정 - opacity: 1, // 시작 시 opacity 0으로 설정 + zIndex: 2, }} > 집-나무-사람 그림으로 심리를 분석해보세요! -
+ {/* 버튼 */} - + window.location.href = '/select'} /> +
); }; diff --git a/frontend/src/styles/pages/drawing.css b/frontend/src/styles/pages/drawing.css index 1d8c327..77d4702 100644 --- a/frontend/src/styles/pages/drawing.css +++ b/frontend/src/styles/pages/drawing.css @@ -1,279 +1,281 @@ .drawing-page-container { - width: 100%; - max-width: 1100px; - padding: 2.5rem 4rem 4rem 4rem; - margin-top: -1rem; - background: linear-gradient(180deg, #DE523A 0%, #FA8E41 100%); - border-radius: 32px; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - position: relative; - overflow: hidden; + width: 100%; + max-width: 1100px; + padding: 2.5rem 4rem 4rem 4rem; + margin: -1rem auto 0; + background: linear-gradient(180deg, #DE523A 0%, #FA8E41 100%); + border-radius: 32px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; } - - /* Noise texture overlay */ - .drawing-page-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: url('../../assets/images/noise.png'); - background-repeat: repeat; - opacity: 0.1; - mix-blend-mode: overlay; - pointer-events: none; - z-index: 3; - } - - /* Conic Animation 1 */ - .conic-animation-1 { - position: absolute; - top: -469px; - bottom: 27px; - left: 187px; - right: 187px; - width: 823px; - height: 823px; - border-radius: 50%; - background: conic-gradient(from 0deg, #FFFFFF, #FFF3D4, #FFD54F); - opacity: 1; - mix-blend-mode: overlay; - filter: blur(100px); - transform-style: preserve-3d; - perspective: 1200px; - z-index: 1; - } - - .conic-animation-1::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: inherit; - border-radius: inherit; - transform: scale(0.8); - opacity: 1; - animation: mirrorEffect1 3s linear infinite; - } - - /* Conic Animation 2 */ - .conic-animation-2 { - position: absolute; - top: -469px; - bottom: 27px; - left: 187px; - right: 187px; - width: 823px; - height: 823px; - border-radius: 50%; - background: conic-gradient(from 0deg, #FFE5B4, #FFA640, #E65C12); - opacity: 1; - filter: blur(100px); - transform-style: preserve-3d; - perspective: 1200px; - z-index: 2; + +/* Noise texture overlay */ +.drawing-page-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url('../../assets/images/noise.png'); + background-repeat: repeat; + opacity: 0.1; + mix-blend-mode: overlay; + pointer-events: none; + z-index: 3; +} + +/* Conic Animation 1 */ +.conic-animation-1 { + position: absolute; + top: -469px; + bottom: 27px; + left: 187px; + right: 187px; + width: 823px; + height: 823px; + border-radius: 50%; + background: conic-gradient(from 0deg, #FFFFFF, #FFF3D4, #FFD54F); + opacity: 1; + mix-blend-mode: overlay; + filter: blur(100px); + transform-style: preserve-3d; + perspective: 1200px; + z-index: 1; +} + +.conic-animation-1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: inherit; + border-radius: inherit; + transform: scale(0.8); + opacity: 1; + animation: mirrorEffect1 3s linear infinite; +} + +/* Conic Animation 2 */ +.conic-animation-2 { + position: absolute; + top: -469px; + bottom: 27px; + left: 187px; + right: 187px; + width: 823px; + height: 823px; + border-radius: 50%; + background: conic-gradient(from 0deg, #FFE5B4, #FFA640, #E65C12); + opacity: 1; + filter: blur(100px); + transform-style: preserve-3d; + perspective: 1200px; + z-index: 2; +} + +.conic-animation-2::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: inherit; + border-radius: inherit; + transform: scale(0.8); + opacity: 1; + animation: mirrorEffect2 3s linear infinite; +} + +@keyframes mirrorEffect1 { + 0% { + transform: scale(0.8) rotate(0deg); } - - .conic-animation-2::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: inherit; - border-radius: inherit; - transform: scale(0.8); - opacity: 1; - animation: mirrorEffect2 3s linear infinite; + 100% { + transform: scale(0.8) rotate(360deg); } - - @keyframes mirrorEffect1 { - 0% { - transform: scale(0.8) rotate(0deg); - } - 100% { - transform: scale(0.8) rotate(360deg); - } +} + +@keyframes mirrorEffect2 { + 0% { + transform: scale(0.8) rotate(0deg); } - - @keyframes mirrorEffect2 { - 0% { - transform: scale(0.8) rotate(0deg); - } - 100% { - transform: scale(0.8) rotate(360deg); - } + 100% { + transform: scale(0.8) rotate(360deg); } - - /* Content z-index hierarchy */ - .drawing-page-container h1, - .drawing-page-container .flex.justify-center.gap-4 { - position: relative; - z-index: 10; +} + +/* Content z-index hierarchy */ +.drawing-page-container h1, +.drawing-page-container .flex.justify-center.gap-4 { + position: relative; + z-index: 10; +} + +.drawing-container, +.upload-area { + position: relative; + width: 100%; + max-width: 900px; + height: 500px; + margin: 0 auto; + background: white; + border-radius: 30px; + z-index: 5; +} + +.drawing-container { + overflow: visible; +} + +.drawing-area { + width: 100%; + height: 100%; + border-radius: 30px; + overflow: hidden; +} + +.pen-cursor { + cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'%3E%3C/path%3E%3C/svg%3E") 0 24, auto !important; +} + +.drawing-area canvas[data-tool="eraser"] { + cursor: default !important; +} + +.tools-container { + position: absolute; + right: 16px; + top: -50px; + display: flex; + flex-direction: row; + gap: 8px; + z-index: 6; +} + +.tool-button { + width: 36px; + height: 36px; + background: white; + border-radius: 8px; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.tool-button.active { + background-color: white; + color: #DE523A; + transform: scale(1.1); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.tool-button:not(.active) { + background-color: rgba(255, 255, 255, 0.9); + color: #666; +} + +.tool-button:not(.active):hover { + background-color: white; + transform: scale(1.05); +} + +.drawing-container { + overflow: visible; +} + +.drawing-area { + width: 100%; + height: 100%; + border-radius: 30px; + overflow: hidden; +} + +.upload-area { + position: relative; + width: 900px; + height: 500px; + margin: 0 auto; + background: white; + border-radius: 30px; + border: 2px dashed #e5e7eb; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + cursor: pointer; + z-index: 5; +} + +.upload-preview-container { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.upload-area.dragging { + border-color: #DE523A; + background-color: rgba(255, 255, 255, 0.98); +} + +.upload-area img, +.upload-area label, +.upload-area button { + z-index: 5; +} + +.submit-button { + width: 180px; + height: 40px; + background-color: #ED7926; + color: #FFFFFF; + border-radius: 8px; + font-weight: 500; + transition: all 0.2s; + position: relative; + z-index: 10; + box-shadow: 0px 10px 10px -10px rgba(0, 0, 0, 0.25); + filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5)); +} + +.submit-button:hover { + transform: translateY(-2px); + box-shadow: 0px 12px 10px -10px rgba(0, 0, 0, 0.25); + filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5)); +} + +.submit-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +@media (max-width: 1024px) { + .drawing-page-container { + margin: 0 1rem; + padding: 2rem; } - + .drawing-container, .upload-area { - position: relative; - width: 900px; - height: 500px; - margin: 0 auto; - background: white; - border-radius: 30px; - z-index: 5; - } - - .drawing-container { - overflow: visible; - } - - .drawing-area { width: 100%; - height: 100%; - border-radius: 30px; - overflow: hidden; - } - - .pen-cursor { - cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'%3E%3C/path%3E%3C/svg%3E") 0 24, auto !important; - } - - .drawing-area canvas[data-tool="eraser"] { - cursor: default !important; + max-width: 1000px; } +} - .tools-container { - position: absolute; - right: 16px; - top: -50px; - display: flex; - flex-direction: row; - gap: 8px; - z-index: 6; - } - - .tool-button { - width: 36px; - height: 36px; - background: white; - border-radius: 8px; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - } - - .tool-button.active { - background-color: white; - color: #DE523A; - transform: scale(1.1); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - } - - .tool-button:not(.active) { - background-color: rgba(255, 255, 255, 0.9); - color: #666; - } - - .tool-button:not(.active):hover { - background-color: white; - transform: scale(1.05); - } - - .drawing-container { - overflow: visible; - } - - .drawing-area { - width: 100%; - height: 100%; - border-radius: 30px; - overflow: hidden; - } - +@media (max-width: 768px) { + .drawing-container, .upload-area { - position: relative; - width: 900px; - height: 500px; - margin: 0 auto; - background: white; - border-radius: 30px; - border: 2px dashed #e5e7eb; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s; - cursor: pointer; - z-index: 5; - } - - .upload-preview-container { - position: relative; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - } - - .upload-area.dragging { - border-color: #DE523A; - background-color: rgba(255, 255, 255, 0.98); - } - - .upload-area img, - .upload-area label, - .upload-area button { - z-index: 5; - } - - .submit-button { - width: 180px; - height: 40px; - background-color: #ED7926; - color: #FFFFFF; - border-radius: 8px; - font-weight: 500; - transition: all 0.2s; - position: relative; - z-index: 10; - box-shadow: 0px 10px 10px -10px rgba(0, 0, 0, 0.25); - filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5)); - } - - .submit-button:hover { - transform: translateY(-2px); - box-shadow: 0px 12px 10px -10px rgba(0, 0, 0, 0.25); - filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5)); - } - - .submit-button:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - } - -@media (max-width: 1024px) { - .drawing-page-container { - margin: 0 1rem; - } - - .drawing-container, - .upload-area { - width: 100%; - max-width: 1000px; - } + height: 450px; } - - @media (max-width: 768px) { - .drawing-container, - .upload-area { - height: 450px; - } - } \ No newline at end of file +} \ No newline at end of file