Skip to content

Commit 0e9f3ca

Browse files
authored
Merge pull request #61 from JECT-Study/feat/60-fe-uiux-polishing-and-integration
feat: 전시 제안 동의서 프론트 연동 개선
2 parents 4359b88 + 96e8f50 commit 0e9f3ca

82 files changed

Lines changed: 745 additions & 451 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

next.config.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ const backendOrigin = process.env.BACKEND_ORIGIN ?? "http://localhost:8080";
88

99
const nextConfig: NextConfig = {
1010
async rewrites() {
11-
return [
12-
{ source: "/api/:path*", destination: `${backendOrigin}/api/:path*` },
13-
];
11+
return [{ source: "/api/:path*", destination: `${backendOrigin}/api/:path*` }];
1412
},
1513
};
1614

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "refit",
33
"version": "0.1.0",
44
"private": true,
5+
"type": "module",
56
"scripts": {
67
"dev": "next dev",
78
"build": "next build",

src/app/art/[id]/page.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"use client";
22

3+
import { useState } from "react";
4+
35
import { useQuery } from "@tanstack/react-query";
46
import { useParams, useRouter } from "next/navigation";
57

68
import ExpandableText from "@/components/archive-detail/ExpandableText";
7-
import SizeText from "@/components/archive-detail/SizeText";
8-
import RegionText from "@/components/archive-detail/RegionText";
99
import ImageSwiper from "@/components/archive-detail/ImageSwiper";
1010
import NicknameCard from "@/components/archive-detail/NicknameCard";
11+
import RegionText from "@/components/archive-detail/RegionText";
12+
import SizeText from "@/components/archive-detail/SizeText";
1113
import { useCreateChatRoom } from "@/hooks/useCreateChatRoom";
1214
import { getArtworkDetail } from "@/services/artworks";
1315
import { normalizeImageUrl } from "@/utils/normalizeImageUrl";
@@ -21,11 +23,16 @@ function hasText(value?: string | null) {
2123
return Boolean(value?.trim());
2224
}
2325

26+
function getErrorMessage(error: unknown) {
27+
return error instanceof Error ? error.message : "전시 문의를 시작하지 못했습니다.";
28+
}
29+
2430
export default function ArtDetailPage() {
2531
const router = useRouter();
2632
const params = useParams<{ id: string }>();
2733
const artworkId = params.id;
2834
const createChatRoom = useCreateChatRoom();
35+
const [inquiryErrorMessage, setInquiryErrorMessage] = useState<string | null>(null);
2936

3037
const query = useQuery({
3138
queryKey: ["artwork-detail", artworkId],
@@ -49,11 +56,12 @@ export default function ArtDetailPage() {
4956
const handleInquiryClick = () => {
5057
if (!Number.isFinite(numericArtworkId)) return;
5158

59+
setInquiryErrorMessage(null);
5260
createChatRoom.mutate(
5361
{ targetType: "ARTWORK", targetId: numericArtworkId },
5462
{
5563
onSuccess: room => router.push(`/chat/${room.id}`),
56-
onError: error => alert(error.message || "전시 문의를 시작하지 못했습니다."),
64+
onError: error => setInquiryErrorMessage(getErrorMessage(error)),
5765
}
5866
);
5967
};
@@ -136,6 +144,14 @@ export default function ArtDetailPage() {
136144
</div>
137145

138146
<div className="border-border-primary bg-bg-primary fixed right-0 bottom-0 left-0 z-50 border-t px-5 pt-3 pb-9">
147+
{inquiryErrorMessage && (
148+
<p
149+
role="alert"
150+
className="text-caption text-error-default mx-auto mb-2 max-w-[430px]"
151+
>
152+
{inquiryErrorMessage}
153+
</p>
154+
)}
139155
<button
140156
onClick={handleInquiryClick}
141157
disabled={createChatRoom.isPending}

src/app/art/new/page.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import { useState } from "react";
44

5+
import { useMutation } from "@tanstack/react-query";
6+
import Image from "next/image";
7+
import { useRouter } from "next/navigation";
8+
59
import ArtTooltip from "@/components/archive-form/ArtToolTip";
610
import DatePicker from "@/components/archive-form/DayPicker";
711
import Dropdown from "@/components/archive-form/Dropdown";
@@ -14,16 +18,18 @@ import Textarea from "@/components/archive-form/Textarea";
1418
import ToggleButton from "@/components/archive-form/ToggleButton";
1519
import Header from "@/components/common/Header";
1620
import { ART_TYPES } from "@/constants/art";
17-
import { useImageStore } from "@/stores/useImageStore";
1821
import { useUploadImage } from "@/hooks/useImageUploader";
19-
import { useRouter } from "next/navigation";
20-
import { useMutation } from "@tanstack/react-query";
2122
import { createArtwork } from "@/services/artworks";
23+
import { useImageStore } from "@/stores/useImageStore";
2224

2325
function FieldWrapper({ children }: { children: React.ReactNode }) {
2426
return <div className="flex flex-col gap-2">{children}</div>;
2527
}
2628

29+
function getErrorMessage(error: unknown) {
30+
return error instanceof Error ? error.message : "작품 등록에 실패했습니다.";
31+
}
32+
2733
export default function ArtCreatePage() {
2834
const router = useRouter();
2935

@@ -47,6 +53,8 @@ export default function ArtCreatePage() {
4753
const [notes, setNotes] = useState("");
4854

4955
const [isPublic, setIsPublic] = useState(false);
56+
const [isSubmitting, setIsSubmitting] = useState(false);
57+
const [submitErrorMessage, setSubmitErrorMessage] = useState<string | null>(null);
5058

5159
const isFormValid =
5260
images.length > 0 &&
@@ -62,6 +70,11 @@ export default function ArtCreatePage() {
6270
});
6371

6472
const handleSubmit = async () => {
73+
if (isSubmitting) return;
74+
75+
setIsSubmitting(true);
76+
setSubmitErrorMessage(null);
77+
6578
try {
6679
const uploadedImages = await Promise.all(images.map(image => uploadImage(image.file)));
6780

@@ -91,7 +104,9 @@ export default function ArtCreatePage() {
91104
clearImages();
92105
router.push("/");
93106
} catch (error) {
94-
console.error(error);
107+
setSubmitErrorMessage(getErrorMessage(error));
108+
} finally {
109+
setIsSubmitting(false);
95110
}
96111
};
97112
return (
@@ -112,9 +127,11 @@ export default function ArtCreatePage() {
112127
<div className="flex justify-between">
113128
<Label required>작품 유형</Label>
114129
<div className="relative">
115-
<img
130+
<Image
116131
src="/info-icon.svg"
117132
alt="작품 유형"
133+
width={20}
134+
height={20}
118135
className="mr-2 cursor-pointer"
119136
onClick={() => setIsTooltipOpen(!isTooltipOpen)}
120137
/>
@@ -202,16 +219,21 @@ export default function ArtCreatePage() {
202219

203220
{/* Bottom Button () */}
204221
<div className="border-border-primary fixed right-0 bottom-4 left-0 h-24.5 border-t bg-white px-5 py-4">
222+
{submitErrorMessage && (
223+
<p role="alert" className="text-caption text-error-default mb-2">
224+
{submitErrorMessage}
225+
</p>
226+
)}
205227
<button
206-
disabled={!isFormValid}
228+
disabled={!isFormValid || isSubmitting}
207229
onClick={handleSubmit}
208230
className={`text-body-1 h-12.5 w-full rounded-lg font-medium transition-colors ${
209-
isFormValid
231+
isFormValid && !isSubmitting
210232
? "bg-object-primary text-text-invert cursor-pointer"
211233
: "bg-object-disabled text-text-disabled cursor-not-allowed"
212234
}`}
213235
>
214-
추가하기
236+
{isSubmitting ? "등록 중..." : "추가하기"}
215237
</button>
216238
</div>
217239
</main>

src/app/auth/callback/page.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"use client";
22

33
import { useEffect, useRef, useState } from "react";
4+
45
import { useRouter } from "next/navigation";
56

6-
import { exchangeToken } from "@/services/authApi";
77
import { ApiError } from "@/services/apiClient";
8+
import { exchangeToken } from "@/services/authApi";
89
import { useAuthStore } from "@/stores/useAuthStore";
910

1011
interface Guide {
@@ -14,33 +15,29 @@ interface Guide {
1415

1516
export default function AuthCallbackPage() {
1617
const router = useRouter();
17-
const setAuth = useAuthStore((s) => s.setAuth);
18+
const setAuth = useAuthStore(s => s.setAuth);
1819
const [guide, setGuide] = useState<Guide | null>(null);
1920
const handled = useRef(false);
2021

2122
useEffect(() => {
2223
if (handled.current) return; // resultKey는 1회용 — 이중 실행 방지
2324
handled.current = true;
2425

25-
const resultKey = new URLSearchParams(window.location.search).get(
26-
"resultKey",
27-
);
26+
const resultKey = new URLSearchParams(window.location.search).get("resultKey");
2827
if (!resultKey) {
2928
router.replace("/auth/error?error.code=MISSING_RESULT_KEY");
3029
return;
3130
}
3231

3332
exchangeToken(resultKey)
34-
.then((res) => {
33+
.then(res => {
3534
switch (res.loginStatus) {
3635
case "LOGIN_SUCCESS":
37-
if (res.accessToken)
38-
setAuth({ accessToken: res.accessToken, userId: res.userId });
36+
if (res.accessToken) setAuth({ accessToken: res.accessToken, userId: res.userId });
3937
router.replace("/");
4038
break;
4139
case "SIGNUP_REQUIRED":
42-
if (res.accessToken)
43-
setAuth({ accessToken: res.accessToken, userId: res.userId });
40+
if (res.accessToken) setAuth({ accessToken: res.accessToken, userId: res.userId });
4441
router.replace("/auth/signup/profile");
4542
break;
4643
case "LOGIN_METHOD_GUIDE":
@@ -55,7 +52,7 @@ export default function AuthCallbackPage() {
5552
router.replace("/auth/error?error.code=UNKNOWN_LOGIN_STATUS");
5653
}
5754
})
58-
.catch((err) => {
55+
.catch(err => {
5956
const code = err instanceof ApiError ? err.code : "UNKNOWN_ERROR";
6057
router.replace(`/auth/error?error.code=${encodeURIComponent(code)}`);
6158
});
@@ -73,7 +70,7 @@ export default function AuthCallbackPage() {
7370
<button
7471
type="button"
7572
onClick={() => router.replace("/auth")}
76-
className="rounded-sm bg-object-primary px-4 py-2 text-label font-medium text-text-invert"
73+
className="bg-object-primary text-label text-text-invert rounded-sm px-4 py-2 font-medium"
7774
>
7875
로그인으로 돌아가기
7976
</button>

src/app/auth/error/page.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useState } from "react";
4+
45
import { useRouter } from "next/navigation";
56

67
const ERROR_MESSAGES: Record<string, string> = {
@@ -10,24 +11,26 @@ const ERROR_MESSAGES: Record<string, string> = {
1011
MISSING_RESULT_KEY: "비정상적인 접근이에요.",
1112
};
1213

14+
function getInitialErrorMessage() {
15+
if (typeof window === "undefined") return "로그인에 실패했어요.";
16+
17+
// error.message(쿼리 값)는 신뢰할 수 없어 표시하지 않는다.
18+
// 매핑된 code만 안내하고, 빈/미매핑 code는 일반 메시지로 폴백(|| 로 빈 문자열도 처리).
19+
const code = new URLSearchParams(window.location.search).get("error.code");
20+
return (code && ERROR_MESSAGES[code]) || "로그인에 실패했어요.";
21+
}
22+
1323
export default function AuthErrorPage() {
1424
const router = useRouter();
15-
const [message, setMessage] = useState("로그인에 실패했어요.");
16-
17-
useEffect(() => {
18-
// error.message(쿼리 값)는 신뢰할 수 없어 표시하지 않는다.
19-
// 매핑된 code만 안내하고, 빈/미매핑 code는 일반 메시지로 폴백(|| 로 빈 문자열도 처리).
20-
const code = new URLSearchParams(window.location.search).get("error.code");
21-
setMessage((code && ERROR_MESSAGES[code]) || "로그인에 실패했어요.");
22-
}, []);
25+
const [message] = useState(getInitialErrorMessage);
2326

2427
return (
2528
<div className="flex min-h-dvh flex-col items-center justify-center gap-4 px-5 text-center">
2629
<div className="text-body-1 text-text-primary">{message}</div>
2730
<button
2831
type="button"
2932
onClick={() => router.replace("/auth")}
30-
className="rounded-sm bg-object-primary px-4 py-2 text-label font-medium text-text-invert"
33+
className="bg-object-primary text-label text-text-invert rounded-sm px-4 py-2 font-medium"
3134
>
3235
로그인으로 돌아가기
3336
</button>

src/app/chat/[roomId]/loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default function ChatRoomLoading() {
22
return (
3-
<div className="flex h-screen flex-col items-center justify-center bg-bg-primary">
3+
<div className="bg-bg-primary flex h-screen flex-col items-center justify-center">
44
<div className="text-body-1 text-text-secondary">채팅방을 불러오는 중...</div>
55
</div>
66
);

src/app/chat/[roomId]/page.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"use client";
22

33
import { use, useEffect, useMemo } from "react";
4+
45
import ChatHeader from "@/components/chat/ChatHeader";
56
import ChatInput from "@/components/chat/ChatInput";
67
import ChatRoomInfo from "@/components/chat/ChatRoomInfo";
78
import MessageList from "@/components/chat/MessageList";
89
import { useChatRoom } from "@/hooks/useChatRoom";
910
import { useChatRooms } from "@/hooks/useChatRooms";
10-
import { useMessages } from "@/hooks/useMessages";
1111
import { useChatSocket } from "@/hooks/useChatSocket";
12+
import { useMessages } from "@/hooks/useMessages";
1213
import { useSession } from "@/services/session";
1314

1415
interface ChatRoomPageProps {
@@ -26,17 +27,16 @@ export default function ChatRoomPage({ params }: ChatRoomPageProps) {
2627
const { data: roomDetail } = useChatRoom(id);
2728
const { data: roomsData } = useChatRooms();
2829
const roomListItem = useMemo(
29-
() => roomsData?.pages.flatMap((page) => page.items).find((r) => r.id === id),
30-
[roomsData, id],
30+
() => roomsData?.pages.flatMap(page => page.items).find(r => r.id === id),
31+
[roomsData, id]
3132
);
3233

3334
const roomContext = roomDetail?.context ?? roomListItem?.context ?? null;
3435

3536
// 상대 닉네임: 상세엔 counterparty가 없어 artist/host 중 내가 아닌 쪽으로 계산, 목록은 counterparty 직접 사용.
3637
const counterpartyNickname = useMemo(() => {
3738
if (roomDetail) {
38-
const counterpart =
39-
roomDetail.artist.id === myUserId ? roomDetail.host : roomDetail.artist;
39+
const counterpart = roomDetail.artist.id === myUserId ? roomDetail.host : roomDetail.artist;
4040
return counterpart.nickname;
4141
}
4242
return roomListItem?.counterparty.nickname ?? null;
@@ -53,8 +53,8 @@ export default function ChatRoomPage({ params }: ChatRoomPageProps) {
5353
const { sendMessage, markAsRead, lastError, isConnected } = useChatSocket(id);
5454

5555
const messages = useMemo(
56-
() => messagesData?.pages.flatMap((page) => page.items) ?? [],
57-
[messagesData],
56+
() => messagesData?.pages.flatMap(page => page.items) ?? [],
57+
[messagesData]
5858
);
5959

6060
// 읽음 처리는 연결 완료 후에만 발행(stompjs v7은 미연결 시 publish()가 동기 throw).
@@ -65,25 +65,23 @@ export default function ChatRoomPage({ params }: ChatRoomPageProps) {
6565
}, [id, isConnected]);
6666

6767
return (
68-
<div className="flex h-screen flex-col bg-bg-primary">
68+
<div className="bg-bg-primary flex h-screen flex-col">
6969
<ChatHeader title={counterpartyNickname} />
7070

71-
{roomContext && <ChatRoomInfo context={roomContext} />}
71+
{roomContext && <ChatRoomInfo roomId={id} context={roomContext} />}
7272

7373
{lastError && (
7474
<div
7575
role="alert"
76-
className="bg-error-light px-4 py-2 text-label font-medium text-error-default"
76+
className="bg-error-light text-label text-error-default px-4 py-2 font-medium"
7777
>
7878
{lastError.error.message}
7979
</div>
8080
)}
8181

8282
{isLoading ? (
8383
<div className="flex flex-1 items-center justify-center">
84-
<div className="text-body-1 text-text-secondary">
85-
메시지를 불러오는 중...
86-
</div>
84+
<div className="text-body-1 text-text-secondary">메시지를 불러오는 중...</div>
8785
</div>
8886
) : (
8987
<MessageList

0 commit comments

Comments
 (0)