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({ 진술 내용을 다시 확인해주세요.

-