diff --git a/src/app/(page)/disputes/[id]/page.tsx b/src/app/(page)/disputes/[id]/page.tsx
index 89faf1c..0bcb23a 100644
--- a/src/app/(page)/disputes/[id]/page.tsx
+++ b/src/app/(page)/disputes/[id]/page.tsx
@@ -134,7 +134,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
const [isRefreshing, setIsRefreshing] = React.useState(false);
React.useEffect(() => {
- if (judgmentError && !judgmentErrorModal) {
+ if (judgmentError && !judgmentErrorModal && dispute?.status !== 'closed') {
setJudgmentErrorMessage(
judgmentErrorData instanceof Error
? judgmentErrorData.message
@@ -172,7 +172,8 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
showToast('초대 링크 발급에 실패했어요.');
return;
}
- await navigator.clipboard.writeText(data.data.inviteUrl);
+ const category = dispute.categoryGroup.toLowerCase()
+ await navigator.clipboard.writeText(`${data.data.inviteUrl}?category=${category}`);
showToast('초대 링크가 복사되었어요!');
} catch {
showToast('초대 링크 발급에 실패했어요.');
@@ -308,7 +309,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
className={`${styles.statementCard}${myRole === 'role_a' && !isCompleted ? ` ${styles.statementCardEditable}` : ''}`}
onClick={
myRole === 'role_a' && !isCompleted
- ? () => router.push(`/disputes/${id}/statement?edit=true`)
+ ? () => router.push(`/disputes/${id}/statement?category=${dispute.categoryGroup}&edit=true`)
: undefined
}
{...(myRole === 'role_a' && !isCompleted
@@ -318,7 +319,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
onKeyDown: (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
- router.push(`/disputes/${id}/statement?edit=true`);
+ router.push(`/disputes/${id}/statement?category=${dispute.categoryGroup}&edit=true`);
}
},
}
@@ -337,9 +338,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
className={styles.statementAvatarImg}
/>
- {participantA?.mbti && (
- {participantA.mbti}
- )}
+ {participantA?.mbti ?? 'MBTI 미설정'}
)}
@@ -348,7 +347,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
className={`${styles.statementCard}${myRole === 'role_b' && !isCompleted ? ` ${styles.statementCardEditable}` : ''}`}
onClick={
myRole === 'role_b' && !isCompleted
- ? () => router.push(`/disputes/${id}/statement?edit=true`)
+ ? () => router.push(`/disputes/${id}/statement?category=${dispute.categoryGroup}&edit=true`)
: undefined
}
{...(myRole === 'role_b' && !isCompleted
@@ -358,7 +357,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
onKeyDown: (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
- router.push(`/disputes/${id}/statement?edit=true`);
+ router.push(`/disputes/${id}/statement?category=${dispute.categoryGroup}&edit=true`);
}
},
}
@@ -377,9 +376,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
className={styles.statementAvatarImg}
/>
- {participantB?.mbti && (
- {participantB.mbti}
- )}
+ {participantB?.mbti ?? 'MBTI 미설정'}
)}
@@ -447,7 +444,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
disabled={!canJudge}
onClick={() => (isSolo ? setShowSoloModal(true) : void runJudge())}
>
- 판결받기
+ {dispute.status === 'opponent_joined' ? '상대방이 작성 중...' : '판결받기'}
diff --git a/src/app/(page)/disputes/[id]/statement/StatementPage.module.scss b/src/app/(page)/disputes/[id]/statement/StatementPage.module.scss
index 412aa6e..2a15d12 100644
--- a/src/app/(page)/disputes/[id]/statement/StatementPage.module.scss
+++ b/src/app/(page)/disputes/[id]/statement/StatementPage.module.scss
@@ -74,6 +74,16 @@
background-color: var(--bg-page);
}
+.footerRow {
+ display: flex;
+ flex-direction: row;
+ gap: fn.r(12);
+
+ > * {
+ flex: 1;
+ }
+}
+
.modalOverlay {
position: fixed;
inset: 0;
diff --git a/src/app/(page)/disputes/[id]/statement/page.tsx b/src/app/(page)/disputes/[id]/statement/page.tsx
index 84b909e..d61aacf 100644
--- a/src/app/(page)/disputes/[id]/statement/page.tsx
+++ b/src/app/(page)/disputes/[id]/statement/page.tsx
@@ -6,6 +6,7 @@ import { useHeaderStore } from '@/stores/headerStore'
import { useDispute } from '@/domains/dispute/dispute.hooks'
import { useUserMe } from '@/domains/user/hooks'
import Button from '@/components/ui/Button'
+import Spinner from '@/components/ui/Spinner'
import Select from '@/components/ui/Select'
import Textarea from '@/components/ui/Textarea'
import { CATEGORY_ICON_MAP, CATEGORY_LABEL_MAP } from '@/components/ui/CategoryIcon'
@@ -62,39 +63,82 @@ export default function StatementPage({
const [isLoading, setIsLoading] = React.useState(false)
const [filterMessage, setFilterMessage] = React.useState(null)
const [showPersonalInfoWarning, setShowPersonalInfoWarning] = React.useState(false)
+ const [savedDisputeId, setSavedDisputeId] = React.useState(null)
+
+ const SAVING_MESSAGES = ['나쁜 말을 검열 중입니다', '내용을 저장 중입니다', 'AI가 분석 중입니다', '조금만 기다려주세요']
+ const [savingMsgIdx, setSavingMsgIdx] = React.useState(0)
+ React.useEffect(() => {
+ if (!isLoading) return
+ const timer = setInterval(() => setSavingMsgIdx((i) => (i + 1) % SAVING_MESSAGES.length), 2000)
+ return () => clearInterval(timer)
+ }, [isLoading])
+
+ const handleCancel = () => {
+ fetch(`/api/rooms/${id}`, { method: 'DELETE', keepalive: true }).catch(() => {})
+ router.push('/')
+ }
const handleSave = async () => {
if (isLoading) return
setIsLoading(true)
setFilterMessage(null)
+ if (mbti !== (userMe?.mbti ?? '')) {
+ const form = new FormData()
+ form.append('mbti', mbti)
+ await fetch('/api/user/me', { method: 'PATCH', body: form })
+ }
+
try {
- const res = await fetch(`/api/disputes/${id}/statements`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content }),
- })
- const json = await res.json() as { success: boolean; data?: { hasPersonalInfo?: boolean }; error?: { code?: string; message?: string } }
-
- if (!json.success) {
- setIsLoading(false)
- const AI_EXTRACTION_CODES = ['AI_EXTRACTION_FAILED', 'AI_EXTRACTION_TIMEOUT', 'AI_EXTRACTION_PARSE_ERROR']
- if (json.error?.code && AI_EXTRACTION_CODES.includes(json.error.code)) {
- alert(json.error.message ?? 'AI 분석에 실패했습니다. 다시 시도해주세요.')
+ if (isEditMode) {
+ const res = await fetch(`/api/disputes/${id}/statements`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content }),
+ })
+ const json = await res.json() as { success: boolean; data?: { hasPersonalInfo?: boolean }; error?: { code?: string; message?: string } }
+
+ if (!json.success) {
+ setIsLoading(false)
+ setFilterMessage(json.error?.message ?? '저장 중 오류가 발생했습니다. 다시 시도해주세요.')
return
}
- setFilterMessage(json.error?.message ?? '저장 중 오류가 발생했습니다. 다시 시도해주세요.')
- return
- }
- if (json.data?.hasPersonalInfo) {
- setIsLoading(false)
- setShowPersonalInfoWarning(true)
- return
- }
+ if (json.data?.hasPersonalInfo) {
+ setIsLoading(false)
+ setShowPersonalInfoWarning(true)
+ return
+ }
+
+ router.push(`/disputes/${id}`)
+ } else {
+ const res = await fetch('/api/disputes', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ roomId: id, categoryGroup: category, content }),
+ })
+ const json = await res.json() as { success: boolean; data?: { id: string; hasPersonalInfo?: boolean }; error?: { code?: string; message?: string } }
+
+ if (!json.success) {
+ setIsLoading(false)
+ const AI_EXTRACTION_CODES = ['AI_EXTRACTION_FAILED', 'AI_EXTRACTION_TIMEOUT', 'AI_EXTRACTION_PARSE_ERROR']
+ if (json.error?.code && AI_EXTRACTION_CODES.includes(json.error.code)) {
+ alert(json.error.message ?? 'AI 분석에 실패했습니다. 다시 시도해주세요.')
+ return
+ }
+ setFilterMessage(json.error?.message ?? '저장 중 오류가 발생했습니다. 다시 시도해주세요.')
+ return
+ }
- // 성공 시 isLoading을 false로 바꾸지 않음 — 페이지가 unmount될 때까지 스피너 유지
- router.push(`/disputes/${id}`)
+ if (json.data?.hasPersonalInfo) {
+ setSavedDisputeId(json.data.id)
+ setIsLoading(false)
+ setShowPersonalInfoWarning(true)
+ return
+ }
+
+ router.push(`/disputes/${json.data!.id}`)
+ }
} catch {
setIsLoading(false)
setFilterMessage('네트워크 오류가 발생했습니다. 다시 시도해주세요.')
@@ -116,6 +160,17 @@ export default function StatementPage({
const Icon = CATEGORY_ICON_MAP[category]
+ if (isLoading) {
+ return (
+
+
+
+
{SAVING_MESSAGES[savingMsgIdx]}
+
+
+ )
+ }
+
return (
@@ -156,9 +211,16 @@ export default function StatementPage({
-
+
+ {!isEditMode && (
+
+ )}
+
+
{showPersonalInfoWarning && (
@@ -170,7 +232,7 @@ export default function StatementPage({
진술 내용을 다시 확인해주세요.
-