Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/api/disputes/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function toParticipantDto(p: DisputeForDetail['participants'][number]): DisputeP
disputeId: p.disputeId,
userId: p.userId,
role: p.role.toLowerCase() as DisputeParticipantDto['role'],
nickname: null,
profileImageUrl: p.user.profileImageUrl ?? null,
joinedAt: p.joinedAt.toISOString(),
createdAt: p.createdAt.toISOString(),
Expand Down
19 changes: 18 additions & 1 deletion src/app/api/disputes/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { 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'
Expand Down Expand Up @@ -32,6 +32,9 @@ function toParticipantDto(p: DisputeForList['participants'][number]): DisputePar
disputeId: p.disputeId,
userId: p.userId,
role: p.role.toLowerCase() as DisputeParticipantDto['role'],
// DisputeParticipantDto 타입에 nickname이 선언되어 있어 null로 채워 타입 불일치 방지
// 닉네임을 DB에서 조회하지 않으므로 null 반환 (타입이 string | null을 허용함)
nickname: null,
profileImageUrl: p.user.profileImageUrl ?? null,
joinedAt: p.joinedAt.toISOString(),
createdAt: p.createdAt.toISOString(),
Expand Down Expand Up @@ -67,6 +70,8 @@ export async function GET(request: NextRequest) {

const { searchParams } = new URL(request.url)
const rawCategory = searchParams.get('categoryGroup')
// URL 쿼리 파라미터는 문자열로 전달되므로 "true" 문자열과 비교
const active = searchParams.get('active') === 'true'
const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10))
const limit = Math.min(50, Math.max(1, parseInt(searchParams.get('limit') ?? '20', 10)))

Expand Down Expand Up @@ -94,6 +99,18 @@ export async function GET(request: NextRequest) {
deletedAt: null,
participants: { some: { userId } },
...(rawCategory ? { categoryGroup: rawCategory.toUpperCase() as PrismaCategoryGroup } : {}),
// active=true 일 때 진행중 상태만 필터링
// draft(진술 전), judged(판결 완료), closed/expired/deleted 제외
...(active ? {
status: {
in: [
DisputeStatus.WAITING_OPPONENT,
DisputeStatus.OPPONENT_JOINED,
DisputeStatus.BOTH_SUBMITTED,
DisputeStatus.JUDGING,
],
},
} : {}),
}

try {
Expand Down
1 change: 0 additions & 1 deletion src/app/page.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
align-items: center;
align-self: center;
width: 100%;
max-width: 365px;
height: 88px;
padding: 20px 16px;
border: 2px solid v.$color-black-700;
Expand Down
3 changes: 2 additions & 1 deletion src/components/home/NewCaseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default function NewCaseButton() {
setIsCreating(true)
setErrorMessage(null)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
// 방 생성 + 사건 생성 두 번의 순차 API 호출을 커버할 수 있도록 30초로 설정
const timeout = setTimeout(() => controller.abort(), 30000)
try {
const roomRes = await fetch('/api/rooms', {
method: 'POST',
Expand Down
46 changes: 33 additions & 13 deletions src/hooks/useActiveCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { useQuery } from '@tanstack/react-query'
import type { CategoryGroup } from '@/types/common'
import type { DisputeListResponse, DisputeDto } from '@/types/dispute'

// ActiveCasesSection 컴포넌트가 기대하는 participant 형태
// API 응답(DisputeParticipantDto)은 profileImageUrl/nickname이 flat하게 있지만
// 컴포넌트는 user.profileImageUrl처럼 nested 구조를 사용하므로 변환이 필요함
type Participant = {
role: 'role_a' | 'role_b'
user: {
Expand All @@ -20,25 +24,41 @@ export type ActiveCase = {
participants: Participant[]
}

// 진행중인 사건 = 진술 시작 후 ~ 판결 전
// draft(진술 전), judging(판결 처리 중), judged(판결 완료) 제외
const ACTIVE_DISPUTE_STATUSES = ['waiting_opponent', 'opponent_joined', 'both_submitted'] as const
type ActiveDisputeStatus = (typeof ACTIVE_DISPUTE_STATUSES)[number]
// DisputeDto(API 응답) → ActiveCase(컴포넌트 타입) 변환
// API의 flat 구조를 컴포넌트가 기대하는 nested user 구조로 맞춰줌
function toActiveCase(dto: DisputeDto): ActiveCase {
return {
id: dto.id,
categoryGroup: dto.categoryGroup,
title: dto.title,
description: dto.description,
createdAt: dto.createdAt,
participants: dto.participants.map((p) => ({
role: p.role,
user: {
nickname: p.nickname,
profileImageUrl: p.profileImageUrl,
},
})),
}
}

type RawCase = ActiveCase & { status: ActiveDisputeStatus }
async function fetchActiveCases(): Promise<ActiveCase[]> {
// active=true: 서버에서 진행중 상태(waiting_opponent, opponent_joined, both_submitted, judging)만 필터링
const res = await fetch('/api/disputes?active=true')
if (!res.ok) throw new Error('진행중인 사건을 불러오지 못했습니다.')

// TODO: 진행중인 사건 API 연동 시 활성화
// async function fetchActiveCases(): Promise<ActiveCase[]> {
// const res = await fetch('/api/disputes?active=true')
// if (!res.ok) throw new Error('진행중인 사건을 불러오지 못했습니다.')
// const json = await res.json()
// return json.data.disputes
// }
const json: { data: DisputeListResponse } = await res.json()

// API가 이미 createdAt 기준 최신순 정렬로 내려주므로 클라이언트 재정렬 불필요
return json.data.disputes.map(toActiveCase)
}

export function useActiveCases() {
return useQuery({
queryKey: ['disputes', 'active'],
queryFn: async (): Promise<ActiveCase[]> => [],
queryFn: fetchActiveCases,
// 1분간 캐시 유지 — 홈 재진입 시 불필요한 재요청 방지
staleTime: 1000 * 60,
})
}
1 change: 1 addition & 0 deletions src/types/dispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface DisputeParticipantDto {
disputeId: string
userId: string
role: ParticipantRole
nickname: string | null
profileImageUrl: string | null
joinedAt: string
createdAt: string
Expand Down