diff --git a/front/public/models/bcduck.glb b/front/public/models/bcduck.glb new file mode 100644 index 0000000..fbe8787 Binary files /dev/null and b/front/public/models/bcduck.glb differ diff --git a/front/public/models/bcduck_glasses.glb b/front/public/models/bcduck_glasses.glb new file mode 100644 index 0000000..d90a1cb Binary files /dev/null and b/front/public/models/bcduck_glasses.glb differ diff --git a/front/public/models/ground1.glb b/front/public/models/ground1.glb new file mode 100644 index 0000000..077e905 Binary files /dev/null and b/front/public/models/ground1.glb differ diff --git a/front/public/models/new.glb b/front/public/models/new.glb new file mode 100644 index 0000000..761dfb9 Binary files /dev/null and b/front/public/models/new.glb differ diff --git a/front/public/models/santa.glb b/front/public/models/santa.glb new file mode 100644 index 0000000..f3d9786 Binary files /dev/null and b/front/public/models/santa.glb differ diff --git a/front/public/models/tux.glb b/front/public/models/tux.glb index ec93126..d2c970f 100644 Binary files a/front/public/models/tux.glb and b/front/public/models/tux.glb differ diff --git a/front/src/App.tsx b/front/src/App.tsx index 44cadae..b29f32f 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -3,29 +3,18 @@ import styled, { ThemeProvider } from 'styled-components'; import GlobalStyles from './GlobalStyles'; import { theme } from './utils'; import { IsLogin, IsSnowballData } from './router'; -import { - Intro, - Nickname, - Snowball, - Main, - Visit, - Deco, - Wrong, - Boostcamp -} from './pages'; +import * as Pages from './pages'; import { Song } from './components'; +import { SnowBallProvider } from './pages/Visit/SnowBallProvider'; +import { MessageProvider } from './pages/Visit/MessageProvider'; +import { DecoProvider } from './pages/Visit/Deco/DecoProvider'; const Outer = styled.div` position: relative; - left: 50%; transform: translateX(-50%); - width: 100%; height: 100%; - /* ::-webkit-scrollbar { - display: none; - } */ @media (min-width: ${theme.size['--desktop-min-width']}) { width: ${theme.size['--desktop-width']}; @@ -41,11 +30,22 @@ const App = () => { - } /> + } /> - }> - } /> - } /> + + + + + + + + } + > + } /> + } /> { } > - } /> - } /> + } /> + } /> -
+ } /> - } /> - } /> + } /> + } /> diff --git a/front/src/components/Msg/Msg.tsx b/front/src/components/Msg/Msg.tsx index 5f18c8e..25ccad2 100644 --- a/front/src/components/Msg/Msg.tsx +++ b/front/src/components/Msg/Msg.tsx @@ -25,7 +25,8 @@ const StyledLetterBox = styled.div` padding: 1.5rem; gap: 1rem; background-color: ${props => props.color + '80'}; - margin: 1rem; + margin: 1rem auto; + pointer-events: all; `; const StyledLetterPerson = styled.div` @@ -107,7 +108,7 @@ const StyledFromInput = styled.input` const Msg = (props: MsgProps): JSX.Element => { const [wordCount, setWordCount] = useState(0); - const { setContent, setSender } = useContext(DecoContext); + const { content, sender, setContent, setSender } = useContext(DecoContext); const maxWordCount = 500; const wordLength = (e: React.ChangeEvent) => { @@ -131,6 +132,7 @@ const Msg = (props: MsgProps): JSX.Element => { @@ -144,6 +146,7 @@ const Msg = (props: MsgProps): JSX.Element => { From. {props.isInput ? ( { e.target.value = ''; diff --git a/front/src/components/SnowGlobeCanvas/SnowGlobeCanvas.tsx b/front/src/components/SnowGlobeCanvas/SnowGlobeCanvas.tsx index 07904ca..f52b318 100644 --- a/front/src/components/SnowGlobeCanvas/SnowGlobeCanvas.tsx +++ b/front/src/components/SnowGlobeCanvas/SnowGlobeCanvas.tsx @@ -19,7 +19,7 @@ const SnowGlobeCanvas = () => { key={i} centerPosition={glassPosition} rangeRadius={glassRadius} - radius={0.3 + Math.random() * 0.7} + radius={0.2 + Math.random() * 0.5} model={Math.floor(Math.random() * 3)} /> )); @@ -35,6 +35,7 @@ const SnowGlobeCanvas = () => { color={message.decoration_color} sender={message.sender ?? '비공개'} letterID={message.letter_id ?? 0} + isOpened={message.opened !== null || message.opened !== 'null'} /> ); }); @@ -75,13 +76,13 @@ const SnowGlobeCanvas = () => { position={new THREE.Vector3(0, glassRadius / 2, 0)} color={new THREE.Color('white')} radius={glassRadius} - opacity={0.08} + opacity={0.1} /> { const deco = useGLTF(DECO[id].fileName).scene.clone(); const target = { x: 8, z: 0 }; @@ -29,6 +31,14 @@ const Deco = ({ deco.name = DECO[id].name; deco.scale.set(scale, scale, scale); deco.position.set(position.x, position.y, position.z); + + if (isOpened) { + const newModel = useGLTF('/models/new.glb').scene.clone().children[0]; + newModel.position.set(0, 1.2, 0); + newModel.scale.set(0.1, 0.1, 0.1); + deco.add(newModel); + } + deco.children.forEach(child => { if (child instanceof THREE.Mesh) { child.userData.message = message; @@ -42,6 +52,7 @@ const Deco = ({ } }); deco.rotateY(Math.PI - test); + return ; }; diff --git a/front/src/components/SnowGlobeCanvas/models/Glass.tsx b/front/src/components/SnowGlobeCanvas/models/Glass.tsx index 40461d1..baf04eb 100644 --- a/front/src/components/SnowGlobeCanvas/models/Glass.tsx +++ b/front/src/components/SnowGlobeCanvas/models/Glass.tsx @@ -10,14 +10,14 @@ interface GlassProps { const Glass: React.FC = ({ position, radius, color, opacity }) => { return ( - + ); diff --git a/front/src/components/SnowGlobeCanvas/models/Ground.tsx b/front/src/components/SnowGlobeCanvas/models/Ground.tsx index 809c4a6..0746457 100644 --- a/front/src/components/SnowGlobeCanvas/models/Ground.tsx +++ b/front/src/components/SnowGlobeCanvas/models/Ground.tsx @@ -8,7 +8,7 @@ interface GroundProps { } const Ground: React.FC = ({ scale, position }) => { - const ground = useGLTF('/models/ground.glb').scene.clone(); + const ground = useGLTF('/models/ground1.glb').scene.clone(); ground.scale.set(scale, scale, scale); ground.position.set(position.x, position.y, position.z); diff --git a/front/src/components/SnowGlobeCanvas/models/MainDeco.tsx b/front/src/components/SnowGlobeCanvas/models/MainDeco.tsx index 1f118df..eb3b3f5 100644 --- a/front/src/components/SnowGlobeCanvas/models/MainDeco.tsx +++ b/front/src/components/SnowGlobeCanvas/models/MainDeco.tsx @@ -3,12 +3,13 @@ import { useFrame } from '@react-three/fiber'; import { useGLTF } from '@react-three/drei'; import * as THREE from 'three'; import { MAIN } from '../../../constants/deco'; +import { makeColorChangedMaterial } from '../../../utils/meshUtils'; interface MyModelProps { id: number; scale: number; position: THREE.Vector3; - color: THREE.Color; + color: string; } const fallingModel = ( @@ -37,12 +38,12 @@ const MainDeco = ({ id, scale, position, color }: MyModelProps) => { const deco = useGLTF(MAIN[id].fileName).scene.clone(); const speedRef = useRef(new THREE.Vector3(0, -0.01, 0)); - //run build error 해결용 console model업데이트 후 색상 적용해야됨 - console.log(color); deco.name = MAIN[id].name; deco.scale.set(scale, scale, scale); deco.position.set(position.x, position.y, position.z); - deco.children.forEach(e => (e.castShadow = true)); + const colorPart = deco.getObjectByName('colorPart') as THREE.Mesh; + colorPart.material = makeColorChangedMaterial(colorPart, color); + deco.children.forEach(mesh => (mesh.castShadow = true)); useFrame(() => { fallingModel(deco, speedRef); }); diff --git a/front/src/components/SnowGlobeCanvas/models/Raycaster.tsx b/front/src/components/SnowGlobeCanvas/models/Raycaster.tsx index d6ec484..bf693ba 100644 --- a/front/src/components/SnowGlobeCanvas/models/Raycaster.tsx +++ b/front/src/components/SnowGlobeCanvas/models/Raycaster.tsx @@ -74,9 +74,7 @@ const Raycaster: React.FC = ({ isClickedRef }) => { intersect => intersect.object.userData.message ); if (selectedDeco) { - const { message, color, sender, letterColor } = - selectedDeco.object.userData; - console.log(color); + const { message, sender, letterColor } = selectedDeco.object.userData; setMessage(message); setSender(sender); setColor(letterColor); diff --git a/front/src/components/SnowGlobeCanvas/models/Snow.tsx b/front/src/components/SnowGlobeCanvas/models/Snow.tsx index d93d52d..603f147 100644 --- a/front/src/components/SnowGlobeCanvas/models/Snow.tsx +++ b/front/src/components/SnowGlobeCanvas/models/Snow.tsx @@ -3,6 +3,7 @@ import { useFrame } from '@react-three/fiber'; import { useGLTF } from '@react-three/drei'; import * as THREE from 'three'; +import { makeColorChangedMaterial } from '../../../utils/meshUtils'; interface SnowProps { radius: number; @@ -11,6 +12,48 @@ interface SnowProps { model: number; } +const randomizePosition = ( + target: THREE.Mesh, + centerPosition: THREE.Vector3, + radius: number +) => { + target.position.set( + centerPosition.x - radius + Math.random() * radius * 2, + centerPosition.y + radius + Math.random() * radius * 2, + centerPosition.z - radius + Math.random() * radius * 2 + ); +}; + +const fallingAnimate = ( + target: THREE.Mesh, + speed: number, + centerPosition: THREE.Vector3, + radius: number +) => { + if (target.position.y <= -1) { + randomizePosition(target, centerPosition, radius); + const newScale = 0.2 + Math.random() * 0.5; + target.scale.set(newScale, newScale, newScale); + } + target.position.y -= speed; +}; +const rotateAnimate = (target: THREE.Mesh, speed: number) => { + target.rotation.y += speed; +}; +const visibleInRange = ( + target: THREE.Mesh, + centerPosition: THREE.Vector3, + radius: number +) => { + if (target.position.distanceTo(centerPosition) > radius) { + target.visible = false; + } else { + target.visible = true; + } +}; + +const snowcolor = ['#99c9fd', '#a5bbd3', '#f1faff']; + const Snow: React.FC = ({ radius, centerPosition, @@ -20,7 +63,7 @@ const Snow: React.FC = ({ const snowRef = useRef(null); const position = new THREE.Vector3( centerPosition.x - rangeRadius + Math.random() * rangeRadius * 2, - centerPosition.y + rangeRadius + Math.random() * 2 * rangeRadius, + centerPosition.y + rangeRadius + Math.random() * rangeRadius * 2, centerPosition.z - rangeRadius + Math.random() * rangeRadius * 2 ); @@ -29,22 +72,21 @@ const Snow: React.FC = ({ snow.position.set(position.x, position.y, position.z); snow.scale.set(radius, radius, radius); snow.rotation.y = Math.random(); + snow.traverse(obj => { + if (obj instanceof THREE.Mesh) { + obj.material = makeColorChangedMaterial( + obj, + snowcolor[Math.floor(Math.random() * 3)] + ); + } + }); useFrame((_, delta) => { const snow = snowRef.current; const speed = 1 * delta; if (snow) { - if (snow.position.y <= 0) { - snow.position.y = - centerPosition.y + rangeRadius + Math.random() * rangeRadius * 2; - } - snow.position.y -= speed; - snow.rotation.y += speed; - - if (snow.position.distanceTo(centerPosition) > rangeRadius - 0.5) { - snow.visible = false; - } else { - snow.visible = true; - } + fallingAnimate(snow, speed, centerPosition, rangeRadius); + rotateAnimate(snow, speed); + visibleInRange(snow, centerPosition, rangeRadius - 1); } }); diff --git a/front/src/constants/deco.tsx b/front/src/constants/deco.tsx index b28038c..ec22d90 100644 --- a/front/src/constants/deco.tsx +++ b/front/src/constants/deco.tsx @@ -25,8 +25,18 @@ export const MAIN = [ img: '/models/img/Main/tux.gif' }, { - name: '트리', - fileName: '/models/tree.glb', + name: '산타', + fileName: '/models/santa.glb', + img: '/models/img/Main/tree.png' + }, + { + name: '부캠덕', + fileName: '/models/bcduck.glb', + img: '/models/img/Main/tree.png' + }, + { + name: '안경부캠덕', + fileName: '/models/bcduck_glasses.glb', img: '/models/img/Main/tree.png' } ]; diff --git a/front/src/mockdata.json b/front/src/mockdata.json index 352f748..2abfcf0 100644 --- a/front/src/mockdata.json +++ b/front/src/mockdata.json @@ -2,7 +2,7 @@ "snowball_data": { "id": 1, "title": "스노우볼 속 내마음❤️", - "main_decoration_id": 1, + "main_decoration_id": 2, "main_decoration_color": "#ff0000", "bottom_decoration_id": 2, "bottom_decoration_color": "#ff0000", diff --git a/front/src/pages/Make/Snowball/MainDeco/MakeSnowballCanvas.tsx b/front/src/pages/Make/Snowball/MainDeco/MakeSnowballCanvas.tsx index 6eeceab..6c7cebf 100644 --- a/front/src/pages/Make/Snowball/MainDeco/MakeSnowballCanvas.tsx +++ b/front/src/pages/Make/Snowball/MainDeco/MakeSnowballCanvas.tsx @@ -32,7 +32,7 @@ const MainSnowballCavnas = () => { id={mainDecoID} scale={1} position={new THREE.Vector3(0, 0, 0)} - color={new THREE.Color(mainColor)} + color={mainColor} /> { return ( <> - - - - - - - - - - - + + + + + ); }; diff --git a/front/src/pages/Visit/Deco/MsgBox.tsx b/front/src/pages/Visit/Deco/MsgBox.tsx index 0bb177f..354b37b 100644 --- a/front/src/pages/Visit/Deco/MsgBox.tsx +++ b/front/src/pages/Visit/Deco/MsgBox.tsx @@ -13,7 +13,6 @@ const MsgContainer = styled.div` display: flex; flex-direction: column-reverse; width: 100%; - height: 100%; pointer-events: all; overflow: scroll; `; diff --git a/front/src/pages/Visit/Deco/PostButton.tsx b/front/src/pages/Visit/Deco/PostButton.tsx index bdaba3e..42ecb1c 100644 --- a/front/src/pages/Visit/Deco/PostButton.tsx +++ b/front/src/pages/Visit/Deco/PostButton.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { LongButton } from '../../../utils'; import { DecoContext } from './DecoProvider'; import { SnowBallContext } from '../SnowBallProvider'; +import { useNavigate } from 'react-router-dom'; interface ButtonProps { text: string; @@ -30,14 +31,15 @@ const StyledAlert = styled.div` const PostButton = (props: ButtonProps) => { const { color, decoID, letterID, content, sender } = useContext(DecoContext); - const { userData, snowBallData } = useContext(SnowBallContext); - - const [alert, setAlert] = useState(false); + const { userData, snowBallData, setSnowBallData } = + useContext(SnowBallContext); + const navigate = useNavigate(); + const [alerts, setAlerts] = useState(false); const ClickedPost = () => { //여기서 axios요청 if (content === '' || sender === '') { - setAlert(true); + setAlerts(true); return; } const a = { @@ -49,19 +51,29 @@ const PostButton = (props: ButtonProps) => { }; axios .post(`/api/message/${userData.id}/${snowBallData.id}`, a) - .then(res => { - console.log(res, 'post DONE!!!'); - }) - .catch(e => console.error(e)); + .then(() => { + axios.get(`/api/snowball/${snowBallData.id}`).then(res => { + setSnowBallData(res.data); - props.view[1](!props.view[0]); - props.visible[1](-1); + props.view[1](!props.view[0]); + props.visible[1](-1); + }); + }) + .catch(e => { + console.error(e); + alert( + '메시지가 꽉찼어요\n다른 스노우볼을 선택해주세요!\n작성중인 메시지는 유지됩니다!' + ); + navigate('../'); + }); }; return ( <> - {alert ? 내용과 이름을 입력해주세요 ! : null} + {alerts ? ( + 내용과 이름을 입력해주세요 ! + ) : null} {props.text} diff --git a/front/src/pages/Visit/Deco/Steps.tsx b/front/src/pages/Visit/Deco/Steps.tsx index 17c6bdd..6cb61d0 100644 --- a/front/src/pages/Visit/Deco/Steps.tsx +++ b/front/src/pages/Visit/Deco/Steps.tsx @@ -24,9 +24,8 @@ const StateBox = styled.div` `; const StyledBody = styled.div` - flex: 1 1 auto; - display: flex; - overflow-y: hidden; + max-height: fit-content; + overflow-y: scroll; pointer-events: none; * { pointer-events: all; @@ -34,6 +33,7 @@ const StyledBody = styled.div` `; const StyledBottomWrap = styled.div` + min-height: 15rem; display: flex; flex-direction: column; `; diff --git a/front/src/pages/Visit/SnowBallProvider.tsx b/front/src/pages/Visit/SnowBallProvider.tsx index 135bd0e..8d5cb8f 100644 --- a/front/src/pages/Visit/SnowBallProvider.tsx +++ b/front/src/pages/Visit/SnowBallProvider.tsx @@ -66,7 +66,6 @@ const SnowBallProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { axios(`/api/user/${user}`) .then(res => { - console.log('test', res); setSnowBallData(res.data.main_snowball as SnowBallData); setUserData(res.data.user as UserData); }) diff --git a/front/src/pages/Visit/Visit.tsx b/front/src/pages/Visit/Visit.tsx index 6b897f4..6576afe 100644 --- a/front/src/pages/Visit/Visit.tsx +++ b/front/src/pages/Visit/Visit.tsx @@ -2,21 +2,17 @@ import { SnowGlobeCanvas, UIContainer } from '../../components'; import VisitBottom from './VisitBottom'; import VisitHeader from './VisitHeader'; import VisitBody from './VisitBody'; -import { MessageProvider } from './MessageProvider'; -import { SnowBallProvider } from './SnowBallProvider'; const Visit = () => { return ( - - - - - - - - - - + <> + + + + + + + ); }; diff --git a/front/src/pages/Visit/VisitBody.tsx b/front/src/pages/Visit/VisitBody.tsx index 55119d2..2dc9c57 100644 --- a/front/src/pages/Visit/VisitBody.tsx +++ b/front/src/pages/Visit/VisitBody.tsx @@ -1,13 +1,32 @@ import { useContext } from 'react'; import { Msg } from '../../components'; import { MessageContext } from './MessageProvider'; -import { SnowBallContext } from './SnowBallProvider'; +import { SnowBallContext, SnowBallData } from './SnowBallProvider'; +import styled from 'styled-components'; +import axios from 'axios'; + +const MsgContainer = styled.div` + max-height: fit-content; + overflow: scroll; +`; + +const LeftBtn = styled.button` + color: white; + position: fixed; + left: 0; +`; +const RightBtn = styled.button` + color: white; + position: fixed; + right: 0; +`; const VisitBody = () => { const { message, sender, color } = useContext(MessageContext); // message가 '' 비어있지 않을때 - const { userData } = useContext(SnowBallContext); + const { userData, snowBallData, setSnowBallData } = + useContext(SnowBallContext); return ( - <> + {message !== '' ? ( { sender={sender} to={userData.nickname} /> + ) : userData.snowball_list.length > 0 ? ( + <> + { + const nowSnowBallID = userData.snowball_list.findIndex( + id => id == snowBallData.id + ); + if (nowSnowBallID === undefined) { + throw '알수없는 snowballID입니다.'; + } + const nextSnowBallID = + userData.snowball_list[ + (nowSnowBallID + userData.snowball_count - 1) % + userData.snowball_count + ]; + axios(`/api/snowball/${nextSnowBallID}`) + .then(res => { + setSnowBallData(res.data as SnowBallData); + }) + .catch(e => { + console.error(e); + }); + }} + > + 이전 + + { + const nowSnowBallID = userData.snowball_list.findIndex( + id => id == snowBallData.id + ); + if (nowSnowBallID === undefined) { + throw '알수없는 snowballID입니다.'; + } + const nextSnowBallID = + userData.snowball_list[ + (nowSnowBallID + 1) % userData.snowball_count + ]; + axios(`/api/snowball/${nextSnowBallID}`) + .then(res => { + setSnowBallData(res.data as SnowBallData); + }) + .catch(e => { + console.error(e); + }); + }} + > + 다음 + + ) : null} - + ); }; diff --git a/front/src/pages/Visit/VisitHeader.tsx b/front/src/pages/Visit/VisitHeader.tsx index 5c3b890..3a1e3c4 100644 --- a/front/src/pages/Visit/VisitHeader.tsx +++ b/front/src/pages/Visit/VisitHeader.tsx @@ -25,8 +25,8 @@ const VisitHeader = () => { - - {userData.message_count}개의 편지 + 총 {userData.message_count} + 개의 편지 ); diff --git a/front/src/router/IsLogin.tsx b/front/src/router/IsLogin.tsx index 2ee4b0f..d064ad9 100644 --- a/front/src/router/IsLogin.tsx +++ b/front/src/router/IsLogin.tsx @@ -19,7 +19,7 @@ const IsLogin: React.FC<{ children: ReactNode }> = ({ children }) => { const maxSnowball = 5; useEffect(() => { - //saveCookie(); + // saveCookie(); if (url === '') { axios .get('/api/user', { @@ -28,7 +28,7 @@ const IsLogin: React.FC<{ children: ReactNode }> = ({ children }) => { .then(res => { if (res.status === 200) { const data = res.data; - console.log(data); + if (data.user.nickname === null) { setUrl('/make'); } @@ -41,11 +41,10 @@ const IsLogin: React.FC<{ children: ReactNode }> = ({ children }) => { else { setUrl('/main'); } - } }) .catch(err => { - console.log(err); + console.error(err); navigate('*'); }); } diff --git a/front/src/utils/position.tsx b/front/src/utils/position.tsx index 0cdf24f..a7e36f4 100644 --- a/front/src/utils/position.tsx +++ b/front/src/utils/position.tsx @@ -31,8 +31,8 @@ const getDecoPoisition = (n: number): THREE.Vector3 => { [-1.5, 2.5], [-2.5, -1.5], [1.5, -2.5], - [2.5, -2.5], - [2.5, 2.5] + [-2.5, -2.5], + [-2.5, 2.5] ]; return new THREE.Vector3(positions[n][0], 0, positions[n][1]); }; diff --git a/front/src/utils/styled.tsx b/front/src/utils/styled.tsx index 29554c0..afc7f1a 100644 --- a/front/src/utils/styled.tsx +++ b/front/src/utils/styled.tsx @@ -14,7 +14,7 @@ const Container = styled.div` align-items: center; justify-content: center; width: 100%; - height: 10rem; + min-height: 10rem; gap: 1rem; `;