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
32 changes: 15 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@

**단독 판결 흐름**
```txt
AI 대화방 생성
→ AI 대화
→ 단독 진술 작성
방 생성
→ 진술 작성 및 제출
→ AI 판결 생성 (제한적 결과)
→ 판결 결과 확인
```

**1:1 판결 흐름**
```txt
AI 대화방 생성
→ AI 대화
방 생성
→ 초대 링크 발급
→ 상대방 참여
→ 1:1 조정 전환
→ 양측 진술 작성
→ 양측 진술 작성 및 제출
→ AI 판결 생성 (전체 결과)
→ 판결 결과 확인
→ 선물추천
Expand All @@ -39,8 +37,7 @@ AI 대화방 생성
```txt
- 카카오 로그인
- 약관 동의
- AI 대화방 생성
- AI와 갈등 상황 정리
- 방 생성
- 단독 판결 (진술 1건 기반, 제한적 결과 제공)
- 초대 링크 발급
- 상대방 참여
Expand Down Expand Up @@ -102,15 +99,15 @@ MVP 제외 기능은 구현하지 않는다.
역할
```

### AI 대화방 정책
### 정책

```txt
- 모든 방은 먼저 AI 대화방으로 생성한다.
- AI 대화방 단계에서는 갈등 정리, 감정 정리, 대화 방향 조언만 제공한다.
- AI 대화방 단계에서는 판결 점수를 생성하지 않는다.
- 진술저장 후 분기:
- 혼자서 진행: disputes/[id]/statement에서 진술 작성 후 단독 AI 판결 가능
- 상대방 초대: disputes/[id]/statement에서 진술 작성 후 초대 링크 발급 → 1:1 AI 판결은 상대방 참여 후 가능
- 방 생성 시 roomMode = ai_room 상태로 시작한다.
- 진술 작성 후 분기:
- 혼자서 진행: disputes/[id]/statement에서 진술 제출 후 단독 AI 판결 가능
- 상대방 초대: 초대 링크 발급 → roomMode = invite_ready, dispute status = waiting_opponent
상대방 참여 → roomMode = one_to_one, dispute status = opponent_joined
이후 양측 진술 제출 후 1:1 AI 판결 가능
- 단독 판결과 1:1 판결 모두 disputes/[id]/statement를 거친다.
```

Expand Down Expand Up @@ -246,7 +243,7 @@ src/domains/user-items 생성 금지
### room_mode

```txt
ai_chat
ai_room
→ invite_ready
→ one_to_one
→ closed / expired / deleted
Expand All @@ -270,6 +267,7 @@ draft

```txt
draft
→ waiting_opponent
→ judging
→ judged
→ closed / expired / deleted
Expand All @@ -284,7 +282,7 @@ draft
### 권한 기준

```txt
AI 대화방 조회:
방 조회 (open/invite_ready):
creator_user_id 기준

1:1 사건 조회:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- room_mode: ai_chat → ai_room 으로 이름 변경
ALTER TYPE "room_mode" RENAME VALUE 'ai_chat' TO 'ai_room';
4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ model DisputeRoom {
roomNo String @unique @map("room_no") @db.VarChar(50)
creatorUserId String @map("creator_user_id") @db.Uuid
categoryGroup CategoryGroup @map("category_group")
roomMode RoomMode @default(AI_CHAT) @map("room_mode")
roomMode RoomMode @default(AI_ROOM) @map("room_mode")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHAT보다 ROOM이 더 맞는 네이밍같아서 바꾸어보았습니다 참고부탁드려요
@evenif99 @juahcheon

roomTokenHash String? @unique @map("room_token_hash") @db.VarChar(255)
inviteCreatedAt DateTime? @map("invite_created_at")
expiresAt DateTime? @map("expires_at")
Expand Down Expand Up @@ -441,7 +441,7 @@ enum CategoryGroup {
}

enum RoomMode {
AI_CHAT @map("ai_chat")
AI_ROOM @map("ai_room")
INVITE_READY @map("invite_ready")
ONE_TO_ONE @map("one_to_one")
CLOSED @map("closed")
Expand Down
3 changes: 2 additions & 1 deletion src/app/(page)/disputes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ 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 canJudge = dispute?.status === 'waiting_opponent' || dispute?.status === 'both_submitted'

const isSolo = dispute !== undefined && dispute.participants.length === 1
const roleAStatement = dispute?.statements?.find((s) => s.role === 'role_a')
const roleBStatement = dispute?.statements?.find((s) => s.role === 'role_b')
const canJudge = (isSolo && !!roleAStatement?.content) || dispute?.status === 'both_submitted'

const handleInvite = async () => {
if (isInviting || !dispute) return
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/disputes/[id]/judge/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { ApiResponse } from '@/types/common'
import type { AiJudgmentDto } from '@/types/judgment'

// POST /api/disputes/:id/judge
// AI 판결 요청. both_submitted 상태일 때만 가능. 중복 요청 방지 (멱등성)
// AI 판결 요청. 단독: waiting_opponent, 1:1: both_submitted 상태일 때만 가능. 중복 요청 방지 (멱등성)
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
Expand All @@ -28,7 +28,7 @@ export async function POST(

// JUDGING 상태 전환 여부를 추적해 외부 catch에서도 원복할 수 있게 함
let statusSetToJudging = false
let previousStatus: DisputeStatus = 'BOTH_SUBMITTED'
let previousStatus: DisputeStatus = 'WAITING_OPPONENT'

try {
const dispute = await prisma.dispute.findFirst({
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/disputes/[id]/statements/submit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export async function POST(
)
}

// ROLE_B는 ROLE_A가 진술 저장 완료(OPPONENT_JOINED) 후에만 제출 가능
// ROLE_B는 상대방이 참여 완료(OPPONENT_JOINED) 후에만 제출 가능
// BOTH_SUBMITTED 허용: 이미 제출한 경우 재시도 시 아래 멱등성 체크까지 도달해야 함
if (participant.role === 'ROLE_B' && dispute.status !== 'OPPONENT_JOINED' && dispute.status !== 'BOTH_SUBMITTED') {
return NextResponse.json<ApiResponse>(
{
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/rooms/[id]/close/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { prisma } from '@/lib/db'
import { getSessionUserId } from '@/lib/auth/session'
import type { ApiResponse } from '@/types/common'

const CLOSEABLE_MODES = ['AI_CHAT', 'INVITE_READY', 'ONE_TO_ONE'] as const
const CLOSEABLE_MODES = ['AI_ROOM', 'INVITE_READY', 'ONE_TO_ONE'] as const

// POST /api/rooms/:id/close
// 방 종료. 생성자만 가능. closed/expired/deleted 상태에서는 불가
Expand Down
33 changes: 23 additions & 10 deletions src/app/api/rooms/[id]/invite/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export async function POST(
)
}

if (room.roomMode !== 'AI_CHAT') {
if (room.roomMode !== 'AI_ROOM' && room.roomMode !== 'INVITE_READY') {
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'INVALID_STATUS', message: 'AI 대화방 상태에서만 초대 링크를 발급할 수 있습니다.' } },
{ success: false, error: { code: 'INVALID_STATUS', message: '초대 링크를 발급할 수 없는 상태입니다.' } },
{ status: 422 },
)
}
Expand All @@ -58,14 +58,27 @@ export async function POST(
const tokenHash = hashInviteToken(token)
const expiresAt = getInviteExpiresAt()

await prisma.disputeRoom.update({
where: { id },
data: {
roomTokenHash: tokenHash,
inviteCreatedAt: new Date(),
expiresAt,
roomMode: 'INVITE_READY',
},
await prisma.$transaction(async (tx) => {
await tx.disputeRoom.update({
where: { id },
data: {
roomTokenHash: tokenHash,
inviteCreatedAt: new Date(),
expiresAt,
roomMode: 'INVITE_READY',
},
})

const existingDispute = await tx.dispute.findFirst({
where: { roomId: id },
})

if (existingDispute && existingDispute.status === 'DRAFT') {
await tx.dispute.update({
where: { id: existingDispute.id },
data: { status: 'WAITING_OPPONENT' },
})
}
})

const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3030'
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/rooms/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function toRoomDto(room: {
}

function hasAccess(room: { creatorUserId: string; roomMode: PrismaRoomMode; dispute: { participants: { userId: string }[] } | null }, userId: string): boolean {
if (room.roomMode === 'AI_CHAT' || room.roomMode === 'INVITE_READY') {
if (room.roomMode === 'AI_ROOM' || room.roomMode === 'INVITE_READY') {
return room.creatorUserId === userId
}
return (
Expand Down
1 change: 1 addition & 0 deletions src/app/api/rooms/join/[token]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export async function POST(
where: { id: targetDispute.id },
data: { status: 'OPPONENT_JOINED' },
})
// WAITING_OPPONENT(초대 발급 시 설정)에서 OPPONENT_JOINED로 전환
}

await tx.disputeParticipant.create({
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/rooms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function GET(request: NextRequest) {
}

// POST /api/rooms
// AI 대화방 생성. 기본 roomMode = ai_chat
// 생성. 기본 roomMode = ai_room
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)
Expand Down Expand Up @@ -144,7 +144,7 @@ export async function POST(request: NextRequest) {
roomNo: generateRoomNo(),
creatorUserId: userId,
categoryGroup: categoryGroup.toUpperCase() as PrismaCategoryGroup,
roomMode: 'AI_CHAT',
roomMode: 'AI_ROOM',
},
})

Expand Down
2 changes: 1 addition & 1 deletion src/lib/invite.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHmac, randomUUID } from 'crypto'

const INVITE_EXPIRES_HOURS = 72
const INVITE_EXPIRES_HOURS = 168 // 7일

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초대링크 유효기간 3일 -> 7일로 늘렸는데 다른의견있으시면 코멘트 부탁드림니다


export function generateInviteToken(): string {
return randomUUID()
Expand Down
2 changes: 1 addition & 1 deletion src/types/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CategoryGroup } from './common'

// DB RoomMode enum @map 값 기준
export type RoomMode =
| 'ai_chat'
| 'ai_room'
| 'invite_ready'
| 'one_to_one'
| 'closed'
Expand Down