From 160363ba3414c1ce76fdb7d9f2acaaa9381de660 Mon Sep 17 00:00:00 2001 From: topeanut Date: Mon, 15 Sep 2025 20:51:12 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/main/mainPage.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/pages/main/mainPage.tsx b/src/components/pages/main/mainPage.tsx index 283b005..b926a27 100644 --- a/src/components/pages/main/mainPage.tsx +++ b/src/components/pages/main/mainPage.tsx @@ -40,6 +40,13 @@ const MainPage = () => { ))} + + + New 전체보기 + {/* 하단 네비게이션 */} From 7fb099d524241146b90adc02ade713bc19c3e958 Mon Sep 17 00:00:00 2001 From: topeanut Date: Thu, 23 Oct 2025 20:36:19 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=EB=A1=9C=EB=94=A9=EC=8A=A4=ED=94=BC?= =?UTF-8?q?=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(unauth)/balanse/loading.tsx | 5 + src/app/(unauth)/entry/loading.tsx | 5 + .../(unauth)/oauth/kakao/redirect/loading.tsx | 5 + src/app/(unauth)/onboarding/loading.tsx | 5 + src/app/create/loading.tsx | 5 + src/app/main/loading.tsx | 5 + src/app/my/loading.tsx | 5 + src/app/poll/[id]/page.tsx | 38 +--- src/app/poll/loading.tsx | 5 + src/components/_shared/inlineLoading.tsx | 91 ++++++++ src/components/_shared/loading.module.css | 214 ++++++++++++++++++ src/components/_shared/loading.tsx | 94 +++++++- src/components/_shared/nav/bottomNavBar.tsx | 3 +- src/components/pages/balanse/balansePage.tsx | 44 ++-- src/components/pages/create/createPage.tsx | 22 +- src/components/pages/main/bestVoteArea.tsx | 22 +- src/components/pages/main/mainPage.tsx | 17 ++ src/components/pages/my/edit/editPage.tsx | 7 +- src/components/pages/my/myPage.tsx | 21 +- src/components/pages/my/myProfileSection.tsx | 7 +- .../pages/oauth/kakao/RedirectPage.tsx | 55 +++-- .../pages/poll/Comment/commentDetail.tsx | 2 +- 22 files changed, 577 insertions(+), 100 deletions(-) create mode 100644 src/app/(unauth)/balanse/loading.tsx create mode 100644 src/app/(unauth)/entry/loading.tsx create mode 100644 src/app/(unauth)/oauth/kakao/redirect/loading.tsx create mode 100644 src/app/(unauth)/onboarding/loading.tsx create mode 100644 src/app/create/loading.tsx create mode 100644 src/app/main/loading.tsx create mode 100644 src/app/my/loading.tsx create mode 100644 src/app/poll/loading.tsx create mode 100644 src/components/_shared/inlineLoading.tsx create mode 100644 src/components/_shared/loading.module.css diff --git a/src/app/(unauth)/balanse/loading.tsx b/src/app/(unauth)/balanse/loading.tsx new file mode 100644 index 0000000..73345e4 --- /dev/null +++ b/src/app/(unauth)/balanse/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function BalanseLoading() { + return +} diff --git a/src/app/(unauth)/entry/loading.tsx b/src/app/(unauth)/entry/loading.tsx new file mode 100644 index 0000000..766a248 --- /dev/null +++ b/src/app/(unauth)/entry/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function EntryLoading() { + return +} diff --git a/src/app/(unauth)/oauth/kakao/redirect/loading.tsx b/src/app/(unauth)/oauth/kakao/redirect/loading.tsx new file mode 100644 index 0000000..2719813 --- /dev/null +++ b/src/app/(unauth)/oauth/kakao/redirect/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function KakaoRedirectLoading() { + return +} diff --git a/src/app/(unauth)/onboarding/loading.tsx b/src/app/(unauth)/onboarding/loading.tsx new file mode 100644 index 0000000..a72a221 --- /dev/null +++ b/src/app/(unauth)/onboarding/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function OnboardingLoading() { + return +} diff --git a/src/app/create/loading.tsx b/src/app/create/loading.tsx new file mode 100644 index 0000000..7e23783 --- /dev/null +++ b/src/app/create/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function CreateLoading() { + return +} diff --git a/src/app/main/loading.tsx b/src/app/main/loading.tsx new file mode 100644 index 0000000..bb4e882 --- /dev/null +++ b/src/app/main/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function MainLoading() { + return +} diff --git a/src/app/my/loading.tsx b/src/app/my/loading.tsx new file mode 100644 index 0000000..0fd5c76 --- /dev/null +++ b/src/app/my/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function MyLoading() { + return +} diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index e2a5423..debf3dc 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -15,6 +15,7 @@ import VoteChart from '@/components/pages/poll/statistics/statisics' import { fetchBestVote } from '@/api/votes' import Header from '@/components/_shared/header' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' +import Loading from '@/components/_shared/loading' interface PollOption { optionId: number @@ -111,19 +112,7 @@ export default function PollDetailPage() { // 인기 탭에서 로딩 중일 때 if (id === 'hot') { - return ( -
-
-
-
-
-

- 가장 인기 있는 투표를 불러오는 중... -

-
-
-
- ) + return } const getHeaderTitle = () => { @@ -146,28 +135,23 @@ export default function PollDetailPage() { } } - if (loading) - return ( -
-
-
로딩 중...
-
- ) + if (loading) return if (error) return ( -
+
-
{error}
+
+
+

⚠️

+

{error}

+

다시 시도해주세요

+
+
) if (!data) return null diff --git a/src/app/poll/loading.tsx b/src/app/poll/loading.tsx new file mode 100644 index 0000000..b1b7179 --- /dev/null +++ b/src/app/poll/loading.tsx @@ -0,0 +1,5 @@ +import Loading from '@/components/_shared/loading' + +export default function PollLoading() { + return +} diff --git a/src/components/_shared/inlineLoading.tsx b/src/components/_shared/inlineLoading.tsx new file mode 100644 index 0000000..e766e93 --- /dev/null +++ b/src/components/_shared/inlineLoading.tsx @@ -0,0 +1,91 @@ +'use client' + +import { useEffect, useState } from 'react' +import styles from './loading.module.css' + +const messages = [ + '밸런스게임 플랫폼 ValanSe', + '당신의 선택을 기다리는 중...', + '오늘은 어떤 선택을 하시겠어요?', +] + +export default function InlineLoading() { + const [message, setMessage] = useState('') + + useEffect(() => { + // 랜덤 메시지 선택 + const randomMessage = messages[Math.floor(Math.random() * messages.length)] + setMessage(randomMessage) + }, []) + + return ( +
+ + + + + {/* 왼쪽 말풍선 */} + + + + + + + {/* 오른쪽 하트 */} + + + + + + {/* 저울 막대 */} + + + + {/* 브랜드 메시지 */} +
+ {message} +
+
+ ) +} diff --git a/src/components/_shared/loading.module.css b/src/components/_shared/loading.module.css new file mode 100644 index 0000000..ab361d5 --- /dev/null +++ b/src/components/_shared/loading.module.css @@ -0,0 +1,214 @@ +@media (min-width: 600px) { + .hotIssueTitle { + font-size: 2rem; + font-weight: 900; + letter-spacing: 0.02em; + } + .hotIssueBar { + padding-top: 32px; + padding-left: 32px; + } + .hotIssueDivider { + height: 3px; + margin-top: 16px; + margin-bottom: 24px; + } +} + +.loadingContainer { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #f0f0f0; + font-family: 'Noto Sans KR', sans-serif; + position: fixed; + top: 0; + left: 0; + z-index: 99999; +} + +@media (max-width: 415px), (max-height: 903px) { + .hotIssueBar { + padding-left: 4vw; + padding-top: 2vw; + } + .hotIssueTitle { + font-size: 1rem; + } + .hotIssueDivider { + margin-top: 2vw; + margin-bottom: 4vw; + } + .loadingText { + font-size: 0.95rem; + } + .loadingSvg { + width: 28vw; + height: 28vw; + min-width: 100px; + min-height: 100px; + } +} + +@media (max-width: 600px) { + .hotIssueBar { + padding: 10px 0 0 8px; + } + .hotIssueTitle { + font-size: 1rem; + } + .hotIssueDivider { + height: 1px; + margin-top: 6px; + margin-bottom: 10px; + } + .loadingText { + font-size: 1rem; + } + .loadingSvg { + width: 120px; + height: 120px; + } +} + +.hotIssueDivider { + position: fixed; + top: 60px; + left: 20px; + width: calc(100% - 40px); + height: 2px; + background: #e0e0e0; + border-radius: 1px; + margin-top: 4px; + margin-bottom: 4px; + border: none; +} + +.hotIssueBar { + position: fixed; + top: 20px; + left: 10px; + color: #0e0e0e; + font-size: 1.3rem; + font-weight: bold; + padding: 8px 18px; + border-radius: 70px; + z-index: 100000; +} + +.loadingSvg { + width: 200px; + height: 200px; +} + +/* 저울 막대 흔들기 */ +.beam { + transform-origin: 65px 73px; + animation: swing 3s ease-in-out infinite; +} + +/* 말풍선 위아래 */ +.bubble { + animation: bubbleMove 3s ease-in-out infinite; +} + +/* 하트 위아래 + pulse */ +.heart { + animation: heartPulse 3s ease-in-out infinite; +} + +@keyframes swing { + 0%, + 100% { + transform: rotate(0deg); + } + 25% { + transform: rotate(6deg); + } + 75% { + transform: rotate(-6deg); + } +} + +@keyframes bubbleMove { + 0%, + 100% { + transform: translateY(0); + } + 25% { + transform: translateY(-4px); + } + 75% { + transform: translateY(4px); + } +} + +@keyframes heartPulse { + 0%, + 100% { + transform: scale(1) translateY(0); + } + 25% { + transform: scale(1.1) translateY(4px); + } + 75% { + transform: scale(1.1) translateY(-3px); + } +} + +/* 텍스트 순환 */ +.loadingText { + margin-top: 1rem; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.2rem; + font-weight: 700; + color: #2c3e50; + height: 1.5rem; + overflow: hidden; + position: relative; +} + +.loadingText span { + position: absolute; + opacity: 0; + animation: fadeText 9s linear infinite; +} + +.loadingText span:nth-child(1) { + animation-delay: 0s; +} + +.loadingText span:nth-child(2) { + animation-delay: 3s; +} + +.loadingText span:nth-child(3) { + animation-delay: 6s; +} + +@keyframes fadeText { + 0% { + opacity: 0; + } + 5% { + opacity: 1; + } + 30% { + opacity: 1; + } + 35% { + opacity: 0; + } + 100% { + opacity: 0; + } +} diff --git a/src/components/_shared/loading.tsx b/src/components/_shared/loading.tsx index afb0d53..e485536 100644 --- a/src/components/_shared/loading.tsx +++ b/src/components/_shared/loading.tsx @@ -1,5 +1,95 @@ -// TODO: 로딩 스피너 디자인 나오면 반영 +'use client' + +import { useEffect, useState } from 'react' +import styles from './loading.module.css' + +const messages = [ + '밸런스게임 플랫폼 ValanSe', + '당신의 선택을 기다리는 중...', + '오늘은 어떤 선택을 하시겠어요?', +] export default function Loading() { - return
로딩 중...
+ const [mounted, setMounted] = useState(false) + const [message, setMessage] = useState('') + + useEffect(() => { + // 랜덤 메시지 선택 + const randomMessage = messages[Math.floor(Math.random() * messages.length)] + setMessage(randomMessage) + setMounted(true) + + const timer = setTimeout(() => {}, 800) + return () => clearTimeout(timer) + }, []) + + if (!mounted) return null + + return ( +
+ + + + + + + + + + + + + + + + + + + {/* 브랜드 메시지 */} +
+ {message} +
+
+ ) } diff --git a/src/components/_shared/nav/bottomNavBar.tsx b/src/components/_shared/nav/bottomNavBar.tsx index 81d0f2d..e8e55ff 100644 --- a/src/components/_shared/nav/bottomNavBar.tsx +++ b/src/components/_shared/nav/bottomNavBar.tsx @@ -1,9 +1,10 @@ import { Suspense } from 'react' import NavBarContent from './navBarContent' +import Loading from '@/components/_shared/loading' function BottomNavBar() { return ( - Loading...
}> + }> ) diff --git a/src/components/pages/balanse/balansePage.tsx b/src/components/pages/balanse/balansePage.tsx index 6c7b26b..332c814 100644 --- a/src/components/pages/balanse/balansePage.tsx +++ b/src/components/pages/balanse/balansePage.tsx @@ -8,6 +8,7 @@ import { useEffect, useState, Suspense, useCallback, useRef } from 'react' import { Vote } from '@/types/balanse/vote' import { useRouter, useSearchParams } from 'next/navigation' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' +import Loading from '@/components/_shared/loading' import React from 'react' const sortOptions = [ @@ -108,7 +109,15 @@ function BalancePageContent() { try { setLoading(true) setError(null) + + // 최소 0.8초 로딩 시간 보장 + const startTime = Date.now() const data = await fetchVotes({ category, sort, size: 5 }) + const elapsedTime = Date.now() - startTime + const remainingTime = Math.max(0, 800 - elapsedTime) + + await new Promise((resolve) => setTimeout(resolve, remainingTime)) + setVotes(data.votes) setHasNextPage(data.has_next_page) setNextCursor(data.next_cursor) @@ -121,6 +130,11 @@ function BalancePageContent() { getVotes() }, [category, sort]) + // 초기 로딩 중일 때는 전체 화면 로딩 + if (loading && votes.length === 0) { + return + } + return (
@@ -147,15 +161,13 @@ function BalancePageContent() {
- {loading && ( -
-
-

투표를 불러오는 중...

+ {error && ( +
+

⚠️

+

{error}

)} - {error &&
{error}
} - {!loading && - !error && + {!error && votes.map((vote, idx) => ( @@ -166,11 +178,11 @@ function BalancePageContent() { ))} {/* 무한 스크롤 로딩 인디케이터 */} - {hasNextPage && !loading && ( + {hasNextPage && (
{isLoadingMore && ( <> -
+

더 많은 투표를 불러오는 중...

@@ -186,19 +198,7 @@ function BalancePageContent() { export default function BalancePage() { return ( - -
-
-
-
-

페이지를 불러오는 중...

-
-
-
- } - > + }> ) diff --git a/src/components/pages/create/createPage.tsx b/src/components/pages/create/createPage.tsx index 02e809f..4019e1b 100644 --- a/src/components/pages/create/createPage.tsx +++ b/src/components/pages/create/createPage.tsx @@ -2,11 +2,27 @@ import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import CreateForm from './createForm' -import { Suspense } from 'react' +import Loading from '@/components/_shared/loading' +import { useEffect, useState } from 'react' const CreatePage = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true) + + useEffect(() => { + // 최소 0.8초 로딩 시간 보장 + const timer = setTimeout(() => { + setIsInitialLoading(false) + }, 800) + + return () => clearTimeout(timer) + }, []) + + if (isInitialLoading) { + return + } + return ( - Loading...
}> + <>
@@ -19,7 +35,7 @@ const CreatePage = () => {
- + ) } diff --git a/src/components/pages/main/bestVoteArea.tsx b/src/components/pages/main/bestVoteArea.tsx index 4e143d9..21d378a 100644 --- a/src/components/pages/main/bestVoteArea.tsx +++ b/src/components/pages/main/bestVoteArea.tsx @@ -5,32 +5,42 @@ import VoteOptionGrid from './voteOptionGrid' import { BestVoteResponse, fetchBestVote } from '@/api/votes' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' +import InlineLoading from '@/components/_shared/inlineLoading' function BestVoteArea() { const router = useRouter() const [voteData, setVoteData] = useState(null) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const loadBestVote = async () => { try { + // 최소 0.8초 로딩 시간 보장 + const startTime = Date.now() const response = await fetchBestVote() + const elapsedTime = Date.now() - startTime + const remainingTime = Math.max(0, 800 - elapsedTime) + + await new Promise((resolve) => setTimeout(resolve, remainingTime)) setVoteData(response) } catch (error) { console.error('Failed to fetch best vote:', error) + } finally { + setIsLoading(false) } } loadBestVote() }, []) return ( -
- {voteData === null ? ( -
- 로딩중... +
+ {isLoading ? ( +
+
) : ( <> -
+
fire
오늘의 핫이슈 @@ -41,7 +51,7 @@ function BestVoteArea() {
{voteData?.totalParticipants.toLocaleString()}명 참여
-
+
[ {voteData?.title} ]
diff --git a/src/components/pages/main/mainPage.tsx b/src/components/pages/main/mainPage.tsx index 283b005..d0201d3 100644 --- a/src/components/pages/main/mainPage.tsx +++ b/src/components/pages/main/mainPage.tsx @@ -4,6 +4,8 @@ import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Image from 'next/image' import Link from 'next/link' import BestVoteArea from './bestVoteArea' +import Loading from '@/components/_shared/loading' +import { useEffect, useState } from 'react' // 테스트 데이터 const categories = [ @@ -13,6 +15,21 @@ const categories = [ ] const MainPage = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true) + + useEffect(() => { + // 최소 0.8초 로딩 시간 보장 + const timer = setTimeout(() => { + setIsInitialLoading(false) + }, 800) + + return () => clearTimeout(timer) + }, []) + + if (isInitialLoading) { + return + } + return (
diff --git a/src/components/pages/my/edit/editPage.tsx b/src/components/pages/my/edit/editPage.tsx index 0bea8a7..05cb2be 100644 --- a/src/components/pages/my/edit/editPage.tsx +++ b/src/components/pages/my/edit/editPage.tsx @@ -16,6 +16,7 @@ import { } from '@/store/thunks/memberThunks' import { useAppDispatch } from '@/hooks/utils/useAppDispatch' import { useDebounce } from '@/hooks/useDebounce' +import Loading from '@/components/_shared/loading' const ageOptions = ['10대', '20대', '30대', '40대'] const genderOptions = ['여성', '남성'] @@ -129,11 +130,7 @@ const EditPage = () => { } if (!myPageData) { - return ( -
- 마이페이지 정보를 불러오는 중입니다... -
- ) + return } return ( diff --git a/src/components/pages/my/myPage.tsx b/src/components/pages/my/myPage.tsx index 60a6b58..3bbe874 100644 --- a/src/components/pages/my/myPage.tsx +++ b/src/components/pages/my/myPage.tsx @@ -7,19 +7,30 @@ import MyProfileSection from './myProfileSection' import MyActivitySection from './myActivitySection' import AccountControlSection from './accountControlSection' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import { useAppSelector } from '@/hooks/utils/useAppSelector' import { fetchMypageDataThunk } from '@/store/thunks/memberThunks' import { useAppDispatch } from '@/hooks/utils/useAppDispatch' import { useRouter } from 'next/navigation' import { recover } from '@/store/slices/authSlice' +import Loading from '@/components/_shared/loading' function MyPage() { const router = useRouter() const dispatch = useAppDispatch() const mypageData = useAppSelector((state) => state.member.mypageData) const isLogined = useAppSelector((state) => state.auth.isLogined) + const [minLoadingComplete, setMinLoadingComplete] = useState(false) + + useEffect(() => { + // 최소 0.8초 로딩 시간 보장 + const timer = setTimeout(() => { + setMinLoadingComplete(true) + }, 800) + + return () => clearTimeout(timer) + }, []) useEffect(() => { // 로그인 안 되어있으면 리디렉션 @@ -51,12 +62,8 @@ function MyPage() { } }, [dispatch, isLogined, mypageData, router]) - if (!mypageData) { - return ( -
- 마이페이지 정보를 불러오는 중입니다... -
- ) + if (!mypageData || !minLoadingComplete) { + return } return ( diff --git a/src/components/pages/my/myProfileSection.tsx b/src/components/pages/my/myProfileSection.tsx index 618c281..e209e6a 100644 --- a/src/components/pages/my/myProfileSection.tsx +++ b/src/components/pages/my/myProfileSection.tsx @@ -4,17 +4,14 @@ import Image from 'next/image' import { useRouter } from 'next/navigation' import { useAppSelector } from '@/hooks/utils/useAppSelector' +import Loading from '@/components/_shared/loading' export default function MyProfileSection() { const router = useRouter() const { mypageData } = useAppSelector((state) => state.member) if (!mypageData) { - return ( -
- 마이페이지 정보를 불러오는 중입니다... -
- ) + return } const parseGender = (gender: string) => { diff --git a/src/components/pages/oauth/kakao/RedirectPage.tsx b/src/components/pages/oauth/kakao/RedirectPage.tsx index 0062760..f3f9436 100644 --- a/src/components/pages/oauth/kakao/RedirectPage.tsx +++ b/src/components/pages/oauth/kakao/RedirectPage.tsx @@ -1,28 +1,39 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { loginThunk } from '@/store/thunks/authThunks' import { fetchProfileThunk } from '@/store/thunks/memberThunks' import { useAppDispatch } from '@/hooks/utils/useAppDispatch' -import { useAppSelector } from '@/hooks/utils/useAppSelector' +import Loading from '@/components/_shared/loading' export default function KakaoRedirect() { const router = useRouter() const dispatch = useAppDispatch() - const auth = useAppSelector((state) => state.auth) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) useEffect(() => { - const code = new URLSearchParams(window.location.search).get('code') // 코드 확인 + const code = new URLSearchParams(window.location.search).get('code') const handleLogin = async () => { if (code) { try { + // 최소 0.8초 로딩 시간 보장 + const startTime = Date.now() + // 1. 로그인 시도 await dispatch(loginThunk(code)) + try { // 2. 프로필 조회 시도 const profile = await dispatch(fetchProfileThunk()) + + // 최소 0.8초 보장 + const elapsedTime = Date.now() - startTime + const remainingTime = Math.max(0, 800 - elapsedTime) + await new Promise((resolve) => setTimeout(resolve, remainingTime)) + if (profile) { // 프로필이 있으면 main 페이지로 이동 router.push('/main') @@ -35,30 +46,32 @@ export default function KakaoRedirect() { } } catch { // fetchProfileThunk 실패 - alert('프로필 조회 실패') - router.push('/entry') + setError('프로필 조회 실패') + setTimeout(() => router.push('/entry'), 2000) } } catch { // loginThunk 실패 - alert('로그인 실패') - router.push('/entry') + setError('로그인 실패') + setTimeout(() => router.push('/entry'), 2000) + } finally { + setIsLoading(false) } } } handleLogin() - }, []) + }, [dispatch, router]) + + if (error) { + return ( +
+
+

{error}

+

잠시 후 로그인 페이지로 이동합니다...

+
+
+ ) + } - // TODO: 로딩 스피너 디자인 나오면 반영 - return ( - <> - {auth.loading ? ( -
로그인 중입니다...
- ) : auth.error ? ( -
로그인 실패: {auth.error}
- ) : ( -
로그인 성공
- )} - - ) + return isLoading ? : null } diff --git a/src/components/pages/poll/Comment/commentDetail.tsx b/src/components/pages/poll/Comment/commentDetail.tsx index 6cd5b8d..f0e83a3 100644 --- a/src/components/pages/poll/Comment/commentDetail.tsx +++ b/src/components/pages/poll/Comment/commentDetail.tsx @@ -241,7 +241,7 @@ const CommentDetail = ({ {loading && (
-
+

댓글을 불러오는 중...

)} From 3bfbf26eb3876ee80232d2691bae88b00a7a3911 Mon Sep 17 00:00:00 2001 From: topeanut Date: Wed, 29 Oct 2025 10:39:28 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/_shared/inlineLoading.tsx | 2 -- src/components/_shared/loading.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/_shared/inlineLoading.tsx b/src/components/_shared/inlineLoading.tsx index e766e93..11e5e3b 100644 --- a/src/components/_shared/inlineLoading.tsx +++ b/src/components/_shared/inlineLoading.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import styles from './loading.module.css' const messages = [ - '밸런스게임 플랫폼 ValanSe', '당신의 선택을 기다리는 중...', '오늘은 어떤 선택을 하시겠어요?', ] @@ -13,7 +12,6 @@ export default function InlineLoading() { const [message, setMessage] = useState('') useEffect(() => { - // 랜덤 메시지 선택 const randomMessage = messages[Math.floor(Math.random() * messages.length)] setMessage(randomMessage) }, []) diff --git a/src/components/_shared/loading.tsx b/src/components/_shared/loading.tsx index e485536..5a411c6 100644 --- a/src/components/_shared/loading.tsx +++ b/src/components/_shared/loading.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import styles from './loading.module.css' const messages = [ - '밸런스게임 플랫폼 ValanSe', '당신의 선택을 기다리는 중...', '오늘은 어떤 선택을 하시겠어요?', ] @@ -14,7 +13,6 @@ export default function Loading() { const [message, setMessage] = useState('') useEffect(() => { - // 랜덤 메시지 선택 const randomMessage = messages[Math.floor(Math.random() * messages.length)] setMessage(randomMessage) setMounted(true) @@ -27,6 +25,10 @@ export default function Loading() { return (
+
+ 밸런스게임 플랫폼 ValanSe +
+ - {/* 브랜드 메시지 */}
{message}
From 507da07eeebac1ff706d7ad5a67aa19bc2aa37c8 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 4 Nov 2025 00:55:12 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20dimmed=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/modal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx index 1b572c2..4d6660b 100644 --- a/src/components/ui/modal.tsx +++ b/src/components/ui/modal.tsx @@ -7,14 +7,15 @@ import { X } from 'lucide-react' // Overlay: 모달 배경 const ModalOverlay = React.forwardRef< HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( + React.HTMLAttributes & { onClose?: () => void } +>(({ className, onClose, ...props }, ref) => (
)) From d7414c5bc185248487f99e0b92ccccf1dc19cb96 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 4 Nov 2025 01:25:06 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B2=84=EB=B8=94=EB=A7=81=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/modal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx index 4d6660b..6cde652 100644 --- a/src/components/ui/modal.tsx +++ b/src/components/ui/modal.tsx @@ -15,7 +15,11 @@ const ModalOverlay = React.forwardRef< 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center', className, )} - onClick={onClose} + onClick={(e) => { + if (e.target === e.currentTarget) { + onClose?.() + } + }} {...props} /> )) From cd3b6771cec12d937472cfbfc894629e67f442d5 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 17 Nov 2025 02:40:38 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/poll/Comment/commentDetail.tsx | 58 ++++++++++++++++++- src/components/ui/modal.tsx | 10 ++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/components/pages/poll/Comment/commentDetail.tsx b/src/components/pages/poll/Comment/commentDetail.tsx index f0e83a3..dcef7f4 100644 --- a/src/components/pages/poll/Comment/commentDetail.tsx +++ b/src/components/pages/poll/Comment/commentDetail.tsx @@ -17,6 +17,16 @@ import { MoreVertical, Trash2, } from 'lucide-react' +import { + Modal, + ModalOverlay, + ModalHeader, + ModalTitle, + ModalDescription, + ModalBody, + ModalFooter, + ModalCloseButton, +} from '@/components/ui/modal' interface CommentDetailProps { comments?: Comment[] @@ -40,6 +50,10 @@ const CommentDetail = ({ {}, ) const [openMenus, setOpenMenus] = useState>({}) + const [deleteConfirmModal, setDeleteConfirmModal] = useState<{ + isOpen: boolean + commentId: number | null + }>({ isOpen: false, commentId: null }) // 초기 댓글 설정 useEffect(() => { @@ -183,19 +197,29 @@ const CommentDetail = ({ } const handleCommentDelete = async (commentId: number) => { - if (!confirm('정말로 이 댓글을 삭제하시겠습니까?')) return + setDeleteConfirmModal({ isOpen: true, commentId }) + } + + const confirmDelete = async () => { + const { commentId } = deleteConfirmModal + if (!commentId) return try { await deleteComment(commentId) setLocalComments((prev) => prev.filter((comment) => comment.commentId !== commentId), ) + setDeleteConfirmModal({ isOpen: false, commentId: null }) } catch (error) { console.error('댓글 삭제 실패:', error) alert('댓글 삭제에 실패했습니다.') } } + const cancelDelete = () => { + setDeleteConfirmModal({ isOpen: false, commentId: null }) + } + const toggleMenu = (commentId: number) => { setOpenMenus((prev) => ({ ...prev, @@ -412,6 +436,38 @@ const CommentDetail = ({ 접기 )} + + {/* 삭제 확인 모달 */} + {deleteConfirmModal.isOpen && ( + + + + 댓글 삭제 + + + + + 정말로 이 댓글을 삭제하시겠습니까?
이 작업은 되돌릴 수 + 없습니다. +
+
+ + + + +
+
+ )}
) } diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx index 6cde652..b3268a8 100644 --- a/src/components/ui/modal.tsx +++ b/src/components/ui/modal.tsx @@ -32,7 +32,10 @@ const ModalHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)) @@ -45,10 +48,7 @@ const ModalTitle = React.forwardRef< >(({ className, ...props }, ref) => (

)) From 6298dfdd1e06fc04537b16b688b40cb71a99c53c Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 17 Nov 2025 02:45:12 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20dimmed=20=EC=9E=91=EB=8F=99=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/poll/Comment/commentDetail.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/pages/poll/Comment/commentDetail.tsx b/src/components/pages/poll/Comment/commentDetail.tsx index dcef7f4..af223bf 100644 --- a/src/components/pages/poll/Comment/commentDetail.tsx +++ b/src/components/pages/poll/Comment/commentDetail.tsx @@ -25,7 +25,6 @@ import { ModalDescription, ModalBody, ModalFooter, - ModalCloseButton, } from '@/components/ui/modal' interface CommentDetailProps { @@ -439,11 +438,11 @@ const CommentDetail = ({ {/* 삭제 확인 모달 */} {deleteConfirmModal.isOpen && ( - + 댓글 삭제 - + From d201f91fcbb9c9552c02ee8491ed00ae932ecf90 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:59:16 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B8=B0=20=EA=B8=89?= =?UTF-8?q?=EC=83=81=EC=8A=B9=20=ED=86=A0=ED=94=BD=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/pages/valanse/trendinVoteApi.ts | 21 +++++++++++++++++++ src/components/pages/balanse/mockPollCard.tsx | 10 ++++----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 src/api/pages/valanse/trendinVoteApi.ts diff --git a/src/api/pages/valanse/trendinVoteApi.ts b/src/api/pages/valanse/trendinVoteApi.ts new file mode 100644 index 0000000..c31c685 --- /dev/null +++ b/src/api/pages/valanse/trendinVoteApi.ts @@ -0,0 +1,21 @@ +import { authApi } from '../../instance/authApi' + +export type TrendingVoteResponse = { + voteId: number + title: string + content: string + category: string + totalParticipants: number + createdBy: string + createdAt: string + options: { + optionId: number + content: string + vote_count: number + }[] +} + +export async function fetchTrendingVotes() { + const res = await authApi.get('/votes/trending') + return res.data +} diff --git a/src/components/pages/balanse/mockPollCard.tsx b/src/components/pages/balanse/mockPollCard.tsx index b4ed55c..fb8ace0 100644 --- a/src/components/pages/balanse/mockPollCard.tsx +++ b/src/components/pages/balanse/mockPollCard.tsx @@ -1,9 +1,9 @@ 'use client' import { useState, useEffect } from 'react' import { - fetchMostVotedVote, - MostVotedVoteResponse, -} from '@/api/comment/mostVotedVoteApi' + fetchTrendingVotes, + type TrendingVoteResponse, +} from '@/api/pages/valanse/trendinVoteApi' import Link from 'next/link' const categoryMap: Record = { @@ -14,7 +14,7 @@ const categoryMap: Record = { } function MockPollCard() { - const [data, setData] = useState(null) + const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -22,7 +22,7 @@ function MockPollCard() { const getData = async () => { try { setLoading(true) - const res = await fetchMostVotedVote() + const res = await fetchTrendingVotes() setData(res) } catch { setError('불러오기 실패') From eea049358f7786e21600c6d9f477cbb6eb5a043a Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:10:48 +0900 Subject: [PATCH 09/13] =?UTF-8?q?chore:=20packagemanager=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ae187e5..662bde2 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,6 @@ "typescript": "^5", "typescript-eslint": "^8.32.1", "webpack": "^5.99.9" - } + }, + "packageManager": "pnpm@10.16.1" } From e8924f1436f4d3fe6a3205e0a4d598f98802885b Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:30:58 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=EB=82=B4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/my/_shared/balanseHistoryCard.tsx | 11 ++++++++--- .../pages/my/_shared/historyPage.tsx | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/pages/my/_shared/balanseHistoryCard.tsx b/src/components/pages/my/_shared/balanseHistoryCard.tsx index 93ef47b..98e6a57 100644 --- a/src/components/pages/my/_shared/balanseHistoryCard.tsx +++ b/src/components/pages/my/_shared/balanseHistoryCard.tsx @@ -5,15 +5,20 @@ import { useAppSelector } from '@/hooks/utils/useAppSelector' import Image from 'next/image' import { numberToAlphabet } from '@/utils/map' -export default function BalanceHistoryCard({ +export default function BalanseHistoryCard({ data, + onClick, }: { data: MyVoteHistoryItem + onClick?: () => void }) { const nickname = useAppSelector((state) => state.member.profile?.nickname) return ( - + @@ -30,7 +35,7 @@ export default function BalanceHistoryCard({ ))}

- 전체 · {data.category} + {data.category}
{ + const router = useRouter() const [category, setCategory] = useState('ALL') const [sort, setSort] = useState<'latest' | 'popular'>('latest') const [votes, setVotes] = useState([]) const [error, setError] = useState(null) + const handleVoteClick = (voteId: string) => { + router.push(`/poll/${voteId}`) + } + const title = mode === 'created' ? '내가 만든 밸런스 게임' : '내가 투표한 밸런스 게임' useEffect(() => { const getVotes = async () => { try { - const data = mode === 'created' - ? await fetchMineVotesCreated(category, sort) - : await fetchMineVotesVoted(category, sort) + const data = + mode === 'created' + ? await fetchMineVotesCreated(category, sort) + : await fetchMineVotesVoted(category, sort) setVotes(data) setError(null) } catch { @@ -60,7 +67,11 @@ const HistoryPage = ({ mode }: HistoryPageProps) => {
{error &&
{error}
} {votes.map((vote) => ( - + handleVoteClick(String(vote.voteId))} + /> ))}
From 46c526b9b808eb83bed9e0df91b6c643769a162b Mon Sep 17 00:00:00 2001 From: Vercel Date: Tue, 16 Dec 2025 06:29:59 +0000 Subject: [PATCH 11/13] Fix React Server Components CVE vulnerabilities Updated dependencies to fix Next.js and React CVE vulnerabilities. The fix-react2shell-next tool automatically updated the following packages to their secure versions: - next - react-server-dom-webpack - react-server-dom-parcel - react-server-dom-turbopack All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory. Co-authored-by: Vercel --- package.json | 2 +- pnpm-lock.yaml | 82 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 662bde2..498c60c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.525.0", - "next": "15.1.8", + "next": "15.1.11", "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 242c39b..b60af94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^0.525.0 version: 0.525.0(react@19.1.0) next: - specifier: 15.1.8 - version: 15.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.1.11 + version: 15.1.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -315,53 +315,53 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@next/env@15.1.8': - resolution: {integrity: sha512-Kd9zsi2ariJvtAvA5KapkzM/Qp9eXIcVqsuUMQHu9yYmhlGa9kyklf+6TQgVGSCbzsrApKCq9olyk51SmPnyLA==} + '@next/env@15.1.11': + resolution: {integrity: sha512-yp++FVldfLglEG5LoS2rXhGypPyoSOyY0kxZQJ2vnlYJeP8o318t5DrDu5Tqzr03qAhDWllAID/kOCsXNLcwKw==} - '@next/swc-darwin-arm64@15.1.8': - resolution: {integrity: sha512-Mc++CDJgInIjIc1uA5+K6Lde8wObQztaXnuz6rOsN7tVgYBWvwKSa9wtXQDEETl46WNI8ksgpth2SR1DDo52xQ==} + '@next/swc-darwin-arm64@15.1.9': + resolution: {integrity: sha512-sQF6MfW4nk0PwMYYq8xNgqyxZJGIJV16QqNDgaZ5ze9YoVzm4/YNx17X0exZudayjL9PF0/5RGffDtzXapch0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.1.8': - resolution: {integrity: sha512-xmek+PBDN9K7rjDXCXgLsEzgmeJcevm3531pJOriqK+zh7k+yZEEE44G6lOnOqjVdc7ErLoDX6GxuHicDTatkw==} + '@next/swc-darwin-x64@15.1.9': + resolution: {integrity: sha512-fp0c1rB6jZvdSDhprOur36xzQvqelAkNRXM/An92sKjjtaJxjlqJR8jiQLQImPsClIu8amQn+ZzFwl1lsEf62w==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.1.8': - resolution: {integrity: sha512-jrmutnfNjpLUB8bk+n2yJ8tzNdS+A8Q9UxzWUTCcxU08Q96eRtMY2/o/x1y2e5Yu79CgYPYuEe6E0SBOU+HU0Q==} + '@next/swc-linux-arm64-gnu@15.1.9': + resolution: {integrity: sha512-77rYykF6UtaXvxh9YyRIKoaYPI6/YX6cy8j1DL5/1XkjbfOwFDfTEhH7YGPqG/ePl+emBcbDYC2elgEqY2e+ag==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.1.8': - resolution: {integrity: sha512-lq1YacM3+Cyc8iwXD0h16AKp1e786KPFUpcIgFnsmjjOrMU5xBosBN2S395yD791P8i6q0qbbMnAoNOFLiaKhw==} + '@next/swc-linux-arm64-musl@15.1.9': + resolution: {integrity: sha512-uZ1HazKcyWC7RA6j+S/8aYgvxmDqwnG+gE5S9MhY7BTMj7ahXKunpKuX8/BA2M7OvINLv7LTzoobQbw928p3WA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.1.8': - resolution: {integrity: sha512-fmllobaA+xGh8Rlb4CcF84sniDKADIXuAvLJ5nKtDCR0BbfQtHmK4xR2z1E+c9B6dbASW3MCXRj35KBmtAhhnw==} + '@next/swc-linux-x64-gnu@15.1.9': + resolution: {integrity: sha512-gQIX1d3ct2RBlgbbWOrp+SHExmtmFm/HSW1Do5sSGMDyzbkYhS2sdq5LRDJWWsQu+/MqpgJHqJT6ORolKp/U1g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.1.8': - resolution: {integrity: sha512-PX0010o4k+w4M4Z38UfcxDGup1O36n10GUrENQANQMOjcE1cA6Gbb+/R6pBKeIqSOaxsPBIanDlbaQ7f6ylB8g==} + '@next/swc-linux-x64-musl@15.1.9': + resolution: {integrity: sha512-fJOwxAbCeq6Vo7pXZGDP6iA4+yIBGshp7ie2Evvge7S7lywyg7b/SGqcvWq/jYcmd0EbXdb7hBfdqSQwTtGTPg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.1.8': - resolution: {integrity: sha512-5zPbJAzaJvEo/UPR8ch4isVOjUP17/6qLU9TyF7Bl1EYN3c5zguAki5WN6QXMEjWAirerR2EFgE1B6VUHzt2Qg==} + '@next/swc-win32-arm64-msvc@15.1.9': + resolution: {integrity: sha512-crfbUkAd9PVg9nGfyjSzQbz82dPvc4pb1TeP0ZaAdGzTH6OfTU9kxidpFIogw0DYIEadI7hRSvuihy2NezkaNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.1.8': - resolution: {integrity: sha512-tWR35z+E8rThPnwIMtOHwF/7lh7x1eB5p1wW0e5sWtyDIc+HRikxxuDc0U8B5G4YqGPX+O9NOgX35pCeKL28EA==} + '@next/swc-win32-x64-msvc@15.1.9': + resolution: {integrity: sha512-SBB0oA4E2a0axUrUwLqXlLkSn+bRx9OWU6LheqmRrO53QEAJP7JquKh3kF0jRzmlYOWFZtQwyIWJMEJMtvvDcQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1665,8 +1665,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.1.8: - resolution: {integrity: sha512-lToSu4zUZEQw1nHUsmmPpkrWM8Zk/J7RXL7E7x/Kbk9SZ6rz3VK8knTaJ+Vtdj6RV4XFZS1qp93hgm8z8j6UGw==} + next@15.1.11: + resolution: {integrity: sha512-UiVJaOGhKST58AadwbFUZThlNBmYhKqaCs8bVtm4plTxsgKq0mJ0zTsp7t7j/rzsbAEj9WcAMdZCztjByi4EoQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -2546,30 +2546,30 @@ snapshots: '@kurkle/color@0.3.4': {} - '@next/env@15.1.8': {} + '@next/env@15.1.11': {} - '@next/swc-darwin-arm64@15.1.8': + '@next/swc-darwin-arm64@15.1.9': optional: true - '@next/swc-darwin-x64@15.1.8': + '@next/swc-darwin-x64@15.1.9': optional: true - '@next/swc-linux-arm64-gnu@15.1.8': + '@next/swc-linux-arm64-gnu@15.1.9': optional: true - '@next/swc-linux-arm64-musl@15.1.8': + '@next/swc-linux-arm64-musl@15.1.9': optional: true - '@next/swc-linux-x64-gnu@15.1.8': + '@next/swc-linux-x64-gnu@15.1.9': optional: true - '@next/swc-linux-x64-musl@15.1.8': + '@next/swc-linux-x64-musl@15.1.9': optional: true - '@next/swc-win32-arm64-msvc@15.1.8': + '@next/swc-win32-arm64-msvc@15.1.9': optional: true - '@next/swc-win32-x64-msvc@15.1.8': + '@next/swc-win32-x64-msvc@15.1.9': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4012,9 +4012,9 @@ snapshots: neo-async@2.6.2: {} - next@15.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.1.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.1.8 + '@next/env': 15.1.11 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -4024,14 +4024,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.1.8 - '@next/swc-darwin-x64': 15.1.8 - '@next/swc-linux-arm64-gnu': 15.1.8 - '@next/swc-linux-arm64-musl': 15.1.8 - '@next/swc-linux-x64-gnu': 15.1.8 - '@next/swc-linux-x64-musl': 15.1.8 - '@next/swc-win32-arm64-msvc': 15.1.8 - '@next/swc-win32-x64-msvc': 15.1.8 + '@next/swc-darwin-arm64': 15.1.9 + '@next/swc-darwin-x64': 15.1.9 + '@next/swc-linux-arm64-gnu': 15.1.9 + '@next/swc-linux-arm64-musl': 15.1.9 + '@next/swc-linux-x64-gnu': 15.1.9 + '@next/swc-linux-x64-musl': 15.1.9 + '@next/swc-win32-arm64-msvc': 15.1.9 + '@next/swc-win32-x64-msvc': 15.1.9 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' From c7d99d85b45ed6c8c1a6893523e58255fc834c7c Mon Sep 17 00:00:00 2001 From: topeanut Date: Mon, 22 Dec 2025 20:50:53 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=EC=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/votes.ts | 1 + src/app/poll/[id]/page.tsx | 6 ++- src/components/pages/balanse/balanseList.tsx | 5 ++ src/components/pages/balanse/mockPollCard.tsx | 8 ++- src/components/pages/create/createForm.tsx | 49 ++++++++++++++++--- .../pages/my/_shared/balanseHistoryCard.tsx | 5 ++ src/components/pages/poll/pollCard.tsx | 9 +++- src/types/api/votes.ts | 2 + src/types/balanse/vote.ts | 1 + src/types/my/history.ts | 1 + 10 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/api/votes.ts b/src/api/votes.ts index 7561654..4521c1e 100644 --- a/src/api/votes.ts +++ b/src/api/votes.ts @@ -24,6 +24,7 @@ export interface VoteOption { export interface BestVoteResponse { voteId: number title: string + content: string | null category: VoteCategory totalParticipants: number createdBy: string diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index debf3dc..0361b14 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -27,13 +27,14 @@ interface PollOption { interface PollDetail { voteId: number title: string + content: string | null category: string creatorNickname: string createdAt: string totalVoteCount: number options: PollOption[] hasVoted: boolean - votedOptionLabel: string + votedOptionLabel: string | null } export default function PollDetailPage() { @@ -170,6 +171,7 @@ export default function PollDetailPage() { voteId={data.voteId} createdBy={data.creatorNickname} title={data.title} + content={data.content} options={data.options.map((opt) => ({ optionId: opt.optionId, content: opt.content, @@ -177,7 +179,7 @@ export default function PollDetailPage() { }))} totalParticipants={data.totalVoteCount} hasVoted={data.hasVoted} - votedOptionLabel={data.votedOptionLabel} + votedOptionLabel={data.votedOptionLabel ?? undefined} /> )} {bestComment && !open && ( diff --git a/src/components/pages/balanse/balanseList.tsx b/src/components/pages/balanse/balanseList.tsx index b549cd4..75d47a7 100644 --- a/src/components/pages/balanse/balanseList.tsx +++ b/src/components/pages/balanse/balanseList.tsx @@ -23,6 +23,11 @@ export default function BalanceList({ data }: { data: Vote }) {

{data.title}

+ {data.content && ( +

+ {data.content} +

+ )}
{data.options.map((opt) => (
diff --git a/src/components/pages/balanse/mockPollCard.tsx b/src/components/pages/balanse/mockPollCard.tsx index b4ed55c..f9ae930 100644 --- a/src/components/pages/balanse/mockPollCard.tsx +++ b/src/components/pages/balanse/mockPollCard.tsx @@ -5,6 +5,7 @@ import { MostVotedVoteResponse, } from '@/api/comment/mostVotedVoteApi' import Link from 'next/link' +import InlineLoading from '@/components/_shared/inlineLoading' const categoryMap: Record = { ETC: '기타', @@ -33,7 +34,12 @@ function MockPollCard() { getData() }, []) - if (loading) return
로딩 중...
+ if (loading) + return ( +
+ +
+ ) if (error) return
{error}
if (!data) return null diff --git a/src/components/pages/create/createForm.tsx b/src/components/pages/create/createForm.tsx index 3d4a6c1..94c342b 100644 --- a/src/components/pages/create/createForm.tsx +++ b/src/components/pages/create/createForm.tsx @@ -18,6 +18,7 @@ const CreateForm = () => { const [title, setTitle] = useState('') const [category, setCategory] = useState(null) const [options, setOptions] = useState(['', '']) // A, B + const [content, setContent] = useState('') const isFormValid = () => { if (title.trim() === '') return false @@ -37,6 +38,9 @@ const CreateForm = () => { title, options, category: category as VoteCategory, + ...(content.trim() + ? ({ content: content.trim() } as Pick) + : {}), } const voteId = await createVote(voteData) return voteId @@ -46,8 +50,9 @@ const CreateForm = () => {
{/* 질문 */}
-
- 질문을 작성해주세요 +
+
질문을 작성해주세요
+
*
{
+ {/* 썰(경험담) */} +
+
+
+ 썰(경험담)을 들려주세요 +
+
+ 선택사항 +
+
+
+