Skip to content
4 changes: 2 additions & 2 deletions src/features/DetailMaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export default function RequestDetailDreamer() {
{`
@media (min-width: 744px) and (max-width: 2700px) {
.main-container {
padding: 0 24px;
padding: 0 60px;
}
}
`}
Expand Down Expand Up @@ -458,7 +458,7 @@ export default function RequestDetailDreamer() {
<ModalLayout label="지정 플랜 요청하기" closeModal={() => setIsListModalOpen(false)}>
<div className="flex flex-col items-center gap-8">
{pendingPlanTitles.length > 0 ? (
<div className="flex max-h-80 w-full flex-col gap-8 overflow-y-auto">
<div className="flex max-h-80 w-full flex-col gap-6 overflow-y-auto">
{pendingPlans.map((plan) => (
<>
<div
Expand Down
9 changes: 1 addition & 8 deletions src/features/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,12 @@ export default function LoginForm() {
);
router.replace("/");
router.reload();

} catch (error) {
console.error("유저 정보 가져오기 실패", error);
}
},
onError: (error: any) => {
if (error.response) {
alert(error.message);
} else if (error.request) {
console.error(error.request);
} else {
console.error(error.message);
}
alert(error.message);
},
});

Expand Down
15 changes: 13 additions & 2 deletions src/features/ProfileDreamer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function ProfileDreamer() {
const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [isOpenImageModal, setIsOpenImageModal] = useState(false);
const [profileImg, setProfileImg] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const router = useRouter();

Expand Down Expand Up @@ -50,9 +51,15 @@ export default function ProfileDreamer() {
onError: (error: any) => {
alert(error.message);
},
onSettled: () => {
setIsSubmitting(false);
},
});

const handleSubmit = async () => {
if (isSubmitting) return;

setIsSubmitting(true);
const profileData = {
image: profileImg || undefined,
tripTypes: selectedServices,
Expand All @@ -68,7 +75,11 @@ export default function ProfileDreamer() {
};

const isButtonDisabled =
selectedServices.length === 0 || selectedLocations.length === 0 || !profileImg || !userData;
selectedServices.length === 0 ||
selectedLocations.length === 0 ||
!profileImg ||
!userData ||
isSubmitting;

return (
<div className="mb-20 flex justify-center">
Expand Down Expand Up @@ -136,7 +147,7 @@ export default function ProfileDreamer() {
</div>
</div>
<Button
label="시작하기"
label={isSubmitting ? "처리중..." : "시작하기"}
onClick={handleSubmit}
disabled={isButtonDisabled}
type="submit"
Expand Down
17 changes: 10 additions & 7 deletions src/services/apiClient.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios from "axios";
import { getAccessToken, setAccessToken } from "@/utils/tokenUtils";
import { getAccessToken, removeAccessToken } from "@/utils/tokenUtils";
import authService from "./authService";
import router from "next/router";
import useAuthStore from "@/stores/useAuthStore";

const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
Expand Down Expand Up @@ -29,14 +31,15 @@ apiClient.interceptors.response.use(
async (error) => {
if (error.response && error.response?.status === 401) {
try {
const response = await authService.refreshToken();
const accessToken = response;
setAccessToken(accessToken);

error.config.headers["Authorization"] = `Bearer ${accessToken}`;
const newAccessToken = await authService.refreshToken();
alert("새 토큰 발급!");
error.config.headers["Authorization"] = `Bearer ${newAccessToken}`;
return apiClient(error.config);
} catch (error: any) {
alert(error.message);
console.error("토큰 갱신 실패", error);
removeAccessToken();
router.push("/login");
alert("새로 로그인해주세요!");
return Promise.reject(error);
}
}
Expand Down
20 changes: 12 additions & 8 deletions src/services/authService.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const authService = {
return response;
} catch (error: any) {
if (error.response && error.response.status === BAD_REQUEST) {
throw new Error("유저 정보가 일치하지 않습니다.");
throw new Error("이메일과 비밀번호를 확인해주세요.");
}
throw new Error("로그인 중 오류가 발생했습니다.");
}
Expand Down Expand Up @@ -103,15 +103,19 @@ const authService = {
throw new Error("네이버 로그인에 실패했습니다.");
}
},
refreshToken: async (): Promise<string> => {
refreshToken: async () => {
try {
const response: RefreshTokenResponse = await api.post("/auth/refresh/token", true);
return response.accessToken;
} catch (error: any) {
if (error.response && error.response.status === UNAUTHORIZED) {
throw new Error("리프레시 토큰이 없거나 만료되었습니다.");
const response: RefreshTokenResponse = await api.post("/auth/refresh/token", true); //withCrediential
const newAccessToken = response.accessToken;

if (!newAccessToken) {
throw new Error("서버에서 새로운 accessToken을 받지 못했습니다.");
}
throw new Error("토큰 발급 중 오류가 발생했습니다.");

return newAccessToken;
} catch (error: any) {
console.error("토큰 갱신 실패", error);
throw error;
}
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/services/planService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const planService = {

getPendingPlan: async () => {
try {
const response = await api.get<PlanResponse, {}>(`/plans/dreamer?status=PENDING`);
const response = await api.get<PlanResponse, {}>(`/plans/dreamer?status=PENDING&pageSize=99`);
return response.list;
} catch (error) {
console.error("지정 플랜 조회 실패", error);
Expand Down
26 changes: 19 additions & 7 deletions src/stores/useRealTimeNotification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ const useRealTimeNotification = () => {
const [realTimeNotifications, setRealTimeNotifications] = useState<
{ id: string; content: string; timestamp: number }[]
>([]);
const [eventSource, setEventSource] = useState<EventSourcePolyfill | null>(null);

useEffect(() => {
const connectToSSE = () => {
const accessToken = getAccessToken();
const eventSource = new EventSourcePolyfill(
const newEventSource = new EventSourcePolyfill(
`${process.env.NEXT_PUBLIC_API_URL}/notifications/stream`,
{
headers: { Authorization: `Bearer ${accessToken}` },
},
);

eventSource.onopen = () => console.log("SSE 연결 ON ✅");
newEventSource.onopen = () => console.log("SSE 연결 ON ✅");

eventSource.onerror = (err) => {
newEventSource.onerror = (err) => {
console.error("SSE 연결 ERROR ❌", err);
eventSource.close();
newEventSource.close();

setTimeout(() => {
console.log("♻️ SSE 재연결 시도...");
connectToSSE();
}, 5000);
};

eventSource.onmessage = (event) => {
newEventSource.onmessage = (event) => {
const notification = event.data;

const newNotificationObject = {
Expand All @@ -44,8 +50,14 @@ const useRealTimeNotification = () => {
}, 5000);
};

setEventSource(newEventSource);
};

useEffect(() => {
connectToSSE();

return () => {
eventSource.close();
eventSource?.close();
};
}, []);

Expand Down