Skip to content
15 changes: 15 additions & 0 deletions src/app/(page)/disputes/[id]/DisputePage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 23 additions & 1 deletion src/app/(page)/disputes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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<string | null>(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 {
Expand Down Expand Up @@ -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 판결 요청에 실패했습니다.'),
});
Expand Down Expand Up @@ -347,6 +360,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
onAlone={() => void runJudge()}
onInvite={() => void handleInvite()}
/>

<Modal open={judgmentErrorModal}>
<div className={styles.modalContent}>
<p className={styles.modalMessage}>
{judgmentErrorMessage ?? '판결 결과를 불러올 수 없습니다.'}
</p>
<Button onClick={() => setJudgmentErrorModal(false)}>확인</Button>
</div>
</Modal>
</div>
);
}
19 changes: 10 additions & 9 deletions src/app/(page)/mypage/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function ProfileEditPage() {
return () => setHeader(null)
}, [])
const fileInputRef = useRef<HTMLInputElement>(null);
const blobUrlRef = useRef<string | null>(null);

const { data: user, isLoading, isError, error } = useUserMe();
const updateProfile = useUpdateProfile();
Expand All @@ -43,23 +44,23 @@ export default function ProfileEditPage() {
}
}, [user, reset]);

useEffect(() => {
return () => {
if (blobUrlRef.current) URL.revokeObjectURL(blobUrlRef.current);
};
}, []);

const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<string, string> = {};

Expand Down
16 changes: 2 additions & 14 deletions src/app/(page)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.page}>
<header className={styles.header}>
<button
type="button"
className={styles.backButton}
onClick={() => router.back()}
aria-label="뒤로 가기"
>
<ArrowBackIosNewIcon />
</button>
<BackButton className={styles.backButton} />
<h1 className={styles.title}>개인정보 처리방침</h1>
<span className={styles.spacer} />
</header>
Expand Down
16 changes: 2 additions & 14 deletions src/app/(page)/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.page}>
<header className={styles.header}>
<button
type="button"
className={styles.backButton}
onClick={() => router.back()}
aria-label="뒤로 가기"
>
<ArrowBackIosNewIcon />
</button>
<BackButton className={styles.backButton} />
<h1 className={styles.title}>이용약관</h1>
<span className={styles.spacer} />
</header>
Expand Down
7 changes: 2 additions & 5 deletions src/app/api/calendar/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
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';

// GET /api/calendar?year=2026&month=6
// 해당 월의 날짜별 감정일기/사건 요약 반환 (달력 마킹용)
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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down
7 changes: 2 additions & 5 deletions src/app/api/diary/route.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down
7 changes: 2 additions & 5 deletions src/app/api/disputes/[id]/judge/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down
46 changes: 20 additions & 26 deletions src/app/api/disputes/[id]/result/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand All @@ -25,28 +22,32 @@ 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<ApiResponse>(
{ success: false, error: { code: 'DISPUTE_NOT_FOUND', message: '사건을 찾을 수 없습니다.' } },
{ status: 404 },
)
}

const participant = await prisma.disputeParticipant.findFirst({
where: { disputeId: id, userId },
})
if (!participant) {
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'FORBIDDEN', message: '판결 결과를 조회할 권한이 없습니다.' } },
{ status: 403 },
)
}

if (dispute.status === 'JUDGING') {
return NextResponse.json<ApiResponse>(
{
Expand Down Expand Up @@ -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<ApiResponse>(
{
Expand Down
13 changes: 4 additions & 9 deletions src/app/api/disputes/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down Expand Up @@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down Expand Up @@ -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<ApiResponse>(
{ success: false, error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } },
Expand Down
7 changes: 2 additions & 5 deletions src/app/api/disputes/[id]/statements/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
Loading