diff --git a/package-lock.json b/package-lock.json index 59af41e..35f0715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -262,29 +263,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -292,7 +270,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -355,6 +332,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -398,6 +376,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1213,6 +1192,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.1.1.tgz", "integrity": "sha512-Wv+gInjrpf99l1Q0oHe0eOWGTnlbkzs5nowClX65KCT/2fyPMwcbFEEkUsOHdpcHhB5UAbz/d7jlwt5ajWVvlA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.29.2", "@mui/core-downloads-tracker": "^9.1.1", @@ -1329,6 +1309,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.1.1.tgz", "integrity": "sha512-q+aqNa58QZUwmmyUvJKKrStrej+4BcWFw4M0Ug+zRylPIQgR64cqvBnE3QTfLZm4OXulydp8Hl3zwKxMayrdsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.29.2", "@mui/private-theming": "^9.1.1", @@ -2122,6 +2103,7 @@ "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -2504,6 +2486,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2578,6 +2561,7 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -3192,6 +3176,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4198,7 +4183,8 @@ "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -4665,6 +4651,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4838,6 +4825,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6343,6 +6331,7 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6910,6 +6899,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.19", "@swc/helpers": "0.5.15", @@ -7516,6 +7506,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7578,6 +7569,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" @@ -7728,6 +7720,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7737,6 +7730,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7749,6 +7743,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.79.0.tgz", "integrity": "sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7764,13 +7759,15 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7853,7 +7850,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8090,6 +8088,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", @@ -8766,6 +8765,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8939,6 +8939,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/public/images/characters/character-blocked.png b/public/images/characters/character-blocked.png deleted file mode 100644 index 2d90003..0000000 Binary files a/public/images/characters/character-blocked.png and /dev/null differ diff --git a/public/images/characters/character-blocked.svg b/public/images/characters/character-blocked.svg new file mode 100644 index 0000000..fbe3060 --- /dev/null +++ b/public/images/characters/character-blocked.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-case.png b/public/images/characters/character-case.png deleted file mode 100644 index 41588cf..0000000 Binary files a/public/images/characters/character-case.png and /dev/null differ diff --git a/public/images/characters/character-case.svg b/public/images/characters/character-case.svg new file mode 100644 index 0000000..d796f8b --- /dev/null +++ b/public/images/characters/character-case.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-closed.png b/public/images/characters/character-closed.png deleted file mode 100644 index 785586f..0000000 Binary files a/public/images/characters/character-closed.png and /dev/null differ diff --git a/public/images/characters/character-closed.svg b/public/images/characters/character-closed.svg new file mode 100644 index 0000000..6fe8f53 --- /dev/null +++ b/public/images/characters/character-closed.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-gift.png b/public/images/characters/character-gift.png deleted file mode 100644 index 154f48a..0000000 Binary files a/public/images/characters/character-gift.png and /dev/null differ diff --git a/public/images/characters/character-gift.svg b/public/images/characters/character-gift.svg new file mode 100644 index 0000000..b4067e0 --- /dev/null +++ b/public/images/characters/character-gift.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-home.png b/public/images/characters/character-home.png deleted file mode 100644 index a9d1c05..0000000 Binary files a/public/images/characters/character-home.png and /dev/null differ diff --git a/public/images/characters/character-home.svg b/public/images/characters/character-home.svg new file mode 100644 index 0000000..2f776a9 --- /dev/null +++ b/public/images/characters/character-home.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-loading.png b/public/images/characters/character-loading.png deleted file mode 100644 index 5139c8a..0000000 Binary files a/public/images/characters/character-loading.png and /dev/null differ diff --git a/public/images/characters/character-loading.svg b/public/images/characters/character-loading.svg new file mode 100644 index 0000000..8274449 --- /dev/null +++ b/public/images/characters/character-loading.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/characters/character-welcome.png b/public/images/characters/character-welcome.png deleted file mode 100644 index 764f6bc..0000000 Binary files a/public/images/characters/character-welcome.png and /dev/null differ diff --git a/public/images/characters/character-welcome.svg b/public/images/characters/character-welcome.svg new file mode 100644 index 0000000..f1d2422 --- /dev/null +++ b/public/images/characters/character-welcome.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/common/favicon.svg b/public/images/common/favicon.svg new file mode 100644 index 0000000..385a9b0 --- /dev/null +++ b/public/images/common/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/common/login_character.png b/public/images/common/login_character.png deleted file mode 100644 index 2262227..0000000 Binary files a/public/images/common/login_character.png and /dev/null differ diff --git a/public/images/common/login_character.svg b/public/images/common/login_character.svg new file mode 100644 index 0000000..97e74ff --- /dev/null +++ b/public/images/common/login_character.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/common/logo.png b/public/images/common/logo.png deleted file mode 100644 index 2542702..0000000 Binary files a/public/images/common/logo.png and /dev/null differ diff --git a/public/images/common/logo.svg b/public/images/common/logo.svg new file mode 100644 index 0000000..9eb87a9 --- /dev/null +++ b/public/images/common/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/common/logo_kakao.svg b/public/images/common/logo_kakao.svg new file mode 100644 index 0000000..06c11ee --- /dev/null +++ b/public/images/common/logo_kakao.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/common/ogimg.jpg b/public/images/common/ogimg.jpg new file mode 100644 index 0000000..bcce82f Binary files /dev/null and b/public/images/common/ogimg.jpg differ diff --git a/public/images/common/thumbnail-default.png b/public/images/common/thumbnail-default.png deleted file mode 100644 index f4d0ae4..0000000 Binary files a/public/images/common/thumbnail-default.png and /dev/null differ diff --git a/public/images/common/thumbnail-default.svg b/public/images/common/thumbnail-default.svg new file mode 100644 index 0000000..f1fa36d --- /dev/null +++ b/public/images/common/thumbnail-default.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/app/(page)/disputes/[id]/page.tsx b/src/app/(page)/disputes/[id]/page.tsx index 98b7faa..9624f0c 100644 --- a/src/app/(page)/disputes/[id]/page.tsx +++ b/src/app/(page)/disputes/[id]/page.tsx @@ -1,142 +1,153 @@ -'use client' +'use client'; -import React from 'react' -import Image from 'next/image' -import { useRouter } from 'next/navigation' -import { useQueryClient } from '@tanstack/react-query' -import AutorenewRoundedIcon from '@mui/icons-material/AutorenewRounded' -import CircularProgress from '@mui/material/CircularProgress' -import Header from '@/components/layout/Header' -import Button from '@/components/ui/Button' -import Tabs from '@/components/ui/Tabs' -import Tab from '@/components/ui/Tab' -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 JudgmentResult from '@/components/judgement/JudgmentResult' -import JudgmentTypeResult from '@/components/judgement/JudgmentTypeResult' -import { useDispute, useRequestJudgment, useCloseDispute, disputeKeys } from '@/domains/dispute/dispute.hooks' -import { useJudgment } from '@/domains/judgement/judgement.hooks' -import { useUserMe } from '@/domains/user/hooks' -import { useToastStore } from '@/stores/toastStore' -import styles from './DisputePage.module.scss' +import React from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import { useQueryClient } from '@tanstack/react-query'; +import AutorenewRoundedIcon from '@mui/icons-material/AutorenewRounded'; +import CircularProgress from '@mui/material/CircularProgress'; +import Header from '@/components/layout/Header'; +import Button from '@/components/ui/Button'; +import Tabs from '@/components/ui/Tabs'; +import Tab from '@/components/ui/Tab'; +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 JudgmentResult from '@/components/judgement/JudgmentResult'; +import JudgmentTypeResult from '@/components/judgement/JudgmentTypeResult'; +import { + useDispute, + useRequestJudgment, + useCloseDispute, + disputeKeys, +} from '@/domains/dispute/dispute.hooks'; +import { useJudgment } from '@/domains/judgement/judgement.hooks'; +import { useUserMe } from '@/domains/user/hooks'; +import { useToastStore } from '@/stores/toastStore'; +import styles from './DisputePage.module.scss'; -const COMPLETED_STATUSES = ['judged', 'closed'] as const +const COMPLETED_STATUSES = ['judged', 'closed'] as const; const TABS = [ { id: 'statement', label: '진술' }, { id: 'judgement', label: '판결' }, -] +]; export default function DisputePage({ params }: { params: Promise<{ id: string }> }) { - const { id } = React.use(params) - const router = useRouter() - const queryClient = useQueryClient() + const { id } = React.use(params); + const router = useRouter(); + const queryClient = useQueryClient(); - const pollCountRef = React.useRef(0) - const MAX_POLL = 15 - const [pollExhausted, setPollExhausted] = React.useState(false) + const pollCountRef = React.useRef(0); + const MAX_POLL = 15; + const [pollExhausted, setPollExhausted] = React.useState(false); const { data: dispute, isLoading: fetchLoading } = useDispute(id, { refetchInterval: (query) => { - const d = query.state.data - if (!d) return false - const hasStatement = d.statements?.some((s) => s.role === 'role_a' && s.content) - const isTerminal = (COMPLETED_STATUSES as readonly string[]).includes(d.status) + const d = query.state.data; + if (!d) return false; + const hasStatement = d.statements?.some((s) => s.role === 'role_a' && s.content); + const isTerminal = (COMPLETED_STATUSES as readonly string[]).includes(d.status); if (d.title === '새 사건' && hasStatement && !isTerminal) { if (pollCountRef.current >= MAX_POLL) { - setPollExhausted(true) - return false + setPollExhausted(true); + return false; } - pollCountRef.current += 1 - return 2000 + pollCountRef.current += 1; + return 2000; } - pollCountRef.current = 0 - return false + pollCountRef.current = 0; + return false; }, - }) - const { mutate: requestJudgment, isPending: isJudging } = useRequestJudgment(id) - const { mutate: closeDispute, isPending: isClosing } = useCloseDispute(id) - const { data: userMe } = useUserMe() + }); + const { mutate: requestJudgment, isPending: isJudging } = useRequestJudgment(id); + const { mutate: closeDispute, isPending: isClosing } = useCloseDispute(id); + const { data: userMe } = useUserMe(); // judged(판결완료) + closed(종료) 모두 판결 결과 탭 노출 - const isCompleted = !!dispute && (COMPLETED_STATUSES as readonly string[]).includes(dispute.status) + const isCompleted = + !!dispute && (COMPLETED_STATUSES as readonly string[]).includes(dispute.status); const isExtractingMeta = !!dispute && dispute.title === '새 사건' && dispute.statements?.some((s) => s.role === 'role_a' && s.content) && !isCompleted && - !pollExhausted + !pollExhausted; const META_MESSAGES = [ '사건 정보를 분석하고 있어요', 'AI가 핵심 쟁점을 파악하고 있어요', '진술 내용을 정리하고 있어요', '거의 다 됐어요, 잠시만요', - ] - const [metaMsgIdx, setMetaMsgIdx] = React.useState(0) + ]; + const [metaMsgIdx, setMetaMsgIdx] = React.useState(0); React.useEffect(() => { - if (!isExtractingMeta) return + if (!isExtractingMeta) return; const timer = setInterval(() => { - setMetaMsgIdx((i) => (i + 1) % META_MESSAGES.length) - }, 2500) - return () => clearInterval(timer) - }, [isExtractingMeta]) + setMetaMsgIdx((i) => (i + 1) % META_MESSAGES.length); + }, 2500); + return () => clearInterval(timer); + }, [isExtractingMeta]); // 판결 완료/종료 상태일 때만 fetch — 불필요한 API 호출 방지 - const { data: judgment, isLoading: judgmentLoading, isError: judgmentError } = useJudgment(id, isCompleted) + const { + data: judgment, + isLoading: judgmentLoading, + isError: judgmentError, + } = useJudgment(id, isCompleted); - const showToast = useToastStore((s) => s.show) + const showToast = useToastStore((s) => s.show); - const [activeTab, setActiveTab] = React.useState('statement') - const [judgmentSubTab, setJudgmentSubTab] = React.useState<'verdict' | 'type'>('verdict') - const [showSoloModal, setShowSoloModal] = React.useState(false) - const [isInviting, setIsInviting] = React.useState(false) - const [isRefreshing, setIsRefreshing] = React.useState(false) + const [activeTab, setActiveTab] = React.useState('statement'); + const [judgmentSubTab, setJudgmentSubTab] = React.useState<'verdict' | 'type'>('verdict'); + const [showSoloModal, setShowSoloModal] = React.useState(false); + const [isInviting, setIsInviting] = React.useState(false); + const [isRefreshing, setIsRefreshing] = React.useState(false); const handleRefresh = async () => { - setIsRefreshing(true) + setIsRefreshing(true); try { - await queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id) }) + await queryClient.invalidateQueries({ queryKey: disputeKeys.detail(id) }); } finally { - setIsRefreshing(false) + setIsRefreshing(false); } - } + }; - 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 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 - setIsInviting(true) - setShowSoloModal(false) + if (isInviting || !dispute) return; + setIsInviting(true); + setShowSoloModal(false); try { - const res = await fetch(`/api/rooms/${dispute.roomId}/invite`, { method: 'POST' }) - const data = await res.json() + const res = await fetch(`/api/rooms/${dispute.roomId}/invite`, { method: 'POST' }); + const data = await res.json(); if (!res.ok) { - showToast('초대 링크 발급에 실패했어요.') - return + showToast('초대 링크 발급에 실패했어요.'); + return; } - await navigator.clipboard.writeText(data.data.inviteUrl) - showToast('초대 링크가 복사되었어요!') + await navigator.clipboard.writeText(data.data.inviteUrl); + showToast('초대 링크가 복사되었어요!'); } catch { - showToast('초대 링크 발급에 실패했어요.') + showToast('초대 링크 발급에 실패했어요.'); } finally { - setIsInviting(false) + setIsInviting(false); } - } + }; const runJudge = () => { - setShowSoloModal(false) + setShowSoloModal(false); requestJudgment(undefined, { onSuccess: () => window.location.reload(), - onError: (error) => showToast(error instanceof Error ? error.message : 'AI 판결 요청에 실패했습니다.'), - }) - } + onError: (error) => + showToast(error instanceof Error ? error.message : 'AI 판결 요청에 실패했습니다.'), + }); + }; - if (fetchLoading) return null + if (fetchLoading) return null; if (isExtractingMeta) { return ( @@ -146,7 +157,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }

{META_MESSAGES[metaMsgIdx]}

- ) + ); } if (isJudging) { @@ -157,7 +168,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }

{'작성한 진술서를 바탕으로\n분석중이에요'}

- ) + ); } if (!dispute) { @@ -166,10 +177,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
router.back()} />

사건을 찾을 수 없습니다.

- ) + ); } - const formattedDate = dispute.createdAt.slice(2, 10) + const formattedDate = dispute.createdAt.slice(2, 10); return (
@@ -185,31 +196,39 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } {dispute.title}
{isRefreshing ? ( - + ) : ( void handleRefresh()} /> )} - {dispute.description && ( -

{dispute.description}

- )} + {dispute.description &&

{dispute.description}

}
{dispute.participants.slice(0, 2).map((p) => { - const imgSrc = p.profileImageUrl - ?? (p.userId === userMe?.id ? userMe?.profileImageUrl : null) - ?? '/images/common/thumbnail-default.png' + const imgSrc = + p.profileImageUrl ?? + (p.userId === userMe?.id ? userMe?.profileImageUrl : null) ?? + '/images/common/thumbnail-default.svg'; return (
- ) + ); })}
{formattedDate} @@ -220,10 +239,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } {/* 초대 배너 (단독 + 미완료) */} {isSolo && !isCompleted && ( - @@ -318,7 +335,6 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } onAlone={() => void runJudge()} onInvite={() => void handleInvite()} /> -
- ) + ); } diff --git a/src/app/(page)/join/[token]/page.tsx b/src/app/(page)/join/[token]/page.tsx index d6098cd..ab145b6 100644 --- a/src/app/(page)/join/[token]/page.tsx +++ b/src/app/(page)/join/[token]/page.tsx @@ -95,7 +95,7 @@ export default function JoinPage() { if (state === 'error') { return ( } + character={접근 불가} message={'죄송해요\n이 페이지는 접속할 수 없어요'} buttonLabel="메인으로 돌아가기" onButtonClick={goToMain} @@ -106,7 +106,7 @@ export default function JoinPage() { if (state === 'closed') { return ( } + character={종료된 사건} message={'사건이 종료되어\n더 이상 열람할 수 없어요'} buttonLabel="메인으로 돌아가기" onButtonClick={goToMain} @@ -117,7 +117,7 @@ export default function JoinPage() { if (state === 'expired') { return ( } + character={만료된 사건} message={'초대 링크가 만료되어\n더 이상 참여할 수 없어요'} buttonLabel="메인으로 돌아가기" onButtonClick={goToMain} @@ -129,7 +129,7 @@ export default function JoinPage() {
- 말해부엉 판사 + 말해부엉 판사

{joinInfo?.inviterNickname ?? ''} diff --git a/src/app/(page)/login/page.tsx b/src/app/(page)/login/page.tsx index 4549310..42f9303 100644 --- a/src/app/(page)/login/page.tsx +++ b/src/app/(page)/login/page.tsx @@ -20,12 +20,12 @@ export default function LoginPage() {

{/* eslint-disable-next-line @next/next/no-img-element */} 말해부엉 캐릭터 말해부엉
- diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 828e406..4089d31 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,15 +14,26 @@ const pretendard = localFont({ }); export const metadata: Metadata = { - title: 'TALKY-OWL', + title: '말해부엉', description: 'AI 갈등 조정 판결 서비스', + icons: { + icon: '/images/common/favicon.svg', + }, + openGraph: { + title: '말해부엉', + description: 'AI 갈등 조정 판결 서비스', + images: [ + { + url: '/images/common/ogimg.jpg', + width: 1200, + height: 630, + alt: '말해부엉', + }, + ], + }, }; -export default async function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { const session = await getCachedSession(); return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 48330dd..9374edc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,25 +1,25 @@ -import Image from 'next/image' -import Link from 'next/link' -import { getCachedSession } from '@/lib/auth/getSession' -import Header from '@/components/layout/Header' -import BottomNavigation from '@/components/layout/BottomNavigation' -import StatsCategorySection from '@/components/home/StatsCategorySection' -import ActiveCasesSection from '@/components/home/ActiveCasesSection' -import NewCaseButton from '@/components/home/NewCaseButton' -import HomeServiceInfo from '@/components/home/HomeServiceInfo' -import HomeGreeting from '@/components/home/HomeGreeting' -import styles from './page.module.scss' +import Image from 'next/image'; +import Link from 'next/link'; +import { getCachedSession } from '@/lib/auth/getSession'; +import Header from '@/components/layout/Header'; +import BottomNavigation from '@/components/layout/BottomNavigation'; +import StatsCategorySection from '@/components/home/StatsCategorySection'; +import ActiveCasesSection from '@/components/home/ActiveCasesSection'; +import NewCaseButton from '@/components/home/NewCaseButton'; +import HomeServiceInfo from '@/components/home/HomeServiceInfo'; +import HomeGreeting from '@/components/home/HomeGreeting'; +import styles from './page.module.scss'; export default async function HomePage() { - const session = await getCachedSession() - const isLoggedIn = !!session + const session = await getCachedSession(); + const isLoggedIn = !!session; return (
말해부엉 캐릭터오늘의 일기를 적어보세요

감정일기 작성하러가기

- + {/* 고민 카테고리 TOP4 */} @@ -56,7 +58,9 @@ export default async function HomePage() {

말해부엉이 궁금하신가요?

말해부엉 알아보기

- + )} @@ -69,5 +73,5 @@ export default async function HomePage() { {isLoggedIn && }
- ) + ); } diff --git a/src/components/home/ActiveCasesSection.tsx b/src/components/home/ActiveCasesSection.tsx index cc50edc..31f9157 100644 --- a/src/components/home/ActiveCasesSection.tsx +++ b/src/components/home/ActiveCasesSection.tsx @@ -1,28 +1,29 @@ -'use client' +'use client'; -import Image from 'next/image' -import { useRouter } from 'next/navigation' -import CaseCard from '@/components/ui/CaseCard' -import Avatar, { AvatarGroup } from '@/components/ui/Avatar' -import { useActiveCases } from '@/hooks/useActiveCases' -import styles from './ActiveCasesSection.module.scss' +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import CaseCard from '@/components/ui/CaseCard'; +import Avatar, { AvatarGroup } from '@/components/ui/Avatar'; +import { useActiveCases } from '@/hooks/useActiveCases'; +import styles from './ActiveCasesSection.module.scss'; function formatDate(isoString: string): string { - return isoString.slice(2, 10) + return isoString.slice(2, 10); } export default function ActiveCasesSection() { - const router = useRouter() - const { data: cases = [], isLoading, isError } = useActiveCases() + const router = useRouter(); + const { data: cases = [], isLoading, isError } = useActiveCases(); - if (isLoading) return null + if (isLoading) return null; - if (isError) return ( -
-

진행중인 사건

-

사건 목록을 불러올 수 없어요

-
- ) + if (isError) + return ( +
+

진행중인 사건

+

사건 목록을 불러올 수 없어요

+
+ ); return (
@@ -31,7 +32,7 @@ export default function ActiveCasesSection() { {cases.length === 0 ? (
)}
- ) + ); } diff --git a/src/components/home/HomeServiceInfo.tsx b/src/components/home/HomeServiceInfo.tsx index 21ef086..a35fcb4 100644 --- a/src/components/home/HomeServiceInfo.tsx +++ b/src/components/home/HomeServiceInfo.tsx @@ -1,18 +1,18 @@ -import Image from 'next/image' -import styles from './HomeServiceInfo.module.scss' +import Image from 'next/image'; +import styles from './HomeServiceInfo.module.scss'; const LINKS = [ { label: '서비스 소개', href: null }, { label: '개인정보 처리방침', href: '/privacy' }, { label: '이용약관', href: '/terms' }, { label: '고객문의', href: null }, -] +]; export default function HomeServiceInfo() { return (
말해부엉

Copyright ⓒ 말해부엉. ALL Rights Reserved.

- ) + ); } diff --git a/src/components/judgement/JudgmentResult.tsx b/src/components/judgement/JudgmentResult.tsx index d717baa..4efad65 100644 --- a/src/components/judgement/JudgmentResult.tsx +++ b/src/components/judgement/JudgmentResult.tsx @@ -1,41 +1,41 @@ -'use client' +'use client'; -import Image from 'next/image' -import { useDispute } from '@/domains/dispute/dispute.hooks' -import { useJudgment } from '@/domains/judgement/judgement.hooks' -import styles from './JudgmentResult.module.scss' +import Image from 'next/image'; +import { useDispute } from '@/domains/dispute/dispute.hooks'; +import { useJudgment } from '@/domains/judgement/judgement.hooks'; +import styles from './JudgmentResult.module.scss'; interface Props { - disputeId: string + disputeId: string; } function Avatar({ src }: { src: string | null }) { return (
- {src - ? - :
- } + {src ? ( + + ) : ( +
+ )}
- ) + ); } export default function JudgmentResult({ disputeId }: Props) { - const { data: dispute } = useDispute(disputeId) - const { data: judgment } = useJudgment(disputeId) + const { data: dispute } = useDispute(disputeId); + const { data: judgment } = useJudgment(disputeId); - if (!judgment || !dispute) return null + if (!judgment || !dispute) return null; - const { participants } = dispute - const isSolo = participants.length === 1 - const participantA = participants.find((p) => p.role === 'role_a') - const participantB = participants.find((p) => p.role === 'role_b') + const { participants } = dispute; + const isSolo = participants.length === 1; + const participantA = participants.find((p) => p.role === 'role_a'); + const participantB = participants.find((p) => p.role === 'role_b'); - const showReconcile = !!judgment.aSuggestedLine || (!isSolo && !!judgment.bSuggestedLine) + const showReconcile = !!judgment.aSuggestedLine || (!isSolo && !!judgment.bSuggestedLine); return (
- {/* ── 판결결과 ── */}

판결결과

@@ -72,7 +72,7 @@ export default function JudgmentResult({ disputeId }: Props) {

이렇게 사과해보면 어떨까요?

{/* 선물 아이콘: 제목 우측 상단 고정, 아래 카드와 살짝 겹침 */} )}
- ) + ); } - diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 48dc045..c8d9ac7 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -24,10 +24,12 @@ type HeaderProps = HeaderLogoProps | HeaderTitleProps; export default function Header(props: HeaderProps) { if (props.variant === 'logo') { return ( -
+
말해부엉
{onBack && ( - )} @@ -58,9 +56,7 @@ export default function Header(props: HeaderProps) { {title}
- {subtitle && ( -
{subtitle}
- )} + {subtitle &&
{subtitle}
}
); } diff --git a/src/components/records/RecordListSection.tsx b/src/components/records/RecordListSection.tsx index 56b1648..7b07022 100644 --- a/src/components/records/RecordListSection.tsx +++ b/src/components/records/RecordListSection.tsx @@ -1,19 +1,20 @@ -'use client' +'use client'; -import { useState } from 'react' -import Image from 'next/image' -import CategoryFilter, { type Category } from '@/components/ui/CategoryFilter' -import CaseRecordCard from '@/components/ui/CaseRecordCard' -import Spinner from '@/components/ui/Spinner' -import { useCompletedCases } from '@/domains/dispute/dispute.hooks' -import type { CategoryGroup } from '@/types/common' -import styles from './RecordListSection.module.scss' +import { useState } from 'react'; +import Image from 'next/image'; +import CategoryFilter, { type Category } from '@/components/ui/CategoryFilter'; +import CaseRecordCard from '@/components/ui/CaseRecordCard'; +import Spinner from '@/components/ui/Spinner'; +import { useCompletedCases } from '@/domains/dispute/dispute.hooks'; +import type { CategoryGroup } from '@/types/common'; +import styles from './RecordListSection.module.scss'; export default function RecordListSection() { - const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedCategory, setSelectedCategory] = useState('all'); - const categoryGroup = selectedCategory === 'all' ? undefined : selectedCategory as CategoryGroup - const { data: records = [], isLoading, isError } = useCompletedCases(categoryGroup) + const categoryGroup = + selectedCategory === 'all' ? undefined : (selectedCategory as CategoryGroup); + const { data: records = [], isLoading, isError } = useCompletedCases(categoryGroup); return (
@@ -29,7 +30,7 @@ export default function RecordListSection() {

사건 기록을 불러오지 못했어요. 잠시 후 다시 시도해주세요.

) : records.length === 0 ? (
- +

등록한 사건이 없어요

) : ( @@ -50,5 +51,5 @@ export default function RecordListSection() { )}
- ) + ); } diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index 3a156a8..9cd9f71 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -29,7 +29,7 @@ interface AvatarGroupProps { className?: string; } -const FALLBACK_SRC = '/images/common/thumbnail-default.png'; +const FALLBACK_SRC = '/images/common/thumbnail-default.svg'; export default function Avatar({ src, alt, size: sizeProp = 's', className }: AvatarProps) { const groupSize = useContext(AvatarSizeContext); diff --git a/src/components/ui/CaseRecordCard.tsx b/src/components/ui/CaseRecordCard.tsx index 6e4c883..6179bf0 100644 --- a/src/components/ui/CaseRecordCard.tsx +++ b/src/components/ui/CaseRecordCard.tsx @@ -1,29 +1,29 @@ -'use client' +'use client'; -import Link from 'next/link' -import MuiAvatar from '@mui/material/Avatar' -import MuiAvatarGroup from '@mui/material/AvatarGroup' -import CaseCard from '@/components/ui/CaseCard' -import StatusBadge, { type DisputeStatus } from '@/components/ui/StatusBadge' -import type { CategoryGroup } from '@/types/common' -import styles from './CaseRecordCard.module.scss' +import Link from 'next/link'; +import MuiAvatar from '@mui/material/Avatar'; +import MuiAvatarGroup from '@mui/material/AvatarGroup'; +import CaseCard from '@/components/ui/CaseCard'; +import StatusBadge, { type DisputeStatus } from '@/components/ui/StatusBadge'; +import type { CategoryGroup } from '@/types/common'; +import styles from './CaseRecordCard.module.scss'; interface Participant { - profileImageUrl?: string | null + profileImageUrl?: string | null; } interface Props { - href: string - title: string - description?: string | null - categoryGroup: CategoryGroup - status: DisputeStatus - createdAt: string - participants: Participant[] + href: string; + title: string; + description?: string | null; + categoryGroup: CategoryGroup; + status: DisputeStatus; + createdAt: string; + participants: Participant[]; } function formatDate(dateStr: string): string { - return dateStr.slice(2, 10) + return dateStr.slice(2, 10); } export default function CaseRecordCard({ @@ -35,7 +35,7 @@ export default function CaseRecordCard({ createdAt, participants, }: Props) { - const date = formatDate(createdAt) + const date = formatDate(createdAt); return ( @@ -61,15 +61,17 @@ export default function CaseRecordCard({ {participants.map((p, i) => ( ))} - +
- ) + ); } diff --git a/src/components/ui/Spinner.tsx b/src/components/ui/Spinner.tsx index 30dfa5c..a5a57c6 100644 --- a/src/components/ui/Spinner.tsx +++ b/src/components/ui/Spinner.tsx @@ -24,7 +24,7 @@ export default function Spinner({ className }: SpinnerProps) { sx={{ color: 'var(--bg-brand)' }} />