diff --git a/src/app/(page)/disputes/[id]/DisputePage.module.scss b/src/app/(page)/disputes/[id]/DisputePage.module.scss index 4d6b127..435659b 100644 --- a/src/app/(page)/disputes/[id]/DisputePage.module.scss +++ b/src/app/(page)/disputes/[id]/DisputePage.module.scss @@ -314,6 +314,21 @@ // Modal // =========================== +.modalContent { + display: flex; + flex-direction: column; + gap: fn.r(20); +} + +.modalMessage { + font-family: v.$font-family-base; + font-size: v.$font-size-title-s; + font-weight: v.$font-weight-semibold; + line-height: v.$line-height-title-s; + color: var(--text-primary); + white-space: pre-line; +} + .modalOverlay { position: fixed; inset: 0; diff --git a/src/app/(page)/disputes/[id]/page.tsx b/src/app/(page)/disputes/[id]/page.tsx index f5e7f88..572564c 100644 --- a/src/app/(page)/disputes/[id]/page.tsx +++ b/src/app/(page)/disputes/[id]/page.tsx @@ -14,6 +14,7 @@ import Spinner from '@/components/ui/Spinner'; import StatusBadge from '@/components/ui/StatusBadge'; import CategoryIcon from '@/components/ui/CategoryIcon'; import InviteChoiceModal from '@/components/room/InviteChoiceModal'; +import Modal from '@/components/ui/Modal'; import JudgmentResult from '@/components/judgement/JudgmentResult'; import JudgmentTypeResult from '@/components/judgement/JudgmentTypeResult'; import { @@ -100,6 +101,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } data: judgment, isLoading: judgmentLoading, isError: judgmentError, + error: judgmentErrorData, } = useJudgment(id, isCompleted); const showToast = useToastStore((s) => s.show); @@ -108,8 +110,19 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } const [judgmentSubTab, setJudgmentSubTab] = React.useState<'verdict' | 'type'>('verdict'); const [showSoloModal, setShowSoloModal] = React.useState(false); const [isInviting, setIsInviting] = React.useState(false); + const [judgmentErrorModal, setJudgmentErrorModal] = React.useState(false); + const [judgmentErrorMessage, setJudgmentErrorMessage] = React.useState(null); const [isRefreshing, setIsRefreshing] = React.useState(false); + React.useEffect(() => { + if (judgmentError && !judgmentErrorModal) { + setJudgmentErrorMessage( + judgmentErrorData instanceof Error ? judgmentErrorData.message : '판결 결과를 불러올 수 없습니다.', + ); + setJudgmentErrorModal(true); + } + }, [judgmentError]); + const handleRefresh = async () => { setIsRefreshing(true); try { @@ -148,7 +161,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } const runJudge = () => { setShowSoloModal(false); requestJudgment(undefined, { - onSuccess: () => window.location.reload(), + onSuccess: () => queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id) }), onError: (error) => showToast(error instanceof Error ? error.message : 'AI 판결 요청에 실패했습니다.'), }); @@ -347,6 +360,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } onAlone={() => void runJudge()} onInvite={() => void handleInvite()} /> + + +
+

+ {judgmentErrorMessage ?? '판결 결과를 불러올 수 없습니다.'} +

+ +
+
); } diff --git a/src/app/(page)/mypage/edit/page.tsx b/src/app/(page)/mypage/edit/page.tsx index 9b45175..ae65006 100644 --- a/src/app/(page)/mypage/edit/page.tsx +++ b/src/app/(page)/mypage/edit/page.tsx @@ -22,6 +22,7 @@ export default function ProfileEditPage() { return () => setHeader(null) }, []) const fileInputRef = useRef(null); + const blobUrlRef = useRef(null); const { data: user, isLoading, isError, error } = useUserMe(); const updateProfile = useUpdateProfile(); @@ -43,23 +44,23 @@ export default function ProfileEditPage() { } }, [user, reset]); + useEffect(() => { + return () => { + if (blobUrlRef.current) URL.revokeObjectURL(blobUrlRef.current); + }; + }, []); + const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - if (previewUrl?.startsWith('blob:')) { - URL.revokeObjectURL(previewUrl); - } + const prevUrl = blobUrlRef.current; const objectUrl = URL.createObjectURL(file); + blobUrlRef.current = objectUrl; setPreviewUrl(objectUrl); + if (prevUrl) setTimeout(() => URL.revokeObjectURL(prevUrl), 0); }; - useEffect(() => { - return () => { - if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl); - }; - }, [previewUrl]); - const validate = () => { const next: Record = {}; diff --git a/src/app/(page)/privacy/page.tsx b/src/app/(page)/privacy/page.tsx index 72f59c2..248b490 100644 --- a/src/app/(page)/privacy/page.tsx +++ b/src/app/(page)/privacy/page.tsx @@ -1,23 +1,11 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import BackButton from '@/components/ui/BackButton'; import styles from './privacy.module.scss'; export default function PrivacyPage() { - const router = useRouter(); - return (
- +

개인정보 처리방침

diff --git a/src/app/(page)/terms/page.tsx b/src/app/(page)/terms/page.tsx index cb5b6ad..3bfa8d4 100644 --- a/src/app/(page)/terms/page.tsx +++ b/src/app/(page)/terms/page.tsx @@ -1,23 +1,11 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import BackButton from '@/components/ui/BackButton'; import styles from './terms.module.scss'; export default function TermsPage() { - const router = useRouter(); - return (
- +

이용약관

diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index d3136ee..affcbe3 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/db'; -import { getSessionUserId } from '@/lib/auth/session'; +import { getRequestUserId } from '@/lib/auth/session'; import type { ApiResponse } from '@/types/common'; import type { CalendarMonthResponse, CalendarRecordItem } from '@/types/calendar'; @@ -10,8 +8,7 @@ import type { CalendarMonthResponse, CalendarRecordItem } from '@/types/calendar // 해당 월의 날짜별 감정일기/사건 요약 반환 (달력 마킹용) export async function GET(request: NextRequest) { try { - const session = await getServerSession(authOptions); - const userId = getSessionUserId(session); + const userId = await getRequestUserId(request); if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts index 1c8c208..27ad5c9 100644 --- a/src/app/api/diary/route.ts +++ b/src/app/api/diary/route.ts @@ -1,16 +1,13 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/db'; -import { getSessionUserId } from '@/lib/auth/session'; +import { getRequestUserId } from '@/lib/auth/session'; import type { ApiResponse } from '@/types/common'; import type { DiaryItem } from '@/types/diary'; // GET /api/diary?date=2026-06-16 // 특정 날짜의 감정일기 목록 조회. 최신순 정렬. 본인 데이터만 반환 export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions); - const userId = getSessionUserId(session); + const userId = await getRequestUserId(request); if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/disputes/[id]/judge/route.ts b/src/app/api/disputes/[id]/judge/route.ts index 38f9ccf..427abdf 100644 --- a/src/app/api/disputes/[id]/judge/route.ts +++ b/src/app/api/disputes/[id]/judge/route.ts @@ -1,9 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import type { DisputeStatus } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { generateAiJudgment } from '@/lib/ai/judgment' import { toAiJudgmentDto } from '@/domains/judgement/judgment.mapper' import type { ApiResponse } from '@/types/common' @@ -15,8 +13,7 @@ export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/disputes/[id]/result/route.ts b/src/app/api/disputes/[id]/result/route.ts index 11e1f04..2f4a9af 100644 --- a/src/app/api/disputes/[id]/result/route.ts +++ b/src/app/api/disputes/[id]/result/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { toAiJudgmentDto } from '@/domains/judgement/judgment.mapper' import type { ApiResponse } from '@/types/common' import type { AiJudgmentDto } from '@/types/judgment' @@ -13,8 +11,7 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -25,11 +22,25 @@ export async function GET( const { id } = await params try { - // 존재 확인 먼저, 권한 확인은 그 이후 (정보 노출 방지) + // dispute 조회 + 참여자 권한 확인 + judgment를 단일 쿼리로 처리 const dispute = await prisma.dispute.findFirst({ - where: { id, deletedAt: null }, - select: { status: true }, + where: { + id, + deletedAt: null, + participants: { some: { userId } }, + }, + select: { + status: true, + aiJudgment: { + include: { + resultConflictDetail: true, + resultCard: true, + aiNotice: true, + }, + }, + }, }) + if (!dispute) { return NextResponse.json( { success: false, error: { code: 'DISPUTE_NOT_FOUND', message: '사건을 찾을 수 없습니다.' } }, @@ -37,16 +48,6 @@ export async function GET( ) } - const participant = await prisma.disputeParticipant.findFirst({ - where: { disputeId: id, userId }, - }) - if (!participant) { - return NextResponse.json( - { success: false, error: { code: 'FORBIDDEN', message: '판결 결과를 조회할 권한이 없습니다.' } }, - { status: 403 }, - ) - } - if (dispute.status === 'JUDGING') { return NextResponse.json( { @@ -74,14 +75,7 @@ export async function GET( ) } - const judgment = await prisma.aiJudgment.findFirst({ - where: { disputeId: id }, - include: { - resultConflictDetail: true, - resultCard: true, - aiNotice: true, - }, - }) + const judgment = dispute.aiJudgment if (!judgment) { return NextResponse.json( { diff --git a/src/app/api/disputes/[id]/route.ts b/src/app/api/disputes/[id]/route.ts index 71ec1d9..d614847 100644 --- a/src/app/api/disputes/[id]/route.ts +++ b/src/app/api/disputes/[id]/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { z } from 'zod' import { type Prisma, type CategoryGroup as PrismaCategoryGroup } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { VALID_CATEGORY_GROUPS, IMMUTABLE_DISPUTE_STATUSES } from '@/lib/constants/dispute' import type { ApiResponse, CategoryGroup } from '@/types/common' import type { DisputeDto, DisputeParticipantDto, DisputeStatementDto } from '@/types/dispute' @@ -85,8 +83,7 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -135,8 +132,7 @@ export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -245,8 +241,7 @@ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/disputes/[id]/statements/route.ts b/src/app/api/disputes/[id]/statements/route.ts index 6130e91..47e4d9b 100644 --- a/src/app/api/disputes/[id]/statements/route.ts +++ b/src/app/api/disputes/[id]/statements/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { z } from 'zod' import { Prisma, type DisputeStatus, type DisputeStatement } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { moderateContent } from '@/lib/ai/moderation' import { extractDisputeMeta } from '@/lib/ai/judgment' import type { ApiResponse } from '@/types/common' @@ -39,8 +37,7 @@ export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - let userId = getSessionUserId(session) + let userId = await getRequestUserId(request) if (!userId) { if (!DEV_BYPASS) { diff --git a/src/app/api/disputes/[id]/statements/submit/route.ts b/src/app/api/disputes/[id]/statements/submit/route.ts index 9c0f589..cde2b28 100644 --- a/src/app/api/disputes/[id]/statements/submit/route.ts +++ b/src/app/api/disputes/[id]/statements/submit/route.ts @@ -1,19 +1,16 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import type { ApiResponse } from '@/types/common' import type { DisputeStatus, StatementSubmitResponse } from '@/types/dispute' // POST /api/disputes/:id/statements/submit // 진술 최종 제출. submittedAt 설정 후 dispute 상태 업데이트. 멱등성 보장. export async function POST( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( diff --git a/src/app/api/disputes/[id]/status/route.ts b/src/app/api/disputes/[id]/status/route.ts index 1c3da4b..1e7e440 100644 --- a/src/app/api/disputes/[id]/status/route.ts +++ b/src/app/api/disputes/[id]/status/route.ts @@ -1,9 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { z } from 'zod' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import type { ApiResponse } from '@/types/common' import type { DisputeStatus } from '@/types/dispute' @@ -21,8 +19,7 @@ export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/disputes/route.ts b/src/app/api/disputes/route.ts index 126c4c6..051107c 100644 --- a/src/app/api/disputes/route.ts +++ b/src/app/api/disputes/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { z } from 'zod' import { type Prisma, type CategoryGroup as PrismaCategoryGroup, DisputeStatus } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { VALID_CATEGORY_GROUPS } from '@/lib/constants/dispute' import type { ApiResponse, CategoryGroup } from '@/types/common' import type { DisputeDto, DisputeParticipantDto, DisputeListResponse } from '@/types/dispute' @@ -57,8 +55,7 @@ function toDisputeDto(dispute: DisputeForList): DisputeDto { // GET /api/disputes // 내가 참여한 사건 목록 조회. categoryGroup으로 필터링, page/limit으로 페이지네이션 export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -148,8 +145,7 @@ export async function GET(request: NextRequest) { // POST /api/disputes // 사건 생성. 활성 방(ai_chat, invite_ready, one_to_one)에서 생성 가능. 생성자는 role_a로 확정 export async function POST(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/rooms/[id]/close/route.ts b/src/app/api/rooms/[id]/close/route.ts index 068b1b1..1d61cfb 100644 --- a/src/app/api/rooms/[id]/close/route.ts +++ b/src/app/api/rooms/[id]/close/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import type { ApiResponse } from '@/types/common' const CLOSEABLE_MODES = ['AI_ROOM', 'INVITE_READY', 'ONE_TO_ONE'] as const @@ -13,8 +11,7 @@ export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/rooms/[id]/invite/route.ts b/src/app/api/rooms/[id]/invite/route.ts index 10eb767..414d7c4 100644 --- a/src/app/api/rooms/[id]/invite/route.ts +++ b/src/app/api/rooms/[id]/invite/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { generateInviteToken, hashInviteToken, getInviteExpiresAt } from '@/lib/invite' import type { ApiResponse } from '@/types/common' @@ -17,8 +15,7 @@ export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/rooms/[id]/route.ts b/src/app/api/rooms/[id]/route.ts index 0e9f533..a6b8e76 100644 --- a/src/app/api/rooms/[id]/route.ts +++ b/src/app/api/rooms/[id]/route.ts @@ -1,9 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { type RoomMode as PrismaRoomMode, type CategoryGroup as PrismaCategoryGroup } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import type { ApiResponse, CategoryGroup } from '@/types/common' import type { RoomDto, RoomMode } from '@/types/room' @@ -49,8 +47,7 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -94,8 +91,7 @@ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/rooms/join/[token]/route.ts b/src/app/api/rooms/join/[token]/route.ts index 24a89f8..a4156b1 100644 --- a/src/app/api/rooms/join/[token]/route.ts +++ b/src/app/api/rooms/join/[token]/route.ts @@ -1,9 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { type CategoryGroup as PrismaCategoryGroup } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { hashInviteToken } from '@/lib/invite' import type { ApiResponse } from '@/types/common' @@ -27,8 +25,7 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ token: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -105,8 +102,7 @@ export async function POST( request: NextRequest, { params }: { params: Promise<{ token: string }> }, ) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts index ca8b9a4..75aef10 100644 --- a/src/app/api/rooms/route.ts +++ b/src/app/api/rooms/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' import { z } from 'zod' import { type RoomMode as PrismaRoomMode, type CategoryGroup as PrismaCategoryGroup } from '@prisma/client' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { VALID_CATEGORY_GROUPS } from '@/lib/constants/dispute' import type { ApiResponse, CategoryGroup } from '@/types/common' import type { RoomDto, RoomListResponse, RoomMode } from '@/types/room' @@ -50,8 +48,7 @@ function toRoomDto(room: { // GET /api/rooms // 내가 생성하거나 참여한 방 목록 조회 export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, @@ -98,8 +95,7 @@ export async function GET(request: NextRequest) { // POST /api/rooms // 방 생성. 기본 roomMode = ai_room export async function POST(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( { success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, diff --git a/src/app/api/user/me/profile-image/route.ts b/src/app/api/user/me/profile-image/route.ts index b3d88d7..17904b0 100644 --- a/src/app/api/user/me/profile-image/route.ts +++ b/src/app/api/user/me/profile-image/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { supabaseAdmin, PROFILE_IMAGES_BUCKET } from '@/lib/storage' import type { ApiResponse } from '@/types/common' @@ -12,8 +10,7 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'] export async function POST(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) { return NextResponse.json( diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts index 007a93c..2a3ef51 100644 --- a/src/app/api/user/me/route.ts +++ b/src/app/api/user/me/route.ts @@ -1,8 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' -import { getSessionUserId } from '@/lib/auth/session' +import { getRequestUserId } from '@/lib/auth/session' import { supabaseAdmin, PROFILE_IMAGES_BUCKET } from '@/lib/storage' import type { ApiResponse } from '@/types/common' @@ -64,9 +62,8 @@ function getAuthErrorResponse() { ) } -export async function GET() { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) +export async function GET(request: NextRequest) { + const userId = await getRequestUserId(request) if (!userId) return getAuthErrorResponse() @@ -105,8 +102,7 @@ export async function GET() { } export async function PATCH(request: NextRequest) { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) + const userId = await getRequestUserId(request) if (!userId) return getAuthErrorResponse() @@ -271,9 +267,8 @@ export async function PATCH(request: NextRequest) { } } -export async function DELETE() { - const session = await getServerSession(authOptions) - const userId = getSessionUserId(session) +export async function DELETE(request: NextRequest) { + const userId = await getRequestUserId(request) if (!userId) return getAuthErrorResponse() diff --git a/src/components/providers/QueryProvider.tsx b/src/components/providers/QueryProvider.tsx index 0a2b6fa..a7db2b6 100644 --- a/src/components/providers/QueryProvider.tsx +++ b/src/components/providers/QueryProvider.tsx @@ -7,6 +7,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; export default function QueryProvider({ children }: { children: React.ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); + const [queryClient] = useState( + () => new QueryClient({ defaultOptions: { queries: { retry: 0 } } }), + ); return {children}; } diff --git a/src/components/ui/BackButton.tsx b/src/components/ui/BackButton.tsx new file mode 100644 index 0000000..ec1e119 --- /dev/null +++ b/src/components/ui/BackButton.tsx @@ -0,0 +1,22 @@ +'use client' + +import { useRouter } from 'next/navigation' +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' + +interface BackButtonProps { + className?: string +} + +export default function BackButton({ className }: BackButtonProps) { + const router = useRouter() + return ( + + ) +} diff --git a/src/components/ui/Modal.module.scss b/src/components/ui/Modal.module.scss index c0d8ede..313b684 100644 --- a/src/components/ui/Modal.module.scss +++ b/src/components/ui/Modal.module.scss @@ -9,9 +9,8 @@ .paper { width: calc(100% - fn.r(40)); max-width: fn.r(360); - border-radius: fn.r(8) !important; border: 2px solid v.$color-black-700 !important; - border-radius: fn.r(16) !important; + border-radius: fn.r(8) !important; padding: fn.r(24) fn.r(20) fn.r(20); box-shadow: 0 fn.r(4) fn.r(24) rgba(0, 0, 0, 0.12); } diff --git a/src/domains/dispute/dispute.hooks.ts b/src/domains/dispute/dispute.hooks.ts index 5b2f86a..c46de30 100644 --- a/src/domains/dispute/dispute.hooks.ts +++ b/src/domains/dispute/dispute.hooks.ts @@ -45,6 +45,7 @@ export function useDispute( queryKey: disputeKeys.detail(disputeId), queryFn: () => fetchDispute(disputeId), enabled: !!disputeId, + staleTime: 1000 * 30, refetchInterval: options?.refetchInterval, }); } diff --git a/src/domains/judgement/judgement.hooks.ts b/src/domains/judgement/judgement.hooks.ts index 0b2ae20..48b80ff 100644 --- a/src/domains/judgement/judgement.hooks.ts +++ b/src/domains/judgement/judgement.hooks.ts @@ -13,5 +13,6 @@ export function useJudgment(disputeId: string, enabled = true) { queryKey: judgementKeys.detail(disputeId), queryFn: () => fetchJudgment(disputeId), enabled: !!disputeId && enabled, + staleTime: 1000 * 60 * 10, }) } diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts index 7b53426..6f1aa7c 100644 --- a/src/lib/auth/session.ts +++ b/src/lib/auth/session.ts @@ -1,7 +1,15 @@ +import type { NextRequest } from 'next/server' import type { Session } from 'next-auth' +import { getToken } from 'next-auth/jwt' // NextAuth v4는 기본 Session 타입에 user.id를 포함하지 않음 // authOptions의 callbacks.session에서 token.sub를 user.id로 추가해야 함 export function getSessionUserId(session: Session | null | undefined): string | undefined { return (session?.user as { id?: string } | undefined)?.id } + +// getServerSession 대신 getToken을 사용해 JWT 파싱만 수행 (callback 실행 없음) +export async function getRequestUserId(req: NextRequest): Promise { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }) + return token?.sub ?? undefined +}