From c8b579cffbb6757685d3816233146b15b2a94146 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:21:18 +0900 Subject: [PATCH 01/31] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=94=8C=EB=A1=9C=ED=84=B0=20UI=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/votes.ts | 9 +++++ src/app/poll/[id]/page.tsx | 20 ++++++++++- .../pages/poll/_admin/AdminFloatingButton.tsx | 34 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/components/pages/poll/_admin/AdminFloatingButton.tsx diff --git a/src/api/votes.ts b/src/api/votes.ts index 7561654..9ad58f2 100644 --- a/src/api/votes.ts +++ b/src/api/votes.ts @@ -104,3 +104,12 @@ export const fetchMineVotesVoted = async ( throw error } } + +// 투표 삭제 API +export const deleteVote = async (voteId: number) => { + try { + await authApi.delete(`/votes/${voteId}`) + } catch (error) { + throw error + } +} diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index debf3dc..6dbdd31 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -12,10 +12,11 @@ import { Comment, } from '@/api/comment/commentApi' import VoteChart from '@/components/pages/poll/statistics/statisics' -import { fetchBestVote } from '@/api/votes' +import { deleteVote, fetchBestVote } from '@/api/votes' import Header from '@/components/_shared/header' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Loading from '@/components/_shared/loading' +import AdminFloatingButton from '@/components/pages/poll/_admin/AdminFloatingButton' interface PollOption { optionId: number @@ -156,6 +157,22 @@ export default function PollDetailPage() { ) if (!data) return null + // mock data + const isAdmin = true + + const handleDelete = async () => { + const confirmed = window.confirm('게시글을 삭제하시겠습니까?') + if (!confirmed) return + + try { + await deleteVote(data.voteId) + router.back() + } catch (error) { + console.error('게시글 삭제 실패:', error) + alert('게시글 삭제에 실패했습니다.') + } + } + return (
{(source === 'hot' || isFromHot) && } + {isAdmin && }
) } diff --git a/src/components/pages/poll/_admin/AdminFloatingButton.tsx b/src/components/pages/poll/_admin/AdminFloatingButton.tsx new file mode 100644 index 0000000..8276303 --- /dev/null +++ b/src/components/pages/poll/_admin/AdminFloatingButton.tsx @@ -0,0 +1,34 @@ +'use client' +import { useState } from 'react' + +interface Props { + onDelete: () => void +} + +export default function AdminFloatingButton({ onDelete }: Props) { + const [open, setOpen] = useState(false) + + return ( +
+ {/* 액션 메뉴 */} + {open && ( +
+ +
+ )} + + {/* 플로팅 버튼 */} + +
+ ) +} From ed6ee15057a4a6b52c3b6fc6444c14db9978873e Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:50:56 +0900 Subject: [PATCH 02/31] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 12 +++- .../ui/modal/deleteConfirmModal.tsx | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/modal/deleteConfirmModal.tsx diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index 6dbdd31..798af50 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -17,6 +17,7 @@ import Header from '@/components/_shared/header' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Loading from '@/components/_shared/loading' import AdminFloatingButton from '@/components/pages/poll/_admin/AdminFloatingButton' +import DeleteConfirmModal from '@/components/ui/modal/deleteConfirmModal' interface PollOption { optionId: number @@ -49,6 +50,7 @@ export default function PollDetailPage() { const [showStats, setShowStats] = useState(false) const [isFromHot, setIsFromHot] = useState(false) const router = useRouter() + const [deleteModalOpen, setDeleteModalOpen] = useState(false) // URL 파라미터에서 출처 확인 const source = searchParams.get('source') @@ -166,6 +168,7 @@ export default function PollDetailPage() { try { await deleteVote(data.voteId) + setDeleteModalOpen(false) router.back() } catch (error) { console.error('게시글 삭제 실패:', error) @@ -221,7 +224,14 @@ export default function PollDetailPage() { )} {(source === 'hot' || isFromHot) && } - {isAdmin && } + {isAdmin && ( + setDeleteModalOpen(true)} /> + )} + setDeleteModalOpen(false)} + onConfirm={handleDelete} + /> ) } diff --git a/src/components/ui/modal/deleteConfirmModal.tsx b/src/components/ui/modal/deleteConfirmModal.tsx new file mode 100644 index 0000000..6a1afa4 --- /dev/null +++ b/src/components/ui/modal/deleteConfirmModal.tsx @@ -0,0 +1,55 @@ +'use client' + +import { + Modal, + ModalOverlay, + ModalHeader, + ModalTitle, + ModalDescription, + ModalBody, + ModalFooter, + ModalCloseButton, +} from '@/components/ui/modal' +import { Button } from '@/components/ui/button' + +interface DeleteConfirmModalProps { + open: boolean + onClose: () => void + onConfirm: () => void + title?: string + description?: string +} + +export default function DeleteConfirmModal({ + open, + onClose, + onConfirm, + title = '삭제하시겠습니까?', + description = '이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?', +}: DeleteConfirmModalProps) { + if (!open) return null + + return ( + + + + {title} + + + + + {description} + + + + + + + + + ) +} From a19317cd8f9eec9f9775e4fa2051c0275b34b088 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:01:47 +0900 Subject: [PATCH 03/31] =?UTF-8?q?feat:=20profile=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=9E=AC=EC=A0=95=EC=9D=98=20/=20=EC=95=B1=20=EB=A7=88?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=8B=9C=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95=EB=B3=B4=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/authBootstrap.tsx | 22 ++++++++++++++++++++++ src/app/providers.tsx | 9 ++++++++- src/types/_shared/profile.ts | 3 +++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/app/authBootstrap.tsx diff --git a/src/app/authBootstrap.tsx b/src/app/authBootstrap.tsx new file mode 100644 index 0000000..84db00f --- /dev/null +++ b/src/app/authBootstrap.tsx @@ -0,0 +1,22 @@ +// app/authBootstrap.tsx +'use client' + +import { useEffect } from 'react' +import { useAppDispatch } from '@/hooks/utils/useAppDispatch' +import { fetchProfileThunk } from '@/store/thunks/memberThunks' +import { getAccessToken } from '@/utils/tokenUtils' + +export default function AuthBootstrap() { + const dispatch = useAppDispatch() + + useEffect(() => { + // 새로고침 시 profile 가져오기 + if (getAccessToken()) { + // access token 이 없으면 로그인 페이지로 이동하기 때문에 아무것도 할 필요없음 + // access token 이 있으면 해당 token 으로 profile 조회 시도 + dispatch(fetchProfileThunk()) + } + }, [dispatch]) + + return null +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index f387121..ca19191 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,8 +1,15 @@ 'use client' import { Provider } from 'react-redux' import { store } from '../store/store' +import AuthBootstrap from './authBootstrap' const Providers = ({ children }: { children: React.ReactNode }) => { - return {children} + return ( + + {/* 앱 시작 시 또는 새로고침 시 인증/프로필 복구 로직 */} + + {children} + + ) } export default Providers diff --git a/src/types/_shared/profile.ts b/src/types/_shared/profile.ts index 207cb06..945a90e 100644 --- a/src/types/_shared/profile.ts +++ b/src/types/_shared/profile.ts @@ -5,6 +5,7 @@ export type Profile = { mbtiIe: mbtiIe mbtiTf: mbtiTf mbti: MBTI + role: UserRole } export type Age = 'TEN' | 'TWENTY' | 'THIRTY' | 'OVER_FOURTY' @@ -31,3 +32,5 @@ export type mbtiIe = 'I' | 'E' export type mbtiNs = 'N' | 'S' export type mbtiTf = 'T' | 'F' export type mbtiPj = 'P' | 'J' + +export type UserRole = 'USER' | 'ADMIN' From a3ae98048bdd6cb4fe00693eff3e506f1713d763 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:18:51 +0900 Subject: [PATCH 04/31] =?UTF-8?q?feat:=20mock=20data=20=EB=8C=80=EC=B2=B4?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index 798af50..e31e0e2 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -18,6 +18,7 @@ import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Loading from '@/components/_shared/loading' import AdminFloatingButton from '@/components/pages/poll/_admin/AdminFloatingButton' import DeleteConfirmModal from '@/components/ui/modal/deleteConfirmModal' +import { useAppSelector } from '@/hooks/utils/useAppSelector' interface PollOption { optionId: number @@ -52,6 +53,9 @@ export default function PollDetailPage() { const router = useRouter() const [deleteModalOpen, setDeleteModalOpen] = useState(false) + // 관리자 여부 파악을 위한 profile 조회 + const profile = useAppSelector((state) => state.member.profile) + // URL 파라미터에서 출처 확인 const source = searchParams.get('source') @@ -159,13 +163,10 @@ export default function PollDetailPage() { ) if (!data) return null - // mock data - const isAdmin = true + // 관리자인지 여부 판단 + const isAdmin = profile?.role === 'ADMIN' const handleDelete = async () => { - const confirmed = window.confirm('게시글을 삭제하시겠습니까?') - if (!confirmed) return - try { await deleteVote(data.voteId) setDeleteModalOpen(false) From 7be5fe1698444d7f8be4311f83f142a70e5c42ed Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:52:14 +0900 Subject: [PATCH 05/31] =?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=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/pages/valanse/trendinVoteApi.ts | 2 ++ src/api/votes.ts | 12 ++++++++++- src/components/pages/balanse/mockPollCard.tsx | 21 ++++++++++++++++++- src/types/api/votes.ts | 2 ++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/api/pages/valanse/trendinVoteApi.ts b/src/api/pages/valanse/trendinVoteApi.ts index c31c685..8f24cfc 100644 --- a/src/api/pages/valanse/trendinVoteApi.ts +++ b/src/api/pages/valanse/trendinVoteApi.ts @@ -1,4 +1,5 @@ import { authApi } from '../../instance/authApi' +import { PinType } from '@/types/api/votes' export type TrendingVoteResponse = { voteId: number @@ -8,6 +9,7 @@ export type TrendingVoteResponse = { totalParticipants: number createdBy: string createdAt: string + pinType: PinType options: { optionId: number content: string diff --git a/src/api/votes.ts b/src/api/votes.ts index 9ad58f2..e042e39 100644 --- a/src/api/votes.ts +++ b/src/api/votes.ts @@ -1,4 +1,4 @@ -import { CreateVoteData, MineVotesResponse } from '@/types/api/votes' +import { CreateVoteData, MineVotesResponse, PinType } from '@/types/api/votes' import { authApi } from './instance/authApi' import { VoteCategory } from '@/types/_shared/vote' import { isAxiosError } from 'axios' @@ -113,3 +113,13 @@ export const deleteVote = async (voteId: number) => { throw error } } + +// 투표 고정 API +export const pinVote = async (voteId: number, pinType: PinType) => { + try { + const response = await authApi.patch(`/votes/${voteId}/pin`, { pinType }) + return response.data + } catch (error) { + throw error + } +} diff --git a/src/components/pages/balanse/mockPollCard.tsx b/src/components/pages/balanse/mockPollCard.tsx index fb8ace0..9f5a0cb 100644 --- a/src/components/pages/balanse/mockPollCard.tsx +++ b/src/components/pages/balanse/mockPollCard.tsx @@ -5,6 +5,7 @@ import { type TrendingVoteResponse, } from '@/api/pages/valanse/trendinVoteApi' import Link from 'next/link' +import { pinVote } from '@/api/votes' const categoryMap: Record = { ETC: '기타', @@ -18,6 +19,13 @@ function MockPollCard() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const toggleFixed = async (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + if (!data) return + await pinVote(data.voteId, 'TRENDING') + } + useEffect(() => { const getData = async () => { try { @@ -43,7 +51,18 @@ function MockPollCard() {
{data.createdBy}
-
{data.title}
+
+
{data.title}
+ +
{data.options.map((option, idx) => { diff --git a/src/types/api/votes.ts b/src/types/api/votes.ts index dcb7da6..77864fc 100644 --- a/src/types/api/votes.ts +++ b/src/types/api/votes.ts @@ -14,3 +14,5 @@ export type MineVotesResponse = { createdAt: string options: string[] }[] + +export type PinType = 'HOT' | 'TRENDING' | 'NONE' From 1d859a40f56f66b11cf29f2c415a631f4a71a1a2 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:30:00 +0900 Subject: [PATCH 06/31] =?UTF-8?q?refactor:=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EB=B3=80=EC=88=98=EB=A1=9C=20=EB=B0=9B=EB=8A=94=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/balanse/mockPollCard.tsx | 17 ------ src/components/ui/modal/confirmModal.tsx | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/components/ui/modal/confirmModal.tsx diff --git a/src/components/pages/balanse/mockPollCard.tsx b/src/components/pages/balanse/mockPollCard.tsx index 9f5a0cb..ede673f 100644 --- a/src/components/pages/balanse/mockPollCard.tsx +++ b/src/components/pages/balanse/mockPollCard.tsx @@ -5,7 +5,6 @@ import { type TrendingVoteResponse, } from '@/api/pages/valanse/trendinVoteApi' import Link from 'next/link' -import { pinVote } from '@/api/votes' const categoryMap: Record = { ETC: '기타', @@ -19,13 +18,6 @@ function MockPollCard() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const toggleFixed = async (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - if (!data) return - await pinVote(data.voteId, 'TRENDING') - } - useEffect(() => { const getData = async () => { try { @@ -53,15 +45,6 @@ function MockPollCard() {
{data.title}
-
diff --git a/src/components/ui/modal/confirmModal.tsx b/src/components/ui/modal/confirmModal.tsx new file mode 100644 index 0000000..95da488 --- /dev/null +++ b/src/components/ui/modal/confirmModal.tsx @@ -0,0 +1,55 @@ +'use client' + +import { + Modal, + ModalOverlay, + ModalHeader, + ModalTitle, + ModalDescription, + ModalBody, + ModalFooter, + ModalCloseButton, +} from '@/components/ui/modal' +import { Button } from '@/components/ui/button' + +interface ConfirmModalProps { + open: boolean + onClose: () => void + onConfirm: () => void + title?: string + description?: string +} + +export default function ConfirmModal({ + open, + onClose, + onConfirm, + title = '진행하시겠습니까?', + description = '이 작업은 되돌릴 수 없습니다.', +}: ConfirmModalProps) { + if (!open) return null + + return ( + + + + {title} + + + + + {description} + + + + + + + + + ) +} From e558c5b00d573995355c428584f7c42f2d914521 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:57:12 +0900 Subject: [PATCH 07/31] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=ED=8A=B8=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/pages/valanse/trendinVoteApi.ts | 2 +- src/components/pages/balanse/balansePage.tsx | 12 +++++++++++- src/components/pages/balanse/header.tsx | 6 ------ .../balanse/{ => trending-section}/mockPollCard.tsx | 0 .../pages/balanse/trending-section/pinButton.tsx | 9 +++++++++ .../pages/balanse/trending-section/sectionHeader.tsx | 10 ++++++++++ src/types/api/votes.ts | 2 -- src/types/balanse/vote.ts | 2 ++ 8 files changed, 33 insertions(+), 10 deletions(-) rename src/components/pages/balanse/{ => trending-section}/mockPollCard.tsx (100%) create mode 100644 src/components/pages/balanse/trending-section/pinButton.tsx create mode 100644 src/components/pages/balanse/trending-section/sectionHeader.tsx diff --git a/src/api/pages/valanse/trendinVoteApi.ts b/src/api/pages/valanse/trendinVoteApi.ts index 8f24cfc..f0cf477 100644 --- a/src/api/pages/valanse/trendinVoteApi.ts +++ b/src/api/pages/valanse/trendinVoteApi.ts @@ -1,5 +1,5 @@ import { authApi } from '../../instance/authApi' -import { PinType } from '@/types/api/votes' +import { PinType } from '@/types/balanse/vote' export type TrendingVoteResponse = { voteId: number diff --git a/src/components/pages/balanse/balansePage.tsx b/src/components/pages/balanse/balansePage.tsx index 332c814..5ff3521 100644 --- a/src/components/pages/balanse/balansePage.tsx +++ b/src/components/pages/balanse/balansePage.tsx @@ -1,6 +1,6 @@ 'use client' import Header from './header' -import MockPollCard from './mockPollCard' +import MockPollCard from './trending-section/mockPollCard' import FilterTabs from './filtertabs' import BalanceList from './balanseList' import { fetchVotes } from '../../../api/pages/valanse/balanseListapi' @@ -10,6 +10,8 @@ import { useRouter, useSearchParams } from 'next/navigation' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Loading from '@/components/_shared/loading' import React from 'react' +import { SectionHeader } from './trending-section/sectionHeader' +import { PinButton } from './trending-section/pinButton' const sortOptions = [ { label: '최신순', value: 'latest' }, @@ -138,9 +140,17 @@ function BalancePageContent() { return (
+ + {/* 인기 급상승 토픽 섹션 */}
+
+ + +
+ + {/* 투표 목록 섹션 */}

밸런스 게임

-
- - 인기 급상승 토픽 -
) } diff --git a/src/components/pages/balanse/mockPollCard.tsx b/src/components/pages/balanse/trending-section/mockPollCard.tsx similarity index 100% rename from src/components/pages/balanse/mockPollCard.tsx rename to src/components/pages/balanse/trending-section/mockPollCard.tsx diff --git a/src/components/pages/balanse/trending-section/pinButton.tsx b/src/components/pages/balanse/trending-section/pinButton.tsx new file mode 100644 index 0000000..5cf772b --- /dev/null +++ b/src/components/pages/balanse/trending-section/pinButton.tsx @@ -0,0 +1,9 @@ +import { PinType } from '@/types/balanse/vote' + +type Props = { + pinType: PinType +} + +export const PinButton = ({ pinType }: Props) => { + return +} diff --git a/src/components/pages/balanse/trending-section/sectionHeader.tsx b/src/components/pages/balanse/trending-section/sectionHeader.tsx new file mode 100644 index 0000000..3ff93a7 --- /dev/null +++ b/src/components/pages/balanse/trending-section/sectionHeader.tsx @@ -0,0 +1,10 @@ +import { Flame } from 'lucide-react' + +export const SectionHeader = () => { + return ( +
+ + 인기 급상승 토픽 +
+ ) +} diff --git a/src/types/api/votes.ts b/src/types/api/votes.ts index 77864fc..dcb7da6 100644 --- a/src/types/api/votes.ts +++ b/src/types/api/votes.ts @@ -14,5 +14,3 @@ export type MineVotesResponse = { createdAt: string options: string[] }[] - -export type PinType = 'HOT' | 'TRENDING' | 'NONE' diff --git a/src/types/balanse/vote.ts b/src/types/balanse/vote.ts index 1eb3a56..564cfdd 100644 --- a/src/types/balanse/vote.ts +++ b/src/types/balanse/vote.ts @@ -20,3 +20,5 @@ export interface VoteListResponse { has_next_page: boolean next_cursor: string } + +export type PinType = 'HOT' | 'TRENDING' | 'NONE' From 120f098d03bd4109034155cd50b11b304a54170f Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:25:56 +0900 Subject: [PATCH 08/31] =?UTF-8?q?fix:=20role=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 2 ++ .../{trendinVoteApi.ts => trendingVoteApi.ts} | 0 src/api/votes.ts | 3 +- src/components/pages/balanse/balansePage.tsx | 24 ++++++++++++-- .../balanse/trending-section/mockPollCard.tsx | 31 +++---------------- src/components/pages/my/edit/editPage.tsx | 9 +++++- .../pages/onboarding/onboardingPage.tsx | 1 + tsconfig.json | 1 + 8 files changed, 41 insertions(+), 30 deletions(-) rename src/api/pages/valanse/{trendinVoteApi.ts => trendingVoteApi.ts} (100%) diff --git a/eslint.config.mjs b/eslint.config.mjs index cba43aa..06a5005 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,6 +34,8 @@ export default defineConfig([ 'react/react-in-jsx-scope': 'off', 'react/jsx-pascal-case': 'error', 'no-useless-catch': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', }, }, diff --git a/src/api/pages/valanse/trendinVoteApi.ts b/src/api/pages/valanse/trendingVoteApi.ts similarity index 100% rename from src/api/pages/valanse/trendinVoteApi.ts rename to src/api/pages/valanse/trendingVoteApi.ts diff --git a/src/api/votes.ts b/src/api/votes.ts index e042e39..1d617b7 100644 --- a/src/api/votes.ts +++ b/src/api/votes.ts @@ -1,4 +1,5 @@ -import { CreateVoteData, MineVotesResponse, PinType } from '@/types/api/votes' +import { CreateVoteData, MineVotesResponse } from '@/types/api/votes' +import type { PinType } from '@/types/balanse/vote' import { authApi } from './instance/authApi' import { VoteCategory } from '@/types/_shared/vote' import { isAxiosError } from 'axios' diff --git a/src/components/pages/balanse/balansePage.tsx b/src/components/pages/balanse/balansePage.tsx index 5ff3521..1848696 100644 --- a/src/components/pages/balanse/balansePage.tsx +++ b/src/components/pages/balanse/balansePage.tsx @@ -12,6 +12,8 @@ import Loading from '@/components/_shared/loading' import React from 'react' import { SectionHeader } from './trending-section/sectionHeader' import { PinButton } from './trending-section/pinButton' +import { fetchTrendingVotes } from '@/api/pages/valanse/trendingVoteApi' +import { TrendingVoteResponse } from '@/api/pages/valanse/trendingVoteApi' const sortOptions = [ { label: '최신순', value: 'latest' }, @@ -22,6 +24,7 @@ function BalancePageContent() { const router = useRouter() const searchParams = useSearchParams() const [votes, setVotes] = useState([]) + const [trendingVote, setTrendingVote] = useState() const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [hasNextPage, setHasNextPage] = useState(false) @@ -132,8 +135,25 @@ function BalancePageContent() { getVotes() }, [category, sort]) + useEffect(() => { + const getTrendingVote = async () => { + try { + setLoading(true) + setError(null) + + const data = await fetchTrendingVotes() + setTrendingVote(data) + } catch (_) { + setError('불러오기 실패') + } finally { + setLoading(false) + } + } + getTrendingVote() + }, []) + // 초기 로딩 중일 때는 전체 화면 로딩 - if (loading && votes.length === 0) { + if (loading || votes.length === 0 || !trendingVote) { return } @@ -147,7 +167,7 @@ function BalancePageContent() {
- +
{/* 투표 목록 섹션 */} diff --git a/src/components/pages/balanse/trending-section/mockPollCard.tsx b/src/components/pages/balanse/trending-section/mockPollCard.tsx index ede673f..8284544 100644 --- a/src/components/pages/balanse/trending-section/mockPollCard.tsx +++ b/src/components/pages/balanse/trending-section/mockPollCard.tsx @@ -1,9 +1,5 @@ 'use client' -import { useState, useEffect } from 'react' -import { - fetchTrendingVotes, - type TrendingVoteResponse, -} from '@/api/pages/valanse/trendinVoteApi' +import { type TrendingVoteResponse } from '@/api/pages/valanse/trendingVoteApi' import Link from 'next/link' const categoryMap: Record = { @@ -13,28 +9,11 @@ const categoryMap: Record = { ALL: '전체', } -function MockPollCard() { - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - const getData = async () => { - try { - setLoading(true) - const res = await fetchTrendingVotes() - setData(res) - } catch { - setError('불러오기 실패') - } finally { - setLoading(false) - } - } - getData() - }, []) +interface Props { + data: TrendingVoteResponse +} - if (loading) return
로딩 중...
- if (error) return
{error}
+function MockPollCard({ data }: Props) { if (!data) return null return ( diff --git a/src/components/pages/my/edit/editPage.tsx b/src/components/pages/my/edit/editPage.tsx index 05cb2be..e64dc85 100644 --- a/src/components/pages/my/edit/editPage.tsx +++ b/src/components/pages/my/edit/editPage.tsx @@ -4,7 +4,14 @@ import { useState, useEffect } from 'react' import MBTIBottomSheet from '@/components/pages/onboarding/mbtiBottomSheet' -import { MBTI, mbtiIe, mbtiTf, Age, Gender } from '@/types/_shared/profile' +import { + MBTI, + mbtiIe, + mbtiTf, + Age, + Gender, + UserRole, +} from '@/types/_shared/profile' import { Profile } from '@/types/_shared/profile' import { useRouter } from 'next/navigation' import Image from 'next/image' diff --git a/src/components/pages/onboarding/onboardingPage.tsx b/src/components/pages/onboarding/onboardingPage.tsx index 7f7fac3..1872de8 100644 --- a/src/components/pages/onboarding/onboardingPage.tsx +++ b/src/components/pages/onboarding/onboardingPage.tsx @@ -47,6 +47,7 @@ const OnboardingPage = () => { mbtiIe: mbtiIe, mbtiTf: mbtiTf, mbti: mbti, + role: 'USER', } } diff --git a/tsconfig.json b/tsconfig.json index c133409..0f0ae73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "noUnusedLocals": false, "plugins": [ { "name": "next" From 1980db2ecbefd996bb8180c4c60e53f8020bd26e Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:39:19 +0900 Subject: [PATCH 09/31] =?UTF-8?q?refactor:=20member=20=EC=AA=BD=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/{ => member}/member.ts | 41 +++++++++----- src/api/member/types.ts | 55 +++++++++++++++++++ src/components/pages/my/edit/editPage.tsx | 2 +- .../pages/onboarding/onboardingPage.tsx | 2 +- src/store/slices/memberSlice.ts | 2 +- src/store/thunks/memberThunks.ts | 2 +- src/types/member/index.ts | 38 +++++++++++++ 7 files changed, 124 insertions(+), 18 deletions(-) rename src/api/{ => member}/member.ts (63%) create mode 100644 src/api/member/types.ts create mode 100644 src/types/member/index.ts diff --git a/src/api/member.ts b/src/api/member/member.ts similarity index 63% rename from src/api/member.ts rename to src/api/member/member.ts index 6b171ff..756db2a 100644 --- a/src/api/member.ts +++ b/src/api/member/member.ts @@ -1,7 +1,15 @@ -import { authApi } from './instance/authApi' +import { authApi } from '../instance/authApi' import { Profile } from '@/types/_shared/profile' +import { + CreateMemberProfileRequest, + FetchMemberProfileResponse, + UpdateMemberProfileRequest, +} from './types' +import { Update } from '@reduxjs/toolkit' -export const createMemberProfile = async (profile: Profile) => { +export const createMemberProfile = async ( + profile: CreateMemberProfileRequest, +) => { try { await authApi.post('/member/profile', profile) } catch (error) { @@ -9,17 +17,29 @@ export const createMemberProfile = async (profile: Profile) => { } } -export const fetchMemberProfile = async (): Promise => { +export const fetchMemberProfile = async (): Promise< + FetchMemberProfileResponse['profile'] +> => { try { - const response = await authApi.get<{ profile: Profile | null }>( - '/member/profile', - ) + const response = + await authApi.get('/member/profile') return response.data.profile } catch (error) { throw error } } +export const updateMemberProfile = async ( + profile: UpdateMemberProfileRequest, +) => { + try { + await authApi.post('/member/profile', profile) + } catch (error) { + throw error + } +} + +// TODO : 마이페이지 타입 정리 export type fetchMemberMypageResponse = { profile: { profile_image_url: string @@ -42,14 +62,7 @@ export const fetchMemberMypage = async () => { } } -export const updateMemberProfile = async (profile: Profile) => { - try { - await authApi.post('/member/profile', profile) - } catch (error) { - throw error - } -} - +// TODO : 닉네임 체크 응답 타입 정리 export const checkNickname = async (nickname: string) => { try { const response = await authApi.get( diff --git a/src/api/member/types.ts b/src/api/member/types.ts new file mode 100644 index 0000000..3f12eca --- /dev/null +++ b/src/api/member/types.ts @@ -0,0 +1,55 @@ +export type CreateMemberProfileRequest = { + nickname: string + gender: 'FEMALE' | 'MALE' + age: 'TEN' | 'TWENTY' | 'THIRTY' | 'OVER_FOURTY' + mbtiIe: 'I' | 'E' + mbtiTf: 'T' | 'F' + mbti: + | 'ISTJ' + | 'ISTP' + | 'ISFJ' + | 'ISFP' + | 'INTJ' + | 'INTP' + | 'INFJ' + | 'INFP' + | 'ESTJ' + | 'ESTP' + | 'ESFJ' + | 'ESFP' + | 'ENTJ' + | 'ENTP' + | 'ENFJ' + | 'ENFP' + role: 'USER' | 'ADMIN' +} + +export type FetchMemberProfileResponse = { + profile: { + nickname: string + gender: 'FEMALE' | 'MALE' + age: 'TEN' | 'TWENTY' | 'THIRTY' | 'OVER_FOURTY' + mbtiIe: 'I' | 'E' + mbtiTf: 'T' | 'F' + mbti: + | 'ISTJ' + | 'ISFJ' + | 'INFJ' + | 'INTJ' + | 'ISTP' + | 'ISFP' + | 'INFP' + | 'INTP' + | 'ESTP' + | 'ESFP' + | 'ENFP' + | 'ENTP' + | 'ESTJ' + | 'ESFJ' + | 'ENFJ' + | 'ENTJ' + role: 'USER' | 'ADMIN' + } +} + +export type UpdateMemberProfileRequest = CreateMemberProfileRequest diff --git a/src/components/pages/my/edit/editPage.tsx b/src/components/pages/my/edit/editPage.tsx index e64dc85..cda9949 100644 --- a/src/components/pages/my/edit/editPage.tsx +++ b/src/components/pages/my/edit/editPage.tsx @@ -16,7 +16,7 @@ import { Profile } from '@/types/_shared/profile' import { useRouter } from 'next/navigation' import Image from 'next/image' import { useAppSelector } from '@/hooks/utils/useAppSelector' -import { checkNickname } from '@/api/member' +import { checkNickname } from '@/api/member/member' import { fetchMypageDataThunk, updateProfileThunk, diff --git a/src/components/pages/onboarding/onboardingPage.tsx b/src/components/pages/onboarding/onboardingPage.tsx index 1872de8..6fff7d8 100644 --- a/src/components/pages/onboarding/onboardingPage.tsx +++ b/src/components/pages/onboarding/onboardingPage.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import MBTIBottomSheet from './mbtiBottomSheet' -import { createMemberProfile } from '@/api/member' +import { createMemberProfile } from '@/api/member/member' import { useRouter } from 'next/navigation' import { MBTI, mbtiIe, mbtiTf, Age, Gender } from '@/types/_shared/profile' import { Profile } from '@/types/_shared/profile' diff --git a/src/store/slices/memberSlice.ts b/src/store/slices/memberSlice.ts index 113349c..6ef59df 100644 --- a/src/store/slices/memberSlice.ts +++ b/src/store/slices/memberSlice.ts @@ -16,7 +16,7 @@ const memberSlice = createSlice({ name: 'member', initialState, reducers: { - setProfile(state, action: PayloadAction) { + setProfile(state, action: PayloadAction) { state.profile = action.payload }, setMypageData(state, action: PayloadAction) { diff --git a/src/store/thunks/memberThunks.ts b/src/store/thunks/memberThunks.ts index 2eaf8e0..a5c0f06 100644 --- a/src/store/thunks/memberThunks.ts +++ b/src/store/thunks/memberThunks.ts @@ -2,7 +2,7 @@ import { fetchMemberMypage, fetchMemberProfile, updateMemberProfile, -} from '@/api/member' +} from '@/api/member/member' import { setProfile, setMypageData } from '../slices/memberSlice' import { AppDispatch } from '../store' import { Profile } from '@/types/_shared/profile' diff --git a/src/types/member/index.ts b/src/types/member/index.ts new file mode 100644 index 0000000..e7c16a5 --- /dev/null +++ b/src/types/member/index.ts @@ -0,0 +1,38 @@ +export type Profile = { + nickname: string + gender: Gender + age: Age + mbtiIe: mbtiIe + mbtiTf: mbtiTf + mbti: MBTI + role: UserRole +} + +export type Gender = 'FEMALE' | 'MALE' + +export type Age = 'TEN' | 'TWENTY' | 'THIRTY' | 'OVER_FOURTY' + +export type mbtiIe = 'I' | 'E' +export type mbtiNs = 'N' | 'S' +export type mbtiTf = 'T' | 'F' +export type mbtiPj = 'P' | 'J' + +export type MBTI = + | 'ISTJ' + | 'ISTP' + | 'ISFJ' + | 'ISFP' + | 'INTJ' + | 'INTP' + | 'INFJ' + | 'INFP' + | 'ESTJ' + | 'ESTP' + | 'ESFJ' + | 'ESFP' + | 'ENTJ' + | 'ENTP' + | 'ENFJ' + | 'ENFP' + +export type UserRole = 'USER' | 'ADMIN' From a1c511762b55a176c9ac16be92ebd9c255c472d6 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:51:46 +0900 Subject: [PATCH 10/31] =?UTF-8?q?refactor:=20edit=20page=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=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 --- src/api/member/member.ts | 2 - src/components/pages/my/edit/editPage.tsx | 46 +++++++++++------------ 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/api/member/member.ts b/src/api/member/member.ts index 756db2a..28a8dbb 100644 --- a/src/api/member/member.ts +++ b/src/api/member/member.ts @@ -1,11 +1,9 @@ import { authApi } from '../instance/authApi' -import { Profile } from '@/types/_shared/profile' import { CreateMemberProfileRequest, FetchMemberProfileResponse, UpdateMemberProfileRequest, } from './types' -import { Update } from '@reduxjs/toolkit' export const createMemberProfile = async ( profile: CreateMemberProfileRequest, diff --git a/src/components/pages/my/edit/editPage.tsx b/src/components/pages/my/edit/editPage.tsx index cda9949..50d4686 100644 --- a/src/components/pages/my/edit/editPage.tsx +++ b/src/components/pages/my/edit/editPage.tsx @@ -4,15 +4,7 @@ import { useState, useEffect } from 'react' import MBTIBottomSheet from '@/components/pages/onboarding/mbtiBottomSheet' -import { - MBTI, - mbtiIe, - mbtiTf, - Age, - Gender, - UserRole, -} from '@/types/_shared/profile' -import { Profile } from '@/types/_shared/profile' +import { Profile, MBTI, mbtiIe, mbtiTf, Age, Gender } from '@/types/member' import { useRouter } from 'next/navigation' import Image from 'next/image' import { useAppSelector } from '@/hooks/utils/useAppSelector' @@ -56,19 +48,26 @@ const genderMap = (label: string) => { const EditPage = () => { const router = useRouter() + const dispatch = useAppDispatch() const myPageData = useAppSelector((state) => state.member.mypageData) - const [nickname, setNickname] = useState(myPageData?.nickname) - const debouncedNickname = useDebounce(nickname || '', 500) + + if (!myPageData) { + return + } + + // 로컬 상태 관리 + const [nickname, setNickname] = useState(myPageData.nickname) + const [gender, setGender] = useState( + myPageData.gender as Gender, + ) + const [age, setAge] = useState(myPageData.age as Age) + const [mbti, setMbti] = useState(myPageData.mbti as MBTI) + const [isDirty, setIsDirty] = useState(false) - const [nickNameMessage, setNickNameMessage] = useState(null) const [isNicknameEditing, setIsNicknameEditing] = useState(false) - const [gender, setGender] = useState( - myPageData?.gender as string, - ) - const [age, setAge] = useState(myPageData?.age as string) + const [nickNameMessage, setNickNameMessage] = useState(null) const [mbtiBottomSheetOpen, setMbtiBottomSheetOpen] = useState(false) - const [mbti, setMbti] = useState(myPageData?.mbti as MBTI) - const dispatch = useAppDispatch() + const debouncedNickname = useDebounce(nickname || '', 500) useEffect(() => { if (debouncedNickname && debouncedNickname.length > 0) { @@ -97,8 +96,8 @@ const EditPage = () => { console.log('로컬 상태 nickname', nickname) if (myPageData) { setNickname(myPageData.nickname) - setGender(myPageData.gender) - setAge(myPageData.age) + setGender(myPageData.gender as Gender) + setAge(myPageData.age as Age) setMbti(myPageData.mbti as MBTI) } else { dispatch(fetchMypageDataThunk()) @@ -122,6 +121,7 @@ const EditPage = () => { mbtiIe: mbtiIe, mbtiTf: mbtiTf, mbti: mbti, + role: 'USER', } } @@ -173,7 +173,7 @@ const EditPage = () => { <> setNickname(e.target.value)} className="text-md text-[#1D1D1D] flex-1" /> @@ -218,7 +218,7 @@ const EditPage = () => { {genderOptions.map((option) => ( + if (!pinType) return null + + const isPinned = pinType === 'TRENDING' + + return ( + + ) } From e823b593c9542c7d6e369e96af517b114111a0c0 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:37:44 +0900 Subject: [PATCH 13/31] =?UTF-8?q?feat:=20=EA=B3=A0=EC=A0=95=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20UI=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../balanseList.tsx | 14 ++- .../balanse/balanse-list-section/pinMenu.tsx | 93 +++++++++++++++++++ src/components/pages/balanse/balansePage.tsx | 2 +- 3 files changed, 103 insertions(+), 6 deletions(-) rename src/components/pages/balanse/{ => balanse-list-section}/balanseList.tsx (79%) create mode 100644 src/components/pages/balanse/balanse-list-section/pinMenu.tsx diff --git a/src/components/pages/balanse/balanseList.tsx b/src/components/pages/balanse/balanse-list-section/balanseList.tsx similarity index 79% rename from src/components/pages/balanse/balanseList.tsx rename to src/components/pages/balanse/balanse-list-section/balanseList.tsx index b549cd4..89f2d23 100644 --- a/src/components/pages/balanse/balanseList.tsx +++ b/src/components/pages/balanse/balanse-list-section/balanseList.tsx @@ -2,6 +2,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/card' import { UserCircle, Share2, MessageCircle } from 'lucide-react' import { Vote } from '@/types/balanse/vote' import Link from 'next/link' +import { PinMenu } from './pinMenu' const categoryMap: Record = { FOOD: '음식', @@ -15,11 +16,14 @@ export default function BalanceList({ data }: { data: Vote }) { - - - - {data.nickname} • {data.created_at} - + +
+ + + {data.nickname} • {data.created_at} + +
+

{data.title}

diff --git a/src/components/pages/balanse/balanse-list-section/pinMenu.tsx b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx new file mode 100644 index 0000000..69c9349 --- /dev/null +++ b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx @@ -0,0 +1,93 @@ +import { useState, useEffect, useRef } from 'react' +import { MoreVertical, Flame, TrendingUp, PinOff } from 'lucide-react' + +type PinType = 'HOT' | 'RISING' | 'NONE' + +type Props = { + onPinChange?: (type: PinType) => void +} + +export const PinMenu = ({ onPinChange }: Props) => { + const [isOpen, setIsOpen] = useState(false) + const menuRef = useRef(null) + + // 메뉴 바깥 클릭 시 닫기 로직 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside) + } + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [isOpen]) + + // 메뉴 열기/닫기 토글 (이벤트 전파 방지 필수) + const toggleMenu = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setIsOpen(!isOpen) + } + + const handleItemClick = (e: React.MouseEvent, type: PinType) => { + e.preventDefault() + e.stopPropagation() + + if (onPinChange) onPinChange(type) + setIsOpen(false) // 선택 후 닫기 + } + + return ( +
+ {/* 트리거 버튼 */} + + + {/* 드롭다운 메뉴 본체 */} + {isOpen && ( +
    e.stopPropagation()} // 메뉴 내부 클릭 시 부모 Link 작동 방지 + > +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ )} +
+ ) +} diff --git a/src/components/pages/balanse/balansePage.tsx b/src/components/pages/balanse/balansePage.tsx index 0ca80cc..493ebf9 100644 --- a/src/components/pages/balanse/balansePage.tsx +++ b/src/components/pages/balanse/balansePage.tsx @@ -2,7 +2,7 @@ import Header from './header' import MockPollCard from './trending-section/mockPollCard' import FilterTabs from './filtertabs' -import BalanceList from './balanseList' +import BalanceList from './balanse-list-section/balanseList' import { fetchVotes } from '../../../api/pages/valanse/balanseListapi' import { useEffect, useState, Suspense, useCallback, useRef } from 'react' import { Vote } from '@/types/balanse/vote' From c5b9e3052062b0e495b7803a7fb1c733119d558d Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:56:58 +0900 Subject: [PATCH 14/31] =?UTF-8?q?feat:=20=EA=B3=A0=EC=A0=95=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=B3=80=EA=B2=BD=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../balanse-list-section/balanseList.tsx | 2 +- .../balanse/balanse-list-section/pinMenu.tsx | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/pages/balanse/balanse-list-section/balanseList.tsx b/src/components/pages/balanse/balanse-list-section/balanseList.tsx index 89f2d23..3286eea 100644 --- a/src/components/pages/balanse/balanse-list-section/balanseList.tsx +++ b/src/components/pages/balanse/balanse-list-section/balanseList.tsx @@ -23,7 +23,7 @@ export default function BalanceList({ data }: { data: Vote }) { {data.nickname} • {data.created_at} - +

{data.title}

diff --git a/src/components/pages/balanse/balanse-list-section/pinMenu.tsx b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx index 69c9349..1f3097d 100644 --- a/src/components/pages/balanse/balanse-list-section/pinMenu.tsx +++ b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx @@ -1,13 +1,14 @@ import { useState, useEffect, useRef } from 'react' import { MoreVertical, Flame, TrendingUp, PinOff } from 'lucide-react' - -type PinType = 'HOT' | 'RISING' | 'NONE' +import { pinVote } from '@/api/votes' +import { PinType } from '@/types/balanse/vote' type Props = { onPinChange?: (type: PinType) => void + voteId: number } -export const PinMenu = ({ onPinChange }: Props) => { +export const PinMenu = ({ onPinChange, voteId }: Props) => { const [isOpen, setIsOpen] = useState(false) const menuRef = useRef(null) @@ -34,10 +35,16 @@ export const PinMenu = ({ onPinChange }: Props) => { setIsOpen(!isOpen) } - const handleItemClick = (e: React.MouseEvent, type: PinType) => { + const handleItemClick = async (e: React.MouseEvent, type: PinType) => { e.preventDefault() e.stopPropagation() + try { + await pinVote(voteId, type) + } catch (error) { + console.error('Failed to pin vote:', error) + } + if (onPinChange) onPinChange(type) setIsOpen(false) // 선택 후 닫기 } @@ -70,7 +77,7 @@ export const PinMenu = ({ onPinChange }: Props) => {
  • + ) +} diff --git a/src/components/pages/poll/sectionHeader.tsx b/src/components/pages/poll/sectionHeader.tsx new file mode 100644 index 0000000..d7eab38 --- /dev/null +++ b/src/components/pages/poll/sectionHeader.tsx @@ -0,0 +1,9 @@ +import { PinButton } from './pinButton' + +export const SectionHeader = () => { + return ( +
    + +
    + ) +} From 06311e5311506038c6cf2998a4dfa2e3da1e666a Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:55:12 +0900 Subject: [PATCH 18/31] =?UTF-8?q?feat:=20=ED=95=AB=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=ED=83=80=EC=9E=85=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/votes.ts | 2 ++ src/app/poll/[id]/page.tsx | 8 ++++++-- src/components/pages/poll/sectionHeader.tsx | 9 +++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/api/votes.ts b/src/api/votes.ts index 1d617b7..ef2da7c 100644 --- a/src/api/votes.ts +++ b/src/api/votes.ts @@ -30,6 +30,8 @@ export interface BestVoteResponse { createdBy: string createdAt: string options: VoteOption[] + content: string + pinType: PinType } // 투표 API 응답 타입 diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index 1e546c5..bdd9100 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -20,6 +20,7 @@ import AdminFloatingButton from '@/components/pages/poll/_admin/AdminFloatingBut import DeleteConfirmModal from '@/components/ui/modal/deleteConfirmModal' import { useAppSelector } from '@/hooks/utils/useAppSelector' import { SectionHeader } from '@/components/pages/poll/sectionHeader' +import { PinType } from '@/types/balanse/vote' interface PollOption { optionId: number @@ -59,6 +60,7 @@ export default function PollDetailPage() { // URL 파라미터에서 출처 확인 const source = searchParams.get('source') + const pin = searchParams.get('pin') as PinType useEffect(() => { if (id === 'hot') { @@ -70,7 +72,9 @@ export default function PollDetailPage() { // fetchBestVote 호출 결과로 불러올 데이터가 없을 경우 404 에러 발생 // -> 404 발생 여부를 반환값이 null 인지 여부로 판정해서 임시로 렌더링 취소하도록 조치함 // 이후 세부 기획이 변경되면 이 부분에서 끌어올린 데이터를 기반으로 렌더링하는 로직을 구현하면 됨 - router.replace(`/poll/${response.voteId}?source=hot`) + router.replace( + `/poll/${response.voteId}?source=hot&pin=${response.pinType}`, + ) } catch (error) { console.error('Failed to fetch best vote:', error) router.replace('/main') @@ -189,7 +193,7 @@ export default function PollDetailPage() { />
    {/* 관리자 계정이면 섹션 헤더에 고정 버튼 표시 */} - {isAdmin && } + {isAdmin && } {data && ( { +type Props = { + pinType: PinType +} + +export const SectionHeader = ({ pinType }: Props) => { return (
    - +
    ) } From d53a464151054fb46952ea0494185803a9a55813 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:30:12 +0900 Subject: [PATCH 19/31] =?UTF-8?q?feat:=20=ED=95=AB=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B3=A0=EC=A0=95=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 33 +++++++++++++++++++-- src/components/pages/poll/sectionHeader.tsx | 5 ++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index bdd9100..74b5128 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -12,7 +12,7 @@ import { Comment, } from '@/api/comment/commentApi' import VoteChart from '@/components/pages/poll/statistics/statisics' -import { deleteVote, fetchBestVote } from '@/api/votes' +import { deleteVote, fetchBestVote, pinVote } from '@/api/votes' import Header from '@/components/_shared/header' import BottomNavBar from '@/components/_shared/nav/bottomNavBar' import Loading from '@/components/_shared/loading' @@ -21,6 +21,7 @@ import DeleteConfirmModal from '@/components/ui/modal/deleteConfirmModal' import { useAppSelector } from '@/hooks/utils/useAppSelector' import { SectionHeader } from '@/components/pages/poll/sectionHeader' import { PinType } from '@/types/balanse/vote' +import ConfirmModal from '@/components/ui/modal/confirmModal' interface PollOption { optionId: number @@ -54,6 +55,7 @@ export default function PollDetailPage() { const [isFromHot, setIsFromHot] = useState(false) const router = useRouter() const [deleteModalOpen, setDeleteModalOpen] = useState(false) + const [showConfirmModal, setShowConfirmModal] = useState(false) // 관리자 여부 파악을 위한 profile 조회 const profile = useAppSelector((state) => state.member.profile) @@ -147,6 +149,16 @@ export default function PollDetailPage() { } } + const handlePinButtonClick = () => { + setShowConfirmModal(true) + } + + // 고정 해제 + const handleUnpin = async () => { + await pinVote(Number(id), 'NONE') + router.replace(`/poll/${id}?source=hot&pin=${'NONE'}`) + } + if (loading) return if (error) return ( @@ -193,7 +205,24 @@ export default function PollDetailPage() { />
    {/* 관리자 계정이면 섹션 헤더에 고정 버튼 표시 */} - {isAdmin && } + {isAdmin && ( + + )} + + {/* 고정 해제 확인 모달 */} + setShowConfirmModal(false)} + onConfirm={() => { + handleUnpin() + setShowConfirmModal(false) + }} + /> {data && ( void } -export const SectionHeader = ({ pinType }: Props) => { +export const SectionHeader = ({ pinType, handlePinButtonClick }: Props) => { return (
    - +
    ) } From 0441e4bad4d7bf274f89b1d56d8b2498ce73e0d6 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:47:52 +0900 Subject: [PATCH 20/31] =?UTF-8?q?feat:=20=ED=95=80=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/letter-x-circle.svg | 4 ++++ .../pages/balanse/trending-section/pinButton.tsx | 13 ++++++++----- src/components/pages/poll/pinButton.tsx | 13 ++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 public/letter-x-circle.svg diff --git a/public/letter-x-circle.svg b/public/letter-x-circle.svg new file mode 100644 index 0000000..2edd993 --- /dev/null +++ b/public/letter-x-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/pages/balanse/trending-section/pinButton.tsx b/src/components/pages/balanse/trending-section/pinButton.tsx index ef574a6..0275108 100644 --- a/src/components/pages/balanse/trending-section/pinButton.tsx +++ b/src/components/pages/balanse/trending-section/pinButton.tsx @@ -18,14 +18,17 @@ export const PinButton = ({ pinType, onClick }: Props) => { return ( ) diff --git a/src/components/pages/poll/pinButton.tsx b/src/components/pages/poll/pinButton.tsx index e78665b..06fd57d 100644 --- a/src/components/pages/poll/pinButton.tsx +++ b/src/components/pages/poll/pinButton.tsx @@ -18,14 +18,17 @@ export const PinButton = ({ pinType, onClick }: Props) => { return ( ) From 52edce59778598b2d1a66048b75e9a6c68d5e0a1 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:56:30 +0900 Subject: [PATCH 21/31] =?UTF-8?q?fix:=20mbti=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/member/types.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/member/types.ts b/src/api/member/types.ts index 3f12eca..6f1a49c 100644 --- a/src/api/member/types.ts +++ b/src/api/member/types.ts @@ -33,21 +33,21 @@ export type FetchMemberProfileResponse = { mbtiTf: 'T' | 'F' mbti: | 'ISTJ' - | 'ISFJ' - | 'INFJ' - | 'INTJ' | 'ISTP' + | 'ISFJ' | 'ISFP' - | 'INFP' + | 'INTJ' | 'INTP' + | 'INFJ' + | 'INFP' + | 'ESTJ' | 'ESTP' + | 'ESFJ' | 'ESFP' - | 'ENFP' + | 'ENTJ' | 'ENTP' - | 'ESTJ' - | 'ESFJ' | 'ENFJ' - | 'ENTJ' + | 'ENFP' role: 'USER' | 'ADMIN' } } From 5cc791b18bc8f878b26f65d1c6f15b80d1ded94a Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:17:13 +0900 Subject: [PATCH 22/31] =?UTF-8?q?fix:=20=EA=B3=A0=EC=A0=95=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20api=20=ED=98=B8=EC=B6=9C=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index 74b5128..c6b196e 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -155,8 +155,13 @@ export default function PollDetailPage() { // 고정 해제 const handleUnpin = async () => { - await pinVote(Number(id), 'NONE') - router.replace(`/poll/${id}?source=hot&pin=${'NONE'}`) + try { + await pinVote(Number(id), 'NONE') + router.replace(`/poll/${id}?source=hot&pin=$NONE`) + } catch (error) { + console.error('Failed to unpin vote:', error) + alert('고정 해제에 실패했습니다.') + } } if (loading) return From e255407f556f974767bd4f15b842e58c2035003c Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:19:29 +0900 Subject: [PATCH 23/31] =?UTF-8?q?feat:=20react=20hooks=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/my/edit/editPage.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/pages/my/edit/editPage.tsx b/src/components/pages/my/edit/editPage.tsx index 50d4686..4b2b2ee 100644 --- a/src/components/pages/my/edit/editPage.tsx +++ b/src/components/pages/my/edit/editPage.tsx @@ -51,17 +51,15 @@ const EditPage = () => { const dispatch = useAppDispatch() const myPageData = useAppSelector((state) => state.member.mypageData) - if (!myPageData) { - return - } - // 로컬 상태 관리 - const [nickname, setNickname] = useState(myPageData.nickname) + const [nickname, setNickname] = useState( + myPageData?.nickname || '', + ) const [gender, setGender] = useState( - myPageData.gender as Gender, + myPageData?.gender as Gender, ) - const [age, setAge] = useState(myPageData.age as Age) - const [mbti, setMbti] = useState(myPageData.mbti as MBTI) + const [age, setAge] = useState(myPageData?.age as Age) + const [mbti, setMbti] = useState(myPageData?.mbti as MBTI) const [isDirty, setIsDirty] = useState(false) const [isNicknameEditing, setIsNicknameEditing] = useState(false) From 4ad7d75961faef3d781c77660d3d908ee25c4c35 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:23:20 +0900 Subject: [PATCH 24/31] =?UTF-8?q?feat:=20=EA=B3=A0=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/balanse/balanse-list-section/pinMenu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/balanse/balanse-list-section/pinMenu.tsx b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx index 7abf690..063ec3b 100644 --- a/src/components/pages/balanse/balanse-list-section/pinMenu.tsx +++ b/src/components/pages/balanse/balanse-list-section/pinMenu.tsx @@ -41,11 +41,12 @@ export const PinMenu = ({ onPinChange, voteId }: Props) => { try { await pinVote(voteId, type) + if (onPinChange) onPinChange() } catch (error) { console.error('Failed to pin vote:', error) + return } - if (onPinChange) onPinChange() setIsOpen(false) // 선택 후 닫기 } From f85f4e7caa21c727fb6b9326b4b2fe26ecacf4be Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:24:33 +0900 Subject: [PATCH 25/31] =?UTF-8?q?fix:=20early=20return=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/balanse/balansePage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/pages/balanse/balansePage.tsx b/src/components/pages/balanse/balansePage.tsx index 570d40d..1cdecd8 100644 --- a/src/components/pages/balanse/balansePage.tsx +++ b/src/components/pages/balanse/balansePage.tsx @@ -40,8 +40,7 @@ function BalancePageContent() { // 관리자 여부 판단 const profile = useAppSelector((state) => state.member.profile) - if (!profile) return - const isAdmin = profile.role === 'ADMIN' + const isAdmin = profile?.role === 'ADMIN' // URL에서 카테고리와 정렬 옵션 가져오기 const category = searchParams.get('category') || 'ALL' @@ -169,6 +168,8 @@ function BalancePageContent() { return } + if (!profile) return + // 고정 해제 const handleUnpin = async () => { await pinVote(trendingVote.voteId, 'NONE') From a904a5f8f86740e3397d0a8b336a581a678cb682 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:55:06 +0900 Subject: [PATCH 26/31] =?UTF-8?q?fix:=20=ED=95=AB=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=98=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index eab9b0a..65772a8 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -158,7 +158,7 @@ export default function PollDetailPage() { const handleUnpin = async () => { try { await pinVote(Number(id), 'NONE') - router.replace(`/poll/${id}?source=hot&pin=$NONE`) + router.replace('/poll/hot') } catch (error) { console.error('Failed to unpin vote:', error) alert('고정 해제에 실패했습니다.') From f878190d2e2273af8384c21d0cf2965ddbf43dbd Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:15:49 +0900 Subject: [PATCH 27/31] =?UTF-8?q?chore:=20=ED=83=88=ED=87=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=8F=99=EC=9E=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 8 ++++++++ src/components/pages/my/accountControlSection.tsx | 14 ++++++++++---- src/store/thunks/authThunks.ts | 12 +++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index df62358..b85185f 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -33,3 +33,11 @@ export const logout = async () => { throw error } } + +export const signout = async () => { + try { + await authApi.post('/auth/withdraw') + } catch (error) { + throw error + } +} diff --git a/src/components/pages/my/accountControlSection.tsx b/src/components/pages/my/accountControlSection.tsx index 689772f..fd21f9f 100644 --- a/src/components/pages/my/accountControlSection.tsx +++ b/src/components/pages/my/accountControlSection.tsx @@ -1,5 +1,5 @@ import { useAppDispatch } from '@/hooks/utils/useAppDispatch' -import { logoutThunk } from '@/store/thunks/authThunks' +import { logoutThunk, signoutThunk } from '@/store/thunks/authThunks' export default function AccountControlSection() { const dispatch = useAppDispatch() @@ -8,14 +8,20 @@ export default function AccountControlSection() { dispatch(logoutThunk()) } + const handleSignout = () => { + dispatch(signoutThunk()) + } + return (
    계정 관리
    -
    +
    -
    탈퇴하기
    + +
    ) diff --git a/src/store/thunks/authThunks.ts b/src/store/thunks/authThunks.ts index e1f0bbd..9460ae2 100644 --- a/src/store/thunks/authThunks.ts +++ b/src/store/thunks/authThunks.ts @@ -9,7 +9,7 @@ import { AppDispatch } from '../store' import { getRefreshToken } from '@/utils/tokenUtils' import { saveTokens, clearTokens } from '@/utils/tokenUtils' import { reissue } from '@/api/auth' -import { logout as logoutApi } from '@/api/auth' +import { logout as logoutApi, signout as signoutApi } from '@/api/auth' import { login } from '@/api/auth' export const loginThunk = (code: string) => async (dispatch: AppDispatch) => { @@ -53,3 +53,13 @@ export const logoutThunk = () => async (dispatch: AppDispatch) => { throw err } } + +export const signoutThunk = () => async (dispatch: AppDispatch) => { + try { + await signoutApi() + dispatch(logout()) + clearTokens() + } catch (err) { + throw err + } +} From ec992e9c9a0526ec57c01541c139c39790567ea6 Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:31:03 +0900 Subject: [PATCH 28/31] =?UTF-8?q?chore:=20=EA=B6=8C=ED=95=9C=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EB=8C=93=EA=B8=80=EC=97=90=EB=8A=94=20?= =?UTF-8?q?=EC=A1=B0=EC=9E=91=20UI=20=EB=85=B8=EC=B6=9C=20=EC=95=88=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/poll/[id]/page.tsx | 1 + .../pages/poll/Comment/commentDetail.tsx | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/poll/[id]/page.tsx b/src/app/poll/[id]/page.tsx index 65772a8..9041f57 100644 --- a/src/app/poll/[id]/page.tsx +++ b/src/app/poll/[id]/page.tsx @@ -259,6 +259,7 @@ export default function PollDetailPage() { comments={comments} voteId={data.voteId} onClose={() => setOpen(false)} + profile={profile} /> )} {data && ( diff --git a/src/components/pages/poll/Comment/commentDetail.tsx b/src/components/pages/poll/Comment/commentDetail.tsx index af223bf..f8e2979 100644 --- a/src/components/pages/poll/Comment/commentDetail.tsx +++ b/src/components/pages/poll/Comment/commentDetail.tsx @@ -26,17 +26,20 @@ import { ModalBody, ModalFooter, } from '@/components/ui/modal' +import { Profile } from '@/types/member' interface CommentDetailProps { comments?: Comment[] voteId?: number | string onClose?: () => void + profile: Profile } const CommentDetail = ({ comments = [], voteId, onClose, + profile, }: CommentDetailProps) => { const [openReplies, setOpenReplies] = useState>({}) const [currentSort, setCurrentSort] = useState<'popular' | 'latest'>( @@ -294,12 +297,15 @@ const CommentDetail = ({
    - + {(profile.role === 'ADMIN' || + comment.nickname === profile.nickname) && ( + + )} {openMenus[comment.commentId] && (
    + +
    +
    + ) +} + +export default AccountDeletionPage diff --git a/src/components/pages/my/accountControlSection.tsx b/src/components/pages/my/accountControlSection.tsx index fd21f9f..4519de0 100644 --- a/src/components/pages/my/accountControlSection.tsx +++ b/src/components/pages/my/accountControlSection.tsx @@ -1,15 +1,17 @@ import { useAppDispatch } from '@/hooks/utils/useAppDispatch' -import { logoutThunk, signoutThunk } from '@/store/thunks/authThunks' +import { logoutThunk } from '@/store/thunks/authThunks' +import { useRouter } from 'next/navigation' export default function AccountControlSection() { const dispatch = useAppDispatch() + const router = useRouter() const handleLogout = () => { dispatch(logoutThunk()) } const handleSignout = () => { - dispatch(signoutThunk()) + router.push('/account-deletion') } return ( From e0c9287dbbe7db3c6a4fbf0613fd0e86a45980fc Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:47:29 +0900 Subject: [PATCH 31/31] =?UTF-8?q?chore:=20=EA=B0=9C=EC=9D=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=B9=A8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=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)/privacy/page.tsx | 7 + .../pages/privacy/privacyPolicyPage.tsx | 180 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/app/(unauth)/privacy/page.tsx create mode 100644 src/components/pages/privacy/privacyPolicyPage.tsx diff --git a/src/app/(unauth)/privacy/page.tsx b/src/app/(unauth)/privacy/page.tsx new file mode 100644 index 0000000..010086f --- /dev/null +++ b/src/app/(unauth)/privacy/page.tsx @@ -0,0 +1,7 @@ +import { PrivacyPolicyPage } from '../../../components/pages/privacy/privacyPolicyPage' + +function Privacy() { + return +} + +export default Privacy diff --git a/src/components/pages/privacy/privacyPolicyPage.tsx b/src/components/pages/privacy/privacyPolicyPage.tsx new file mode 100644 index 0000000..8576dee --- /dev/null +++ b/src/components/pages/privacy/privacyPolicyPage.tsx @@ -0,0 +1,180 @@ +import type { Metadata } from 'next' + +const APP_NAME = 'Valanse' +const CONTACT_EMAIL = 'valansekr@gmail.com' +const SERVICE_URL = 'https://valanse.kr' +const EFFECTIVE_DATE = '2026-01-23' // 시행일자(필요 시 변경) + +export const metadata: Metadata = { + title: `개인정보 처리방침 | ${APP_NAME}`, + description: `${APP_NAME} 개인정보 처리방침`, + robots: { index: true, follow: true }, +} + +export const PrivacyPolicyPage = () => { + return ( +
    +
    +

    개인정보 처리방침

    +

    + {APP_NAME}(이하 “서비스”)는 이용자의 개인정보를 중요하게 생각하며, + 관련 법령을 준수합니다. +

    +

    + 시행일자: {EFFECTIVE_DATE} +

    +
    + +
    +

    + 1. 수집하는 개인정보 항목 +

    +

    + 서비스는 카카오 로그인을 통해 회원가입/로그인을 제공하며, 다음 정보를 + 수집할 수 있습니다. +

    +
      +
    • + 카카오 계정 식별자 (예: 카카오에서 제공하는 고유 + ID) +
    • +
    • + 프로필 정보 (예: 닉네임, 프로필 이미지) — + 카카오에서 제공되는 범위 내 +
    • +
    • + 서비스 이용 기록 (예: 접속 로그, 이용 내역, 오류 + 로그) +
    • +
    • + 기기/환경 정보 (예: OS, 브라우저 종류, 앱 버전 등 + 서비스 제공을 위한 최소 정보) +
    • +
    +

    + ※ 서비스는 원칙적으로 주민등록번호, 금융정보 등 민감정보를 수집하지 + 않습니다. +

    +
    + +
    +

    + 2. 개인정보 수집 및 이용 목적 +

    +
      +
    • 카카오 로그인 기반 회원 식별 및 계정 관리
    • +
    • 서비스 제공 및 기능 운영, 고객 문의 대응
    • +
    • 서비스 안정성 확보(오류 확인/분석) 및 보안/부정 이용 방지
    • +
    +
    + +
    +

    + 3. 개인정보 보관 및 이용 기간 +

    +

    + 서비스는 개인정보의 수집·이용 목적이 달성되면 지체 없이 파기합니다. + 다만, 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 법령에서 정한 + 기간 동안 보관할 수 있습니다. +

    +
      +
    • + 회원 정보: 회원 탈퇴 시까지 (탈퇴 후 지체 없이 + 파기) +
    • +
    • + 이용 기록/로그: 서비스 안정성 및 보안 목적 범위 + 내에서 필요한 기간 보관 후 파기 +
    • +
    +
    + +
    +

    + 4. 개인정보의 제3자 제공 +

    +

    + 서비스는 원칙적으로 이용자의 개인정보를 제3자에게 제공하지 않습니다. + 다만, 법령에 근거가 있거나 이용자의 사전 동의를 받은 경우에 한하여 + 제공할 수 있습니다. +

    +
    + +
    +

    5. 개인정보 처리 위탁

    +

    + 서비스 운영을 위해 아래와 같은 외부 서비스를 이용할 수 있으며, 필요한 + 경우 개인정보 처리를 위탁할 수 있습니다. +

    +
      +
    • + 카카오(Kakao Corp.): 카카오 로그인(OAuth) 제공 +
    • +
    • + AWS(아마존웹서비스): 서버 인프라 운영(예: EC2 + 환경에서 백엔드(Spring) 운영) +
    • +
    +

    + ※ 위탁이 발생하는 경우, 관련 법령에 따라 위탁 계약을 통해 개인정보 + 보호 의무를 준수하도록 관리합니다. +

    +
    + +
    +

    + 6. 개인정보의 파기 절차 및 방법 +

    +

    + 서비스는 개인정보 보관 기간 경과 또는 처리 목적 달성 시 지체 없이 + 파기합니다. +

    +
      +
    • 전자적 파일 형태: 복구 불가능한 방법으로 영구 삭제
    • +
    • 출력물 형태: 분쇄 또는 소각
    • +
    +
    + +
    +

    7. 이용자의 권리

    +

    + 이용자는 개인정보에 대해 열람, 정정, 삭제, 처리 정지 등을 요청할 수 + 있습니다. 요청은 아래 문의처로 접수해 주세요. +

    +
    + +
    +

    + 8. 개인정보 보호 문의처 +

    +
    +
    + +
    +

    9. 변경 사항 공지

    +

    + 본 개인정보 처리방침은 법령, 정책 또는 서비스 변경에 따라 수정될 수 + 있으며, 변경 시 서비스 내 공지 또는 본 페이지를 통해 안내합니다. +

    +
    + + +
    + ) +}