diff --git a/.pnp.cjs b/.pnp.cjs index d2795a5..3106fbc 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -28,7 +28,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@hookform/error-message", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:2.0.1"],\ ["@hookform/resolvers", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:3.3.4"],\ - ["@playwright/test", "npm:1.41.1"],\ + ["@playwright/test", "npm:1.42.1"],\ ["@radix-ui/react-dialog", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:1.0.5"],\ ["@storybook/addon-essentials", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:7.6.11"],\ ["@storybook/addon-interactions", "npm:7.6.11"],\ @@ -4721,11 +4721,11 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@playwright/test", [\ - ["npm:1.41.1", {\ - "packageLocation": "../../../.yarn/berry/cache/@playwright-test-npm-1.41.1-1287dca42b-10c0.zip/node_modules/@playwright/test/",\ + ["npm:1.42.1", {\ + "packageLocation": "../../../.yarn/berry/cache/@playwright-test-npm-1.42.1-1e93511e64-10c0.zip/node_modules/@playwright/test/",\ "packageDependencies": [\ - ["@playwright/test", "npm:1.41.1"],\ - ["playwright", "npm:1.41.1"]\ + ["@playwright/test", "npm:1.42.1"],\ + ["playwright", "npm:1.42.1"]\ ],\ "linkType": "HARD"\ }]\ @@ -19008,7 +19008,7 @@ const RAW_RUNTIME_STATE = ["namui-wiki", "workspace:."],\ ["@hookform/error-message", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:2.0.1"],\ ["@hookform/resolvers", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:3.3.4"],\ - ["@playwright/test", "npm:1.41.1"],\ + ["@playwright/test", "npm:1.42.1"],\ ["@radix-ui/react-dialog", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:1.0.5"],\ ["@storybook/addon-essentials", "virtual:90dbb7f2ed8b1a56cce3f1f73e506cc7e023b0347aaf251ee9326126e62c4ab536d7594f1a2d350fe3ae0a3fa18de5cdd9e511a11073e430e65622303b2a587f#npm:7.6.11"],\ ["@storybook/addon-interactions", "npm:7.6.11"],\ @@ -20207,6 +20207,15 @@ const RAW_RUNTIME_STATE = ["playwright-core", "npm:1.41.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.42.1", {\ + "packageLocation": "../../../.yarn/berry/cache/playwright-npm-1.42.1-ad14b1d04e-10c0.zip/node_modules/playwright/",\ + "packageDependencies": [\ + ["playwright", "npm:1.42.1"],\ + ["fsevents", "patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1"],\ + ["playwright-core", "npm:1.42.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["playwright-core", [\ @@ -20216,6 +20225,13 @@ const RAW_RUNTIME_STATE = ["playwright-core", "npm:1.41.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.42.1", {\ + "packageLocation": "./.yarn/unplugged/playwright-core-npm-1.42.1-9ffc4604de/node_modules/playwright-core/",\ + "packageDependencies": [\ + ["playwright-core", "npm:1.42.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["pnp-webpack-plugin", [\ diff --git a/components/carousel/carousel.components.tsx b/components/carousel/carousel.components.tsx index 0be9abe..a4d3e4f 100644 --- a/components/carousel/carousel.components.tsx +++ b/components/carousel/carousel.components.tsx @@ -1,6 +1,6 @@ import { cn } from '@/lib/client/utils' import { MotionValue, useTransform, m, circOut } from 'framer-motion' -import React, { PropsWithChildren, useMemo } from 'react' +import React, { PropsWithChildren, useEffect, useMemo } from 'react' export const DotButton = ({ index, diff --git a/components/carousel/index.tsx b/components/carousel/index.tsx index 183329f..83fc407 100644 --- a/components/carousel/index.tsx +++ b/components/carousel/index.tsx @@ -72,7 +72,7 @@ const Carousel = ({
{slides.map((item, index) => ( diff --git a/components/compositions/tree-card/index.tsx b/components/compositions/tree-card/index.tsx index 7cf56c6..61f29f0 100644 --- a/components/compositions/tree-card/index.tsx +++ b/components/compositions/tree-card/index.tsx @@ -52,7 +52,7 @@ const TreeCard = ({ + Math.random() * (max - min) + min + +const hexToRgb = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null +} + +interface ParticleOptions { + x: number + y: number + deg: number + r?: number + spread?: number +} + +class Particle { + public x: number + public y: number + public vx: number + public vy: number + public r: number + + public width = 12 + public height = 12 + public opacity = 1 + + public widthDelta = randomNumBetween(0, 360) + public heightDelta = randomNumBetween(0, 360) + public rotation = randomNumBetween(0, 360) + + constructor( + { + x, + y, + deg = 0, + r = randomNumBetween(10, 30), + spread = 15, + }: ParticleOptions, + private readonly friction = 0.89, + private readonly gravity = 0.5, + private readonly angle = (Math.PI / 180) * randomNumBetween(0, 360), + + private readonly rotationDelta = randomNumBetween(-1, 1), + private readonly colors = ['#00BC68', '#E2F6E9', '#005E16', '#E2F5FF'], + private readonly color = hexToRgb( + colors[Math.floor(randomNumBetween(0, colors.length))], + )!, + private readonly shapes = ['circle', 'square'], + private readonly shape = shapes[ + Math.floor(randomNumBetween(0, shapes.length)) + ], + ) { + this.r = r + this.x = x + this.y = y + + this.vx = this.r * Math.cos(this.angle) + this.vy = this.r * Math.sin(this.angle) + } + + update() { + this.vy += this.gravity + + this.vx *= this.friction + this.vy *= this.friction + + this.x += this.vx + this.y += this.vy + + this.opacity -= 0.005 + + this.widthDelta += 2 + this.heightDelta += 2 + this.rotation += this.rotationDelta + } + + drawSquare(ctx: CanvasRenderingContext2D) { + ctx.fillRect( + this.x, + this.y, + this.width * Math.cos((Math.PI / 180) * this.widthDelta), + this.height * Math.sin((Math.PI / 180) * this.heightDelta), + ) + } + drawCircle(ctx: CanvasRenderingContext2D) { + ctx.beginPath() + ctx.ellipse( + this.x, + this.y, + Math.abs(this.width * Math.cos((Math.PI / 180) * this.widthDelta)) / 2, + Math.abs(this.height * Math.sin((Math.PI / 180) * this.heightDelta)) / 2, + 0, + 0, + Math.PI * 2, + ) + ctx.fill() + ctx.closePath() + } + + draw(ctx: CanvasRenderingContext2D) { + ctx.translate(this.x + this.width * 1.2, this.y + this.height * 1.2) + ctx.rotate((Math.PI / 180) * this.rotation) + ctx.translate(-this.x - this.width * 1.2, -this.y - this.height * 1.2) + ctx.fillStyle = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.opacity})` + switch (this.shape) { + case 'square': + this.drawSquare(ctx) + break + case 'circle': + this.drawCircle(ctx) + break + } + + ctx.resetTransform() + } +} + +const defaultOptions = { + fps: 60, + intervalTime: 1000, + dpr: typeof window !== 'undefined' && window?.devicePixelRatio > 1 ? 2 : 1, +} + +interface ConfettiProps extends HTMLAttributes {} + +const Confetti = (props: PropsWithChildren) => { + const canvasRef = useRef(null) + const clickableRef = useRef(null) + useEffect(() => { + if (canvasRef.current && clickableRef.current) { + const { dpr, intervalTime, fps } = defaultOptions + const interval = intervalTime / fps + const ctx = canvasRef.current.getContext('2d') + if (ctx) { + let animationId: number + const canvas = canvasRef.current + const canvasWidth = canvas.clientWidth * dpr + const canvasHeight = canvas.clientHeight * dpr + canvas.style.width = canvasWidth + 'px' + canvas.style.height = canvasHeight + 'px' + canvas.width = canvasHeight + canvas.height = canvasHeight + ctx.scale(dpr, dpr) + + const particles: Particle[] = [] + const createConfetti = (event: MouseEvent) => { + if (!clickableRef.current) return + const count = 20 + const deg = 0 + const spread = -1 + + const po = clickableRef.current?.getBoundingClientRect() + const x = event.clientX + (po.width / 2) * dpr + const y = event.clientY - po.height * dpr + console.log() + for (let i = 0; i < count; i++) { + particles.push(new Particle({ x, y, deg, spread })) + } + } + + const render = () => { + let now, delta + let then = Date.now() + + const frame = () => { + animationId = requestAnimationFrame(frame) + now = Date.now() + delta = now - then + if (delta < interval) return + ctx.clearRect(0, 0, canvasWidth, canvasHeight) + + for (let i = particles.length - 1; i >= 0; i--) { + particles[i].update() + particles[i].draw(ctx) + if (particles[i].opacity < 0) { + particles.splice(i, 1) + } + } + then = now - (delta % interval) + } + animationId = requestAnimationFrame(frame) + } + + render() + const clickable = clickableRef.current + + clickable.addEventListener('click', createConfetti) + + return () => { + cancelAnimationFrame(animationId) + clickable.removeEventListener('click', createConfetti) + } + } + } + }, []) + const { children, ...rest } = props + return ( + <> +
{props.children}
+ + + ) +} + +export default Confetti diff --git a/components/icons/welcome-trees.tsx b/components/icons/welcome-trees.tsx index 9fd07ca..f715a46 100644 --- a/components/icons/welcome-trees.tsx +++ b/components/icons/welcome-trees.tsx @@ -3,6 +3,7 @@ import React from 'react' const WelcomeTrees = () => { return ( -

- 나에 대해 얼마나 알고 있나요? -

- - - - - - - - - - - - - - -
, -
-

- 남의위키를 통해{' '} - - 타인의 눈으로 본
- '나'를 발견 -
- 하고 탐구해보세요 -

- - - - - - - - - - - - - - - -
, -
-

- 남이 작성한 내 소개서를 통해 -
- 나에 대해 더 자세히 알 수 있어요 -

- - - - - - - - - - - - - - - - - -
, + , + , + + + , ] interface OnBoardProps { onStartClick: () => void @@ -366,7 +41,7 @@ const OnBoard = ({ onStartClick }: OnBoardProps) => {
item} /> diff --git a/components/onboard/onboard-step1/index.tsx b/components/onboard/onboard-step1/index.tsx new file mode 100644 index 0000000..b39ca13 --- /dev/null +++ b/components/onboard/onboard-step1/index.tsx @@ -0,0 +1,113 @@ +import Button from '@/components/button' +import Confetti from '@/components/confetti' +import WelcomeTrees from '@/components/icons/welcome-trees' +import MetaHead from '@/components/meta-head' +import React from 'react' + +const OnboardStep1 = () => { + return ( +
+

+ 남의위키 링크를 공유해 친구에게 +
+ 나에 대해 알려달라고 부탁해보세요 +

+
+
+ + + + + + + + + +
+
+ + + + +
+
+ +
+

환영해요 김디엔님

+

+ 친구에게 내 소개를 부탁해보세요 +
+ 친구가 소개서를 작성해주면 +
내 정원에 나무가 심겨요 +

+
+
+ + + +
+
+
+
+
+
+
+ ) +} + +export default OnboardStep1 diff --git a/components/onboard/onboard-step2/index.tsx b/components/onboard/onboard-step2/index.tsx new file mode 100644 index 0000000..349fb60 --- /dev/null +++ b/components/onboard/onboard-step2/index.tsx @@ -0,0 +1,248 @@ +import Button from '@/components/button' +import TreeCard from '@/components/compositions/tree-card' +import Confetti from '@/components/confetti' +import BaseLayout from '@/layout/base-layout' +import { GetSurveyResponse } from '@/model/survey.entity' +import { InfiniteData } from '@tanstack/react-query' +import React, { useState } from 'react' + +const surveys: InfiniteData = { + pageParams: [], + pages: [ + { + data: { + size: 6, + totalCount: 6, + totalPage: 1, + page: 0, + content: [ + { + period: 'SIX_MONTHS', + relation: 'MIDDLE_AND_HIGH_SCHOOL', + senderName: '디엔이', + surveyId: '', + }, + { + period: 'ONE_YEAR', + relation: 'WORK', + senderName: '디엔이', + surveyId: '', + }, + { + period: 'FOUR_YEARS', + relation: 'ETC', + senderName: '디엔이', + surveyId: '', + }, + { + period: 'INFINITE', + relation: 'ELEMENTARY_SCHOOL', + senderName: '디엔이', + surveyId: '', + }, + { + period: 'SIX_MONTHS', + relation: 'SOCIAL', + senderName: '디엔이', + surveyId: '', + }, + { + period: 'ONE_YEAR', + relation: 'WORK', + senderName: '디엔이', + surveyId: '', + }, + ], + }, + }, + ] satisfies GetSurveyResponse[], +} + +const OnboardStep2 = () => { + return ( +
+

+ 친구와 알게 된 기간, 경로에 따라 +
+ 나무 카드의 모양과 색이 달라져요 +

+
+
+
+ + + + + + + + + +
+ + 내 정원 +

+ ), + }} + > +
+
+

+ 내 정원에 심어진 나무는 +

+

총 6그루

+
+ + +
+
+
+

+ 받은 친구 +

+
+
+
+ {surveys?.pages.map((page, pageNo) => + pageNo === 0 && page.data.content.length < 20 ? ( + [ + ...page.data.content, + ...Array.from( + { length: 20 - page.data.content.length }, + (v) => null, + ), + ].map((item, index) => + item ? ( + {}} + /> + ) : ( +
+
+ + + + + + +
+
+ ), + ) + ) : ( +
+ {page.data.content.map((item, index) => ( + {}} + /> + ))} +
+ ), + )} +
+
+
+ + + +
+
+
+
+
+
+
+
+
+ ) +} + +export default OnboardStep2 diff --git a/components/onboard/onboard-step3/index.tsx b/components/onboard/onboard-step3/index.tsx new file mode 100644 index 0000000..3d971ea --- /dev/null +++ b/components/onboard/onboard-step3/index.tsx @@ -0,0 +1,367 @@ +import Logo from '@/components/ui/logo' +import useFilter, { KnowFilterType } from '@/hooks/use-filter' +import { cn } from '@/lib/client/utils' +import { fadeInProps } from '@/variants' +import { motion, AnimatePresence, useAnimate } from 'framer-motion' +import React, { HTMLAttributes, PropsWithChildren, useEffect } from 'react' +import Step3TreeInfo from './step3-tree-info' +import Step3BestWorth from './step3-best-worth' +import Step3Character from './step3-character' +import Step3Money from './step3-money' +import Step3Happy from './step3-happy' +import Step3Sad from './step3-sad' + +const filters: { + type: KnowFilterType + text: string + default: string + items: { label: string; value: string }[] +}[] = [ + { + type: 'total', + text: '전체 보기', + default: 'total', + items: [], + }, + { + type: 'relation', + text: '알게 된 경로', + default: 'ELEMENTARY_SCHOOL', + items: [ + { + label: '초등학교', + value: 'ELEMENTARY_SCHOOL', + }, + { + label: '중·고등학교', + value: 'MIDDLE_AND_HIGH_SCHOOL', + }, + { + label: '대학교', + value: 'UNIVERSITY', + }, + { + label: '직장', + value: 'WORK', + }, + { + label: '모임', + value: 'SOCIAL', + }, + { + label: '기타', + value: 'ETC', + }, + ], + }, + { + type: 'period', + text: '알게 된 기간', + default: '', + items: [ + { + label: '6개월 미만', + value: 'SIX_MONTHS', + }, + { + label: '6개월-1년', + value: 'ONE_YEAR', + }, + { + label: '1년-4년', + value: 'FOUR_YEARS', + }, + { + label: '4년 이상', + value: 'INFINITE', + }, + ], + }, +] +const OnboardStep3 = () => { + const { filterIndex, selectedFilter, setFilterIndex } = useFilter() + return ( +
+

+ 내 결과 보기 페이지에서 그룹별로 +
+ 상세 데이터를 확인할 수 있어요 +

+
+
+
+ + + + + + + + + +
+
+
+ + + + +
+ +
+ +
+ + + + + + + +
+
+
+
+ {filters.map((filter, index) => ( + + ))} +
+ + {filters[filterIndex.typeIdx].items.length && ( + + {filters[filterIndex.typeIdx].items.map((item, idx) => ( + + setFilterIndex((prev) => ({ ...prev, valueIdx: idx })) + } + key={item.value} + label={item.label} + /> + ))} + + )} + +
+
+
+ +
+ {/* 가장 중요한 것 - 파이차트 */} +
+ +
+ {/* 이런사람이에요 - 박스 */} +
+ +
+
+ +
+ {/* 기쁠 떄 */} +
+ +
+
+ +
+
+
+
+
+
+
+
+
+ ) +} + +export default OnboardStep3 + +function Section({ + children, + ...props +}: PropsWithChildren>) { + return ( +
+ {children} +
+ ) +} + +interface FilterButtonProps { + selected?: boolean + label: string + onClick?: () => void +} + +const FilterButton = ({ + label, + onClick, + selected = false, +}: FilterButtonProps) => { + const [ref, animate] = useAnimate() + + useEffect(() => { + animate( + ref.current, + selected + ? { + backgroundColor: '#111111', + color: '#FFFFFF', + } + : { + backgroundColor: '#FAFAFA', + color: '#000000', + }, + { + duration: 0.2, + type: 'tween', + ease: 'backInOut', + }, + ) + }, [animate, ref, selected]) + + return ( + { + event.preventDefault() + }} + onClick={onClick} + draggable={false} + variants={fadeInProps.variants} + className={cn( + 'text-[0.8vb] h-[2vb] rounded-full px-[0.4vb] whitespace-nowrap avoid-min-w', + 'origin-center select-none', + selected && 'text-text-main-whiteFF', + )} + > + {label} + + ) +} diff --git a/components/onboard/onboard-step3/step3-best-worth.tsx b/components/onboard/onboard-step3/step3-best-worth.tsx new file mode 100644 index 0000000..f528b05 --- /dev/null +++ b/components/onboard/onboard-step3/step3-best-worth.tsx @@ -0,0 +1,244 @@ +import { useMemo, useRef } from 'react' +import { cn, useBrowserLayoutEffect } from '@/lib/client/utils' +import { Pie, Sector } from 'recharts' +import { Cell } from 'recharts' +import { PieChart, ResponsiveContainer } from 'recharts' +import { PieSectorDataItem } from 'recharts/types/polar/Pie' +import { useInViewRef } from '@/hooks/use-in-view-ref' +import { FilterType } from '@/hooks/use-filter' +import { RANK_COLOR } from '@/constants' +import Button from '@/components/button' +import { DashboardData } from '@/model/dashboard.entity' +export interface Payload { + percent: number + name: number + midAngle: number + middleRadius: number + cx: number + cy: number + stroke: string + fill: string + legend: string + percentage: number + color: string + innerRadius: number + outerRadius: number + maxRadius: number + value: number + startAngle: number + endAngle: number + paddingAngle: number + tabIndex: number +} +const RenderActiveShape = (props: PieSectorDataItem) => { + const { + cx, + cy, + innerRadius, + outerRadius, + startAngle, + endAngle, + fill, + percent, + name, + payload, + color, + legend, + } = props as PieSectorDataItem & Payload + const textRef = useRef(null) + const IsStartAnimation = useRef(false) + useBrowserLayoutEffect(() => { + if (!IsStartAnimation.current) { + const DURATION = 1500 + const easeOutQuint = (x: number): number => { + return 1 - Math.pow(1 - x, 5) + } + + const target = (percent ?? 0) * 100 + + let animationId: number + // 최초 시작 시간 + let start: number + + const animate = () => { + if (!start) start = new Date().getTime() + // 현재시간 - 최초시작시간 + const timestamp = new Date().getTime() + const progress = timestamp - start + if (progress >= DURATION) { + if (textRef.current) { + textRef.current.textContent = `${Math.floor(target)}%` + } + return cancelAnimationFrame(animationId) + } + + const p = progress / DURATION + const value = easeOutQuint(p) + if (textRef.current) { + textRef.current.textContent = `${Math.round(target * value)}%` + } + if (p < DURATION) { + animationId = requestAnimationFrame(animate) + } + } + + animationId = requestAnimationFrame(animate) + IsStartAnimation.current = true + } + }, [percent]) + + return ( + + + {legend} + + + + + ) +} + +const statisics: DashboardData['statistics'][number] = { + dashboardType: 'BEST_WORTH', + questionId: '', + rank: [ + { + legend: '돈', + percentage: 71, + }, + { + legend: '명예', + percentage: 28, + }, + { + legend: '우정', + percentage: 0, + }, + { + legend: '직접 입력', + percentage: 0, + }, + { + legend: '사랑', + percentage: 0, + }, + ], +} +function Step3BestWorth({ filter }: { filter: FilterType }) { + const { ref, inView } = useInViewRef({ + once: true, + margin: '5%', + }) + + const orderByMaxValueList = useMemo(() => { + const arr = statisics?.rank?.sort((a, b) => b.percentage - a.percentage) + + return arr?.map((item, index) => ({ + ...item, + percentage: item.percentage ?? 0, + color: + RANK_COLOR[index] ?? + `rgb(${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)})`, + })) + }, [statisics]) + + return ( +
+ <> +

+ 가장 중요한 것은{' '} + + {orderByMaxValueList?.[0].legend} + + 이네요 +

+
+
+ {inView && ( + + + + {orderByMaxValueList?.map((entry, index) => ( + + ))} + + + + )} +
+
+ {orderByMaxValueList?.slice(0, 3).map((item) => { + return ( +
+
+

+ {item.legend} +

+ + {item.percentage}% + +
+ ) + })} +
+
+
+ +
+ +
+ ) +} + +export default Step3BestWorth diff --git a/components/onboard/onboard-step3/step3-character.tsx b/components/onboard/onboard-step3/step3-character.tsx new file mode 100644 index 0000000..d7a04a9 --- /dev/null +++ b/components/onboard/onboard-step3/step3-character.tsx @@ -0,0 +1,174 @@ +import { m, LazyMotion, domAnimation } from 'framer-motion' +import { fadeInProps } from '@/variants' +import { useInViewRef } from '@/hooks/use-in-view-ref' +import { useSession } from '@/provider/session-provider' +import { FilterType } from '@/hooks/use-filter' +import { Statistic } from '@/model/dashboard.entity' + +const characterMap = { + busy: [ + { + emoji: '🛌', + top: '주말마다', + bottom: '집에서 쉬는 편', + }, + { + emoji: '🕐', + top: '주말마다', + bottom: '약속이 있는 편', + }, + ], + friendly: [ + { emoji: '🫢', top: '친해지는데', bottom: '시간이 걸리는 편' }, + { emoji: '🤗', top: '사람들과', bottom: '빨리 친해지는 편' }, + ], + mbti: [ + { + emoji: '🙅‍♂️', + top: 'MBTI에', + bottom: '몰입하지 않는 편', + }, + { + emoji: '🧐', + top: 'MBTI에 ', + bottom: '과몰입하는 편', + }, + ], + similar: [ + { + emoji: '🙅‍♂️', + top: '답변자들과', + bottom: '다른 성향', + }, + { + emoji: '🙆‍♂️', + top: '답변자들과', + bottom: '비슷한 성향', + }, + ], +} + +const statistics: Statistic = { + dashboardType: 'CHARACTER', + questionId: '65d8f7b8c934b525dd047566', + friendly: true, + similar: true, + mbti: false, + busy: true, +} + +const Step3Character = ({ filter }: { filter: FilterType }) => { + return ( + +
+ +
+
+ ) +} + +export default Step3Character + +const cardPickingVariants = { + initial: { + rotateX: '40deg', + scale: 0.3, + }, + picking: { + rotateX: '0deg', + scale: 1, + transition: { + delay: 0.3, + duration: 0.2, + }, + }, +} + +function CharacterInfo({ statisics }: { statisics: Statistic }) { + const { data } = useSession() + const { inView, ref } = useInViewRef({ once: true }) + + return ( + <> +

+ 김디엔님은 이런사람이에요 +

+ + {statisics.busy} + + + + + + + ) +} + +function CharacterBlock({ + emoji, + bottomText, + href, + topText, +}: { + emoji: string + topText: string + bottomText: string + href: string +}) { + const { inView, ref } = useInViewRef({ once: true }) + + return ( + +

{emoji}

+
+

+ {topText} +
+ {bottomText} +

+
+
+ ) +} diff --git a/components/onboard/onboard-step3/step3-happy.tsx b/components/onboard/onboard-step3/step3-happy.tsx new file mode 100644 index 0000000..fa7d8e3 --- /dev/null +++ b/components/onboard/onboard-step3/step3-happy.tsx @@ -0,0 +1,149 @@ +import Button from '@/components/button' +import { RANK_COLOR } from '@/constants' +import { FilterType } from '@/hooks/use-filter' +import { useInViewRef } from '@/hooks/use-in-view-ref' +import { cn } from '@/lib/client/utils' +import { HTMLMotionProps, m, LazyMotion, domAnimation } from 'framer-motion' +import React, { useMemo } from 'react' + +const statisics = { + dashboardType: 'HAPPY', + questionId: '65d8f7b8c934b525dd04755e', + rank: [ + { + legend: '🏄🏼 취미생활을 즐겨요', + percentage: 11, + }, + { + legend: '👏 혼자 조용히 즐겨요', + percentage: 11, + }, + { + legend: '🎉 사람들에게 알리고 축하받아요', + percentage: 44, + }, + { + legend: '🍱 맛있는 음식을 먹어요', + percentage: 33, + }, + { + legend: '✍️ 직접 입력', + percentage: 0, + }, + ], +} + +const Step3Happy = ({ filter }: { filter: FilterType }) => { + const { inView, ref } = useInViewRef({ + once: true, + amount: 'all', + }) + + const orderByMaxValueList = useMemo(() => { + const arr = statisics?.rank?.sort((a, b) => b.percentage - a.percentage) + + return arr?.map((item, index) => ({ + ...item, + color: + RANK_COLOR[index] ?? + `rgb(${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)})`, + text: item.legend.split(' ')[1], + })) + }, [statisics]) + + return ( + +
+ <> +

+ 기쁠 때 +
+ + {orderByMaxValueList?.[0].text} + +

+
+ {orderByMaxValueList?.slice(0, 3).map((item, index) => { + return ( + + ) + })} +
+
+ +
+ +
+
+ ) +} + +export default Step3Happy + +interface BarProps extends HTMLMotionProps<'div'> { + title: string + percent: number + color: string + active?: boolean + accent?: boolean +} + +function Bar({ + color, + title, + percent, + active = false, + accent = false, + ...rest +}: BarProps) { + const font = accent ? 'text-[1vb] font-bold' : 'text-[1vb] font-medium' + const 최소바크기보정값 = (80 * percent) / 100 + 10 + return ( +
+

{title}

+
+ +

+ {percent}% +

+
+
+ ) +} diff --git a/components/onboard/onboard-step3/step3-money.tsx b/components/onboard/onboard-step3/step3-money.tsx new file mode 100644 index 0000000..c4d88f8 --- /dev/null +++ b/components/onboard/onboard-step3/step3-money.tsx @@ -0,0 +1,161 @@ +import Button from '@/components/button' +import useDetailDrawer from '@/hooks/use-detail-drawer' +import { FilterType } from '@/hooks/use-filter' +import { useInViewRef } from '@/hooks/use-in-view-ref' +import { cn } from '@/lib/client/utils' +import { useSession } from '@/provider/session-provider' +import { LazyMotion, domAnimation, m } from 'framer-motion' +import React, { useMemo, useState } from 'react' + +const statistics = { + dashboardType: 'MONEY', + questionId: '65d8f7b8c934b525dd047560', + peopleCount: 11, + average: 18309133, + entireAverage: 32453728, +} + +const Step3Money = ({ filter }: { filter: FilterType }) => { + const { handle } = useDetailDrawer() + + const { ref, inView } = useInViewRef({ + once: true, + amount: 'all', + }) + const myAvg = useMemo(() => { + const total = (statistics?.average ?? 0) + (statistics?.entireAverage ?? 0) + + return { + mine: (statistics?.average ?? 0) / total, + entire: (statistics?.entireAverage ?? 0) / total, + } + }, [statistics]) + return ( + +
+ <> +

+ + {statistics.peopleCount}명 + + 에게 +
+ + {statistics.average.toLocaleString()}원 + {' '} + 빌릴 수 있어요 +

+
+
+ + +
+
+ +
+ +
+
+
+ ) +} + +export default Step3Money + +interface BarProps { + active?: boolean + isMe?: boolean + value: number + price: number +} + +function Bar({ active, value, price, isMe = true }: BarProps) { + const { data } = useSession() + const [isDone, setIsDone] = useState(false) + return ( +
+
+
+ {price.toLocaleString()}원 + + + +
+ { + setIsDone(true) + }} + animate={ + active + ? isDone + ? { + height: [`${value}%`, `${value * 0.8}%`], + transition: { + repeat: Infinity, + repeatType: 'mirror', + duration: Math.random() * 5 + 1.2, + }, + } + : { + height: `${value}%`, + transition: { + delay: 0.15, + duration: 0.5, + }, + } + : {} + } + className={cn( + 'w-full origin-bottom relative rounded-md', + isMe ? 'bg-brand-sub1-yellow900' : 'bg-gray-gray100', + )} + /> +
+ +

+ {isMe ? '김디엔 님' : '이용자 평균'} +

+
+ ) +} diff --git a/components/onboard/onboard-step3/step3-sad.tsx b/components/onboard/onboard-step3/step3-sad.tsx new file mode 100644 index 0000000..d1d338c --- /dev/null +++ b/components/onboard/onboard-step3/step3-sad.tsx @@ -0,0 +1,149 @@ +import Button from '@/components/button' +import { RANK_COLOR } from '@/constants' +import { FilterType } from '@/hooks/use-filter' +import { useInViewRef } from '@/hooks/use-in-view-ref' +import { cn } from '@/lib/client/utils' +import { HTMLMotionProps, m, LazyMotion, domAnimation } from 'framer-motion' +import React, { useMemo } from 'react' + +const statisics = { + dashboardType: 'SAD', + questionId: '65d8f7b8c934b525dd04755f', + rank: [ + { + legend: '🙌 사람들의 위로와 공감을 원해요', + percentage: 0, + }, + { + legend: '🚴🏼 스트레스를 풀기 위해 여가생활을 즐겨요', + percentage: 30, + }, + { + legend: '✍️ 직접 입력', + percentage: 0, + }, + { + legend: '😭 혼자 끙끙 앓아요', + percentage: 50, + }, + { + legend: '🙏 사람들에게 조언을 구해요', + percentage: 20, + }, + ], +} + +const Step3Sad = ({ filter }: { filter: FilterType }) => { + const { inView, ref } = useInViewRef({ + once: true, + amount: 'all', + }) + + const orderByMaxValueList = useMemo(() => { + const arr = statisics?.rank?.sort((a, b) => b.percentage - a.percentage) + + return arr?.map((item, index) => ({ + ...item, + color: + RANK_COLOR[index] ?? + `rgb(${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)},${Math.floor(Math.random() * 255)})`, + text: item.legend.split(' ')[1], + })) + }, [statisics]) + + return ( + +
+ <> +

+ 슬프거나 화날 때 +
+ + {orderByMaxValueList?.[0].text} + +

+
+ {orderByMaxValueList?.slice(0, 3).map((item, index) => { + return ( + + ) + })} +
+
+ +
+ +
+
+ ) +} + +export default Step3Sad + +interface BarProps extends HTMLMotionProps<'div'> { + title: string + percent: number + color: string + active?: boolean + accent?: boolean +} + +function Bar({ + color, + title, + percent, + active = false, + accent = false, + ...rest +}: BarProps) { + const font = accent ? 'text-[1vb] font-bold' : 'text-[1vb] font-medium' + const 최소바크기보정값 = (80 * percent) / 100 + 10 + return ( +
+

{title}

+
+ +

+ {percent}% +

+
+
+ ) +} diff --git a/components/onboard/onboard-step3/step3-tree-info.tsx b/components/onboard/onboard-step3/step3-tree-info.tsx new file mode 100644 index 0000000..2a9d90a --- /dev/null +++ b/components/onboard/onboard-step3/step3-tree-info.tsx @@ -0,0 +1,168 @@ +import Button from '@/components/button' +import Confetti from '@/components/confetti' +import { FilterType } from '@/hooks/use-filter' +import { cn } from '@/lib/client/utils' +import { SHORT_TYPE_LIST } from '@/model/question.entity' +import { getQuestionByTypeQuery } from '@/queries/question' +import { useQuery } from '@tanstack/react-query' +import { HTMLAttributes } from 'react' + +const SHORT_FILTER: { [key in SHORT_TYPE_LIST[number]]: string } = { + FIRST_IMPRESSION: '👀 나의 첫인상은?', + CHARACTER_CELEBRITY_ASSOCIATION: '🤔 나는 누구와 닮았나요?', + FIVE_LETTER_WORD: '🧐 나를 5글자로 표현한다면?', + LEARNING_ASPIRATION: '📚 나의 이런점은 꼭 배우고 싶어요!', + SECRET_PLEASURE: '😍 내가 혼자 몰래 좋아하고 있는 것은?', + MOST_USED_WORD: '💬 내가 가장 많이 사용하는 단어는?', +} + +const Step3TreeInfo = ({ filter }: { filter: FilterType }) => { + const { data: short } = useQuery({ + ...getQuestionByTypeQuery('SHORT_ANSWER'), + select(data) { + return data.data + }, + }) + + return ( + <> +
+
+

+ 내 정원에 심어진 나무는 +
+ + 총 11그루 + +

+
+ + + + + + + + + + + +
+
+ + + +
+

+ 김디엔님에 대해 알아보세요! +

+ + {short?.length ? ( + <> +
+ {short.slice(0, short.length / 2).map((item) => ( + {}} + title={SHORT_FILTER[item.name]} + /> + ))} +
+
+ {short.slice(short.length / 2, short.length).map((item) => ( + {}} + title={SHORT_FILTER[item.name]} + /> + ))} +
+ + ) : null} + + ) +} + +export default Step3TreeInfo + +interface BadgeProps extends HTMLAttributes { + title: string + scroll?: boolean + replace?: boolean + prefetch?: boolean +} + +const Badge = ({ title, ...rest }: BadgeProps) => { + return ( + + ) +} diff --git a/lib/namui-api.ts b/lib/namui-api.ts index 7a42dfc..f562a4b 100644 --- a/lib/namui-api.ts +++ b/lib/namui-api.ts @@ -25,6 +25,18 @@ export class NamuiApi { baseURL: process.env.NEXT_PUBLIC_API_URL, } private static accessToken: string = '' + + static initOnBoard() { + return NamuiApi.handler({ + method: 'GET', + url: '/api/init', + baseURL: + typeof window !== 'undefined' + ? window.location.origin + : process.env.HOST, + }) + } + /** * @NOTE 로그인 API */ diff --git a/model/survey.entity.ts b/model/survey.entity.ts index 03d53c2..209ab28 100644 --- a/model/survey.entity.ts +++ b/model/survey.entity.ts @@ -1,3 +1,5 @@ +import { Period, Relation } from './tree.entity' + export interface GetSurveyResponse { data: GetSurveyData } @@ -12,7 +14,7 @@ export interface GetSurveyData { export interface Survey { surveyId: string - relation: string - period: string + relation: Relation + period: Period senderName: string } diff --git a/package.json b/package.json index 071eb7c..cb85bbd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "zustand": "^4.5.0" }, "devDependencies": { - "@playwright/test": "^1.41.1", + "@playwright/test": "^1.42.1", "@storybook/addon-essentials": "7.6.11", "@storybook/addon-interactions": "7.6.11", "@storybook/addon-links": "7.6.11", diff --git a/pages/api/init.ts b/pages/api/init.ts new file mode 100644 index 0000000..dc0bd67 --- /dev/null +++ b/pages/api/init.ts @@ -0,0 +1,25 @@ +import { withError } from '@/lib/server/utils' +import withHandler from '@/lib/server/with-handler' +import { serialize } from 'cookie' +import type { NextApiRequest, NextApiResponse } from 'next' + +async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + res.setHeader('Set-Cookie', [ + serialize('namui-init', new Date().toLocaleString(), { + path: '/', + httpOnly: false, + maxAge: 2147483647, + }), + ]) + return res.status(200).json({ ok: true }) + } catch (err) { + console.log(err, '???') + return withError(res, { status: 400 }) + } +} + +export default withHandler({ + methods: ['GET'], + handler, +}) diff --git a/pages/assets/onboard/1.png b/pages/assets/onboard/1.png new file mode 100644 index 0000000..8a2d1f6 Binary files /dev/null and b/pages/assets/onboard/1.png differ diff --git a/pages/assets/onboard/2.png b/pages/assets/onboard/2.png new file mode 100644 index 0000000..05f6a94 Binary files /dev/null and b/pages/assets/onboard/2.png differ diff --git a/pages/assets/onboard/3.png b/pages/assets/onboard/3.png new file mode 100644 index 0000000..bdcca3f Binary files /dev/null and b/pages/assets/onboard/3.png differ diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 5ffb58b..9c222ce 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -30,33 +30,30 @@ const Page = () => { setSelectedQsId(id) } return ( - <> - router.replace('/garden'), - onCenterClick: () => router.replace('/garden'), - showRight: true, - - }, - }} - className={cn('h-calc-h overflow-y-scroll')} + router.replace('/garden'), + onCenterClick: () => router.replace('/garden'), + showRight: true, + }, + }} + className={cn('h-calc-h overflow-y-scroll')} + > + - - - - + + + - - - - - - + + + + + ) } diff --git a/pages/onboard/index.tsx b/pages/onboard/index.tsx index c79df72..038934f 100644 --- a/pages/onboard/index.tsx +++ b/pages/onboard/index.tsx @@ -1,4 +1,5 @@ import OnBoard from '@/components/onboard' +import { NamuiApi } from '@/lib/namui-api' import Cookie from 'js-cookie' import { GetServerSideProps } from 'next' import { useRouter } from 'next/router' @@ -6,18 +7,11 @@ import { ReactNode } from 'react' const Page = () => { const router = useRouter() - return ( - { - Cookie.set('namui-init', new Date().toLocaleString(), { - secure: false, - expires: Infinity, - path: '/', - }) - router.replace('/') - }} - /> - ) + const checkInit = async () => { + await NamuiApi.initOnBoard() + router.replace('/') + } + return } Page.getLayout = (page: ReactNode) => { diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 238293c..e69e65c 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,15 +1,15 @@ -http://namui-wiki.life2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/answers2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/csrf2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/dashboard2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/garden2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/login2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/onboard2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/signup2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/submit2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/surveys2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/surveys/questions2024-02-28T05:22:34.569Zdaily0.7 -http://namui-wiki.life/welcome2024-02-28T05:22:34.569Zdaily0.7 +http://namui-wiki.life2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/answers2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/csrf2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/dashboard2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/garden2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/login2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/onboard2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/signup2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/submit2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/surveys2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/surveys/questions2024-03-03T11:09:10.222Zdaily0.7 +http://namui-wiki.life/welcome2024-03-03T11:09:10.222Zdaily0.7 \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index 56e19c3..7ba5f79 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -205,6 +205,7 @@ const config = { boxShadow: { basic: '0px 4px 10px rgba(0,0,0,0.06)', 'chat-bubble': '4px 4px 16px rgba(0, 0, 0, 0.1)', + onboard: '4px 8px 20px rgba(0, 0, 0, 0.08)', }, dropShadow: { 'chat-bubble': '4px 4px 16px rgba(0, 0, 0, 0.1)', diff --git a/yarn.lock b/yarn.lock index cda9a49..d34a8a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2867,14 +2867,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.41.1": - version: 1.41.1 - resolution: "@playwright/test@npm:1.41.1" +"@playwright/test@npm:^1.42.1": + version: 1.42.1 + resolution: "@playwright/test@npm:1.42.1" dependencies: - playwright: "npm:1.41.1" + playwright: "npm:1.42.1" bin: playwright: cli.js - checksum: 72bd5bb67c512027d214b9c54c2a22a469bd19d7809771e53a5bfdcc11330591e01579bb22f807d1ebbcdcea35d625e0fc9eb9791cebcc63bf55b82dd1cdefdd + checksum: e5d7c1ffedabb934643edb010038edcb70d51d224fb6444844a854d94365a6179d4407a83da176cae37ccd42b62c148843e0b6f9b4c6506048e06558c00d4267 languageName: node linkType: hard @@ -14123,7 +14123,7 @@ __metadata: dependencies: "@hookform/error-message": "npm:^2.0.1" "@hookform/resolvers": "npm:^3.3.4" - "@playwright/test": "npm:^1.41.1" + "@playwright/test": "npm:^1.42.1" "@radix-ui/react-dialog": "npm:^1.0.5" "@storybook/addon-essentials": "npm:7.6.11" "@storybook/addon-interactions": "npm:7.6.11" @@ -15199,7 +15199,31 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.41.1, playwright@npm:^1.14.0, playwright@npm:^1.41.1": +"playwright-core@npm:1.42.1": + version: 1.42.1 + resolution: "playwright-core@npm:1.42.1" + bin: + playwright-core: cli.js + checksum: 9bb0be6defa32eb1b01429615f10c2ad17dcf701656c081a250369c1eb3b0dcc2a0ee21188cd653cdd2303ca73ff94df0d270b178fe3897eba274793dab368ce + languageName: node + linkType: hard + +"playwright@npm:1.42.1": + version: 1.42.1 + resolution: "playwright@npm:1.42.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.42.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 91dcbfe92d75ca9eb4bfff69bb1ec28007b5a96f6187f48e52aa0f6acf8c24f6039ed6467c152964cc92f4ab64b85dc665b13c52b2fb9f7b9182ddb9db404e37 + languageName: node + linkType: hard + +"playwright@npm:^1.14.0, playwright@npm:^1.41.1": version: 1.41.1 resolution: "playwright@npm:1.41.1" dependencies: