From 4f6a42d970858aacb776debea85256b87865818b Mon Sep 17 00:00:00 2001 From: juahcheon <132863519+juahcheon@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:31:31 +0900 Subject: [PATCH 1/5] =?UTF-8?q?perf:=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=86=8D=EB=8F=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useDispute: staleTime 30s 추가 — 페이지 이동 시 캐시 재사용 - useJudgment: staleTime 10m 추가 — 판결 결과 캐시 유지 - result route: DB 쿼리 3번 → 1번으로 통합 - runJudge: window.location.reload() → invalidateQueries 교체 Co-Authored-By: Claude Sonnet 4.6 --- src/app/(page)/disputes/[id]/page.tsx | 2 +- src/app/api/disputes/[id]/result/route.ts | 39 +++++++++++------------ src/domains/dispute/dispute.hooks.ts | 1 + src/domains/judgement/judgement.hooks.ts | 1 + 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/app/(page)/disputes/[id]/page.tsx b/src/app/(page)/disputes/[id]/page.tsx index 9624f0c..4b54248 100644 --- a/src/app/(page)/disputes/[id]/page.tsx +++ b/src/app/(page)/disputes/[id]/page.tsx @@ -141,7 +141,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 판결 요청에 실패했습니다.'), }); diff --git a/src/app/api/disputes/[id]/result/route.ts b/src/app/api/disputes/[id]/result/route.ts index 11e1f04..c60f17d 100644 --- a/src/app/api/disputes/[id]/result/route.ts +++ b/src/app/api/disputes/[id]/result/route.ts @@ -25,11 +25,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 +51,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 +78,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/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, }) } From 22cc3abdc989a097eaf0355b4614da602026b3a1 Mon Sep 17 00:00:00 2001 From: juahcheon <132863519+juahcheon@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:05:55 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=ED=8C=90=EA=B2=B0=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=97=90=EB=9F=AC=20=EC=8B=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20=EC=A6=89=EC=8B=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(page)/disputes/[id]/DisputePage.module.scss | 15 +++++++++++++++ src/app/(page)/disputes/[id]/page.tsx | 16 ++++++++++++++++ src/domains/judgement/judgement.hooks.ts | 1 + 3 files changed, 32 insertions(+) diff --git a/src/app/(page)/disputes/[id]/DisputePage.module.scss b/src/app/(page)/disputes/[id]/DisputePage.module.scss index d465c0b..8246933 100644 --- a/src/app/(page)/disputes/[id]/DisputePage.module.scss +++ b/src/app/(page)/disputes/[id]/DisputePage.module.scss @@ -305,6 +305,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 4b54248..0839fad 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 { @@ -94,6 +95,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); @@ -102,8 +104,13 @@ 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 [isRefreshing, setIsRefreshing] = React.useState(false); + React.useEffect(() => { + if (judgmentError) setJudgmentErrorModal(true); + }, [judgmentError]); + const handleRefresh = async () => { setIsRefreshing(true); try { @@ -335,6 +342,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } onAlone={() => void runJudge()} onInvite={() => void handleInvite()} /> + + setJudgmentErrorModal(false)}> +
+

+ {judgmentErrorData instanceof Error ? judgmentErrorData.message : '판결 결과를 불러올 수 없습니다.'} +

+ +
+
); } diff --git a/src/domains/judgement/judgement.hooks.ts b/src/domains/judgement/judgement.hooks.ts index 48b80ff..149351f 100644 --- a/src/domains/judgement/judgement.hooks.ts +++ b/src/domains/judgement/judgement.hooks.ts @@ -14,5 +14,6 @@ export function useJudgment(disputeId: string, enabled = true) { queryFn: () => fetchJudgment(disputeId), enabled: !!disputeId && enabled, staleTime: 1000 * 60 * 10, + retry: 0, }) } From c4390ff933817c4028bd9b2cf42cb168111e6622 Mon Sep 17 00:00:00 2001 From: juahcheon <132863519+juahcheon@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:39:02 +0900 Subject: [PATCH 3/5] =?UTF-8?q?perf:=20getServerSession=20=E2=86=92=20getR?= =?UTF-8?q?equestUserId=20=EA=B5=90=EC=B2=B4=EB=A1=9C=20API=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=B2=98=EB=A6=AC=20=EA=B2=BD=EB=9F=89=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(page)/disputes/[id]/page.tsx | 12 +++++++++--- src/app/api/calendar/route.ts | 7 ++----- src/app/api/diary/route.ts | 7 ++----- src/app/api/disputes/[id]/judge/route.ts | 7 ++----- src/app/api/disputes/[id]/result/route.ts | 7 ++----- src/app/api/disputes/[id]/route.ts | 13 ++++--------- src/app/api/disputes/[id]/statements/route.ts | 7 ++----- .../disputes/[id]/statements/submit/route.ts | 9 +++------ src/app/api/disputes/[id]/status/route.ts | 7 ++----- src/app/api/disputes/route.ts | 10 +++------- src/app/api/rooms/[id]/close/route.ts | 7 ++----- src/app/api/rooms/[id]/invite/route.ts | 7 ++----- src/app/api/rooms/[id]/route.ts | 10 +++------- src/app/api/rooms/join/[token]/route.ts | 10 +++------- src/app/api/rooms/route.ts | 10 +++------- src/app/api/user/me/profile-image/route.ts | 7 ++----- src/app/api/user/me/route.ts | 17 ++++++----------- src/components/providers/QueryProvider.tsx | 4 +++- src/components/ui/Modal.module.scss | 3 +-- src/domains/judgement/judgement.hooks.ts | 1 - src/lib/auth/session.ts | 8 ++++++++ src/styles/layout/_page.scss | 3 --- 22 files changed, 64 insertions(+), 109 deletions(-) diff --git a/src/app/(page)/disputes/[id]/page.tsx b/src/app/(page)/disputes/[id]/page.tsx index cd314ff..572564c 100644 --- a/src/app/(page)/disputes/[id]/page.tsx +++ b/src/app/(page)/disputes/[id]/page.tsx @@ -111,10 +111,16 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } 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) setJudgmentErrorModal(true); + if (judgmentError && !judgmentErrorModal) { + setJudgmentErrorMessage( + judgmentErrorData instanceof Error ? judgmentErrorData.message : '판결 결과를 불러올 수 없습니다.', + ); + setJudgmentErrorModal(true); + } }, [judgmentError]); const handleRefresh = async () => { @@ -355,10 +361,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } onInvite={() => void handleInvite()} /> - setJudgmentErrorModal(false)}> +

- {judgmentErrorData instanceof Error ? judgmentErrorData.message : '판결 결과를 불러올 수 없습니다.'} + {judgmentErrorMessage ?? '판결 결과를 불러올 수 없습니다.'}

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 c60f17d..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: '로그인이 필요합니다.' } }, 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/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/judgement/judgement.hooks.ts b/src/domains/judgement/judgement.hooks.ts index 149351f..48b80ff 100644 --- a/src/domains/judgement/judgement.hooks.ts +++ b/src/domains/judgement/judgement.hooks.ts @@ -14,6 +14,5 @@ export function useJudgment(disputeId: string, enabled = true) { queryFn: () => fetchJudgment(disputeId), enabled: !!disputeId && enabled, staleTime: 1000 * 60 * 10, - retry: 0, }) } 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 +} diff --git a/src/styles/layout/_page.scss b/src/styles/layout/_page.scss index 29f6d2a..ed47628 100644 --- a/src/styles/layout/_page.scss +++ b/src/styles/layout/_page.scss @@ -8,7 +8,4 @@ max-width: v.$container-max-width; min-height: 100dvh; margin: 0 auto; - /* TODO: 개발 확인용 임시 border — 배포 전 제거 */ - border-left: 1px solid #ccc; - border-right: 1px solid #ccc; } From 7fb1b7fd68c1ae7d73dc6f5f51df987a3b4b78ab Mon Sep 17 00:00:00 2001 From: juahcheon <132863519+juahcheon@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:31:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20blob=20URL=20=EB=88=84=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - blobUrlRef로 이전 blob URL 추적 - 두 번째 이미지 선택 시 이전 URL revokeObjectURL 처리 - 컴포넌트 언마운트 시 마지막 URL 해제 Co-Authored-By: Claude Sonnet 4.6 --- src/app/(page)/mypage/edit/page.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 = {}; From 8653bf04ee474d20001d8c15301a0da4602e49de Mon Sep 17 00:00:00 2001 From: juahcheon <132863519+juahcheon@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:31:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20privacy=C2=B7terms=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20Server=20Component=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BackButton 공통 클라이언트 컴포넌트 분리 (router.back + MUI 아이콘) - privacy/page.tsx, terms/page.tsx에서 'use client' 제거 - 정적 콘텐츠를 서버에서 렌더링하여 초기 로딩 개선 Co-Authored-By: Claude Sonnet 4.6 --- src/app/(page)/privacy/page.tsx | 16 ++-------------- src/app/(page)/terms/page.tsx | 16 ++-------------- src/components/ui/BackButton.tsx | 22 ++++++++++++++++++++++ 3 files changed, 26 insertions(+), 28 deletions(-) create mode 100644 src/components/ui/BackButton.tsx 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/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 ( + + ) +}