-
{calendarOwner || "My Calendar"}
+ {calendarOwner &&
{calendarOwner}
}
= ({
type: "timeGrid",
duration: { days: 3 },
},
+ dayGridMonth: {
+ type: "dayGridMonth",
+ dayHeaderFormat: { weekday: "short" },
+ },
}}
initialView={isMobile ? VIEW_MODES.THREEDAY : VIEW_MODES.WEEK}
initialDate={currentDate}
headerToolbar={{
left: "title",
center: "",
- right: "prev,next,today",
+ right:
+ "prev,next,today dayGridMonth,timeGridWeek,timeGridDay,timeGridThreeDay",
}}
locale={koLocale}
- slotDuration="00:30:00"
+ slotDuration="00:10:00"
slotLabelInterval="01:00:00"
slotLabelFormat={{
hour: "2-digit",
@@ -334,15 +396,15 @@ const CustomCalendar: React.FC = ({
eventResizableFromStart={!isReadOnly}
eventDrop={isReadOnly ? undefined : handleEventChange}
eventResize={handleEventChange}
- eventContent={(eventInfo) =>
- renderEventContent(eventInfo, handleDelete, handleEdit, isReadOnly)
- }
+ eventContent={eventContent}
selectable={false}
selectMirror={false}
dayMaxEvents
weekends
firstDay={1}
+ timeZone="UTC"
events={parsedEvents}
+ viewDidMount={({ view }) => setCurrentView(view.type)}
datesSet={(dateInfo) => setCurrentDate(dateInfo.start)}
dayHeaderFormat={{
weekday: "short",
@@ -352,29 +414,56 @@ const CustomCalendar: React.FC = ({
}}
height={isMobile ? "85%" : "100%"}
/>
- {/* Edit Modal */}
{isEditModalOpen && currentEditPlan && (
setIsEditModalOpen(false)}>
- 플랜 수정
-
- setCurrentEditPlan((prev) =>
- prev ? { ...prev, title: e.target.value } : prev,
- )
- }
- />
-
- setCurrentEditPlan((prev) =>
- prev ? { ...prev, description: e.target.value } : prev,
- )
- }
- />
-
+
+ 플랜 수정
+
+ setCurrentEditPlan((prev) =>
+ prev ? { ...prev, title: e.target.value } : prev,
+ )
+ }
+ />
+
+ setCurrentEditPlan((prev) =>
+ prev ? { ...prev, description: e.target.value } : prev,
+ )
+ }
+ />
+
+ 공개 여부
+
+ setCurrentEditPlan((prev) => ({
+ ...prev,
+ accessibility: e.target.checked,
+ }))
+ }
+ />
+
+
+ 완료 여부
+
+ setCurrentEditPlan((prev) => ({
+ ...prev,
+ complete: e.target.checked,
+ }))
+ }
+ />
+
+
+
)}
diff --git a/src/components/features/CustomCalendar/PlanModal.tsx b/src/components/features/CustomCalendar/PlanModal.tsx
deleted file mode 100644
index f8f7fae..0000000
--- a/src/components/features/CustomCalendar/PlanModal.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { ReactNode } from "react";
-import styled from "@emotion/styled";
-
-interface ModalProps {
- onClose: () => void;
- children: ReactNode;
-}
-
-const Overlay = styled.div`
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
-`;
-
-const ModalContainer = styled.div`
- background: white;
- padding: 20px;
- border-radius: 8px;
- width: 90%;
- max-width: 500px;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
-`;
-
-const ModalHeader = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
-`;
-
-const CloseButton = styled.button`
- background: none;
- border: none;
- font-size: 1.5rem;
- cursor: pointer;
- color: #333;
-`;
-
-const ModalTitle = styled.h2`
- font-size: 1.25rem;
- font-weight: bold;
-`;
-
-const ModalContent = styled.div`
- margin-top: 10px;
- display: flex;
- flex-direction: column;
- gap: 10px;
-`;
-
-const Modal: React.FC
= ({ onClose, children }) => {
- return (
-
- e.stopPropagation()}>
-
- 플랜 수정
- ×
-
- {children}
-
-
- );
-};
-
-export default Modal;
diff --git a/src/components/features/DatePicker/DatePicker.css b/src/components/features/DatePicker/DatePicker.css
new file mode 100644
index 0000000..7711efd
--- /dev/null
+++ b/src/components/features/DatePicker/DatePicker.css
@@ -0,0 +1,43 @@
+.custom-datepicker-wrapper {
+ position: relative;
+ z-index: 100;
+}
+
+.custom-calendar {
+ border: 1px solid #ddd !important;
+ font-family: -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+/* 시간 선택 컨테이너 스타일 */
+.react-datepicker__time-container {
+ border-left: 1px solid #ddd !important;
+ width: 100px !important;
+ display: block !important;
+}
+
+.react-datepicker {
+ display: flex !important;
+}
+
+.react-datepicker__time-box {
+ width: 100px !important;
+ margin: 0 !important;
+}
+
+/* 시간 리스트 스타일 */
+.react-datepicker__time-list {
+ height: 200px !important;
+ overflow-y: scroll !important;
+ width: 100% !important;
+}
+
+.react-datepicker__time-list-item {
+ padding: 5px 10px !important;
+ height: auto !important;
+}
+
+/* 선택된 시간 스타일 */
+.react-datepicker__time-list-item--selected {
+ background-color: #216ba5 !important;
+ color: white !important;
+}
\ No newline at end of file
diff --git a/src/components/features/DatePicker/DatePicker.tsx b/src/components/features/DatePicker/DatePicker.tsx
new file mode 100644
index 0000000..3577c96
--- /dev/null
+++ b/src/components/features/DatePicker/DatePicker.tsx
@@ -0,0 +1,180 @@
+import { forwardRef, InputHTMLAttributes } from "react";
+import styled from "@emotion/styled";
+import DatePicker from "react-datepicker";
+import { Global, css } from "@emotion/react";
+import "react-datepicker/dist/react-datepicker.css";
+
+// StyledInput 스타일 정의
+const StyledInput = styled.input`
+ width: 100%;
+ padding: 12px;
+ margin-bottom: 10px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ font-size: 1rem;
+ &:focus {
+ outline: none;
+ border-color: #6c63ff;
+ box-shadow: 0 0 0 2px rgba(108, 99, 255, 0.3);
+ }
+`;
+
+// 커스텀 입력 컴포넌트 타입 정의
+interface CustomDateInputProps extends InputHTMLAttributes {
+ onClick?: () => void;
+ value?: string;
+}
+
+// DatePicker의 커스텀 입력 컴포넌트
+const CustomDateInput = forwardRef(
+ ({ value, onClick, placeholder }, ref) => (
+
+ ),
+);
+
+interface ReactDatePickerProps {
+ placeholderText: string;
+ onDateChange: (date: Date | null) => void;
+ selectedDate?: Date | null;
+ showTimeSelect?: boolean; // 시간 선택 기능
+ dateFormat?: string; // 날짜 포맷 설정
+}
+
+const ReactDatePicker = ({
+ placeholderText,
+ onDateChange,
+ selectedDate,
+ showTimeSelect = false,
+ dateFormat = "yyyy/MM/dd",
+}: ReactDatePickerProps) => {
+ const handleDateChange = (date: Date | null) => {
+ if (date) {
+ const utcDate = new Date(
+ date.getTime() - date.getTimezoneOffset() * 60000,
+ );
+ onDateChange(utcDate);
+ } else {
+ onDateChange(null);
+ }
+ };
+
+ return (
+ <>
+
+ }
+ />
+ >
+ );
+};
+
+export default ReactDatePicker;
diff --git a/src/components/features/Introduce/Introduce.tsx b/src/components/features/Introduce/Introduce.tsx
index 14ff031..60fb2a1 100644
--- a/src/components/features/Introduce/Introduce.tsx
+++ b/src/components/features/Introduce/Introduce.tsx
@@ -6,6 +6,7 @@ import effectSVG from "@/assets/effect.svg";
import Button from "@/components/common/Button/Button";
import breakpoints from "@/variants/breakpoints";
import kakao_symbol from "@/assets/kakao_symbol.svg";
+import RouterPath from "@/router/RouterPath";
const LandingContainer = styled.div`
max-width: 1280px;
@@ -130,11 +131,13 @@ const Introduce = () => {
const navigate = useNavigate();
const handleStartClick = () => {
- navigate("/plan/preview");
+ navigate(RouterPath.PREVIEW_PLAN);
};
+ const loginUrl = import.meta.env.VITE_LOGIN_URL;
+
const handleLoginClick = () => {
- navigate("/login");
+ window.location.href = loginUrl;
};
return (
diff --git a/src/components/features/Layout/Layout.tsx b/src/components/features/Layout/Layout.tsx
index d648a62..d98660e 100644
--- a/src/components/features/Layout/Layout.tsx
+++ b/src/components/features/Layout/Layout.tsx
@@ -4,10 +4,10 @@ import Sidebar from "@/components/common/Sidebar/Sidebar";
import breakpoints from "@/variants/breakpoints";
const Wrapper = styled.div`
- flex-direction: row; /* 모바일 이상에서는 가로 정렬 */
-
+ flex-direction: row;
+ display: flex;
+ width: 100%;
${breakpoints.mobile} {
- /* 기본값은 세로 정렬 (모바일) */
flex-direction: column;
display: flex;
width: 100%;
@@ -17,8 +17,9 @@ const Wrapper = styled.div`
const ContentWrapper = styled.div`
padding-left: 225px;
- padding-top: 60px;
-
+ box-sizing: border-box;
+ width: 100%;
+ overflow-x: hidden;
${breakpoints.mobile} {
flex-grow: 1;
padding: 60px 20px;
diff --git a/src/components/features/MicrophoneButton/MicrophoneButton.styles.ts b/src/components/features/MicrophoneButton/MicrophoneButton.styles.ts
index c14a0bd..263fa5b 100644
--- a/src/components/features/MicrophoneButton/MicrophoneButton.styles.ts
+++ b/src/components/features/MicrophoneButton/MicrophoneButton.styles.ts
@@ -1,63 +1,61 @@
+// src/components/features/MicrophoneButton/MicrophoneButton.styles.ts
import styled from "@emotion/styled";
import { motion } from "framer-motion";
import breakpoints from "@/variants/breakpoints";
-export const ButtonContainer = styled(motion.button)({
- border: "none",
- background: "none",
- cursor: "pointer",
- position: "relative",
- overflow: "visible",
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- outline: "none",
- padding: 0,
-
- "&:focus": {
- outline: "none",
- },
-
- // 반응형 설정
- width: "58px",
- height: "58px",
-
- [breakpoints.tablet]: {
- width: "50px",
- height: "50px",
- },
-});
-
-export const Circle = styled(motion.ellipse)({
- fill: "#39a7f7",
- filter: "drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.2))",
-});
-
-export const MicrophoneIcon = styled(motion.g)({
- fill: "white",
-});
-
-export const WaveContainer = styled(motion.div)({
- position: "absolute",
- top: 0,
- left: 0,
- width: "100%",
- height: "100%",
- display: "flex",
- justifyContent: "space-around",
- alignItems: "center",
-});
-
-export const Wave = styled(motion.div)({
- backgroundColor: "white",
- borderRadius: "5.625px",
-
- // 반응형 설정
- width: "4px",
- height: "4px",
-
- [breakpoints.tablet]: {
- width: "6px",
- height: "6px",
- },
-});
+export const ButtonContainer = styled(motion.button)`
+ border: none;
+ background: none;
+ cursor: pointer;
+ position: relative;
+ overflow: visible;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ outline: none;
+
+ width: 58px;
+ height: 58px;
+
+ @media (max-width: ${breakpoints.sm}px) {
+ width: 50px;
+ height: 50px;
+ }
+
+ &:focus {
+ outline: none;
+ }
+`;
+
+export const Circle = styled(motion.ellipse)`
+ fill: #39a7f7;
+ filter: drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.2));
+`;
+
+export const MicrophoneIcon = styled(motion.g)`
+ fill: white;
+`;
+
+export const WaveContainer = styled(motion.div)`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+`;
+
+export const Wave = styled(motion.div)`
+ background-color: white;
+ border-radius: 5px;
+ width: 4px;
+ height: 8px;
+
+ @media (max-width: ${breakpoints.sm}px) {
+ width: 3px;
+ height: 6px;
+ }
+`;
diff --git a/src/components/features/MicrophoneButton/MicrophoneButton.tsx b/src/components/features/MicrophoneButton/MicrophoneButton.tsx
index 932c50a..13a98dc 100644
--- a/src/components/features/MicrophoneButton/MicrophoneButton.tsx
+++ b/src/components/features/MicrophoneButton/MicrophoneButton.tsx
@@ -1,3 +1,5 @@
+// src/components/features/MicrophoneButton/MicrophoneButton.tsx
+import React from "react";
import { AnimatePresence } from "framer-motion";
import {
ButtonContainer,
@@ -8,9 +10,9 @@ import {
} from "./MicrophoneButton.styles";
export interface MicrophoneButtonProps {
- onStartClick?: () => void; // 녹음 시작 콜백
- onStopClick?: () => void; // 녹음 중지 콜백
- isRecording: boolean; // 추가된 부분
+ onStartClick?: () => void;
+ onStopClick?: () => void;
+ isRecording: boolean;
}
const MicrophoneButton: React.FC = ({
@@ -35,6 +37,7 @@ const MicrophoneButton: React.FC = ({
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
+ {/* 마이크 아이콘 경로 */}
diff --git a/src/components/features/ProtectedRoute/ProtectedRoute.tsx b/src/components/features/ProtectedRoute/ProtectedRoute.tsx
new file mode 100644
index 0000000..9dbcfba
--- /dev/null
+++ b/src/components/features/ProtectedRoute/ProtectedRoute.tsx
@@ -0,0 +1,9 @@
+import { Outlet } from "react-router-dom";
+import useAuth from "@/hooks/useAuth";
+
+const ProtectedRoute = () => {
+ const { authState } = useAuth();
+ return authState.isAuthenticated ? : null;
+};
+
+export default ProtectedRoute;
diff --git a/src/context/LoginModalContext.tsx b/src/context/LoginModalContext.tsx
new file mode 100644
index 0000000..f7f440d
--- /dev/null
+++ b/src/context/LoginModalContext.tsx
@@ -0,0 +1,34 @@
+import React, { createContext, useContext, useState, ReactNode } from "react";
+
+interface ModalContextType {
+ isLoginModalOpen: boolean;
+ openLoginModal: () => void;
+ closeLoginModal: () => void;
+}
+
+const ModalContext = createContext(undefined);
+
+export const ModalProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const [isLoginModalOpen, setLoginModalOpen] = useState(false);
+
+ const openLoginModal = () => setLoginModalOpen(true);
+ const closeLoginModal = () => setLoginModalOpen(false);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useModal = () => {
+ const context = useContext(ModalContext);
+ if (!context) {
+ throw new Error("useModal must be used within a ModalProvider");
+ }
+ return context;
+};
diff --git a/src/index.css b/src/index.css
index 01bd549..bc1cb08 100644
--- a/src/index.css
+++ b/src/index.css
@@ -18,13 +18,14 @@
html, body {
margin: 0;
padding: 0;
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
justify-content: center;
align-items: center;
position: relative;
top: 0;
left: 0;
box-sizing: border-box;
-}
-
+ font-family: 'Noto Sans KR', 'Roboto', sans-serif;
+
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index af59181..ad55e7a 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -19,13 +19,14 @@ import "./index.css";
// });
if ("serviceWorker" in navigator) {
- navigator.serviceWorker.getRegistrations().then((registrations) => {
- registrations.forEach((registration) => {
- if (registration.active && registration.scope.includes("mock")) {
- registration.unregister(); // 서비스 워커 해제
- }
+ navigator.serviceWorker
+ .register("/firebase-messaging-sw.js")
+ .then((registration) => {
+ console.log("Service Worker 등록 성공:", registration);
+ })
+ .catch((error) => {
+ console.error("Service Worker 등록 실패:", error);
});
- });
}
ReactDOM.createRoot(document.getElementById("root")!).render(
diff --git a/src/pages/Friend/FriendDetailPage.tsx b/src/pages/Friend/FriendDetailPage.tsx
index fb4989c..3caef98 100644
--- a/src/pages/Friend/FriendDetailPage.tsx
+++ b/src/pages/Friend/FriendDetailPage.tsx
@@ -1,3 +1,4 @@
+// src/pages/Friend/FriendDetailPage.tsx
import { useState } from "react";
import styled from "@emotion/styled";
import { useLocation, useParams } from "react-router-dom";
@@ -13,123 +14,131 @@ import {
import { useGetFriendPlans } from "@/api/hooks/useGetPlans";
const PageContainer = styled.div`
- width: 100%;
- margin: 0 auto;
- padding: 16px;
+ display: flex;
+ min-height: 100vh;
+ background-color: #ffffff;
+`;
+
+const ContentWrapper = styled.main`
+ flex-grow: 1;
+ padding: 32px;
+ overflow: auto;
+ box-sizing: border-box;
+`;
+
+const Heading = styled.h1`
+ font-size: 24px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ color: #2d3748;
+`;
+
+const CalendarWrapper = styled.div`
+ margin-bottom: 32px;
`;
const CommentSection = styled.div`
- display: flex;
- flex-direction: column;
+ padding: 24px;
+ border-radius: 8px;
`;
const CommentInput = styled.div`
display: flex;
align-items: center;
- gap: 24px;
- padding: 8px;
+ margin-bottom: 24px;
+ align-items: flex-start;
+ gap: 10px;
+ background-color: #ffffff;
+ padding: 16px 0px 16px 16px;
`;
const InputWrapper = styled.div`
- flex: 1;
+ flex-grow: 1;
display: flex;
align-items: center;
- padding: 10.4px 14.4px;
- border-radius: 12.8px;
- border: 1px solid black;
+ padding: 10px 14px;
+ border-radius: 12px;
+ border: 1px solid #cbd5e0;
+ background-color: #ffffff;
`;
const Input = styled.input`
flex: 1;
border: none;
outline: none;
- font-size: 15.3px;
- font-family: "Inter", sans-serif;
- font-weight: 700;
+ font-size: 15px;
color: #464646;
&::placeholder {
color: rgba(70, 70, 70, 0.5);
}
`;
-const Divider = styled.hr`
+const IconButton = styled.button`
+ background: none;
border: none;
- height: 1.6px;
- background-color: #eeeeee;
- margin: 8px 0;
+ cursor: pointer;
+ color: #39a7f7;
+ display: flex;
+ align-items: center;
+ padding: 4px;
+ margin-left: 2px;
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ }
`;
-const CommentItem = styled.div`
+const CommentList = styled.div`
display: flex;
flex-direction: column;
- padding: 8px;
+ gap: 16px;
`;
-const CommentContent = styled.div`
+const CommentItem = styled.div`
display: flex;
align-items: flex-start;
- gap: 24px;
-`;
-
-const CommentBubble = styled.div`
- background-color: #d9d9d9;
- border-radius: 12.8px;
- padding: 8px 16.8px;
- display: flex;
- flex-direction: column;
- gap: 8px;
+ gap: 16px;
+ background-color: #ffffff;
+ padding: 16px;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
`;
-const CommentBox = styled.div`
- border-radius: 12.8px;
+const CommentContent = styled.div`
+ flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
`;
const CommentAuthor = styled.div`
- color: black;
- font-size: 15.3px;
- font-family: "Inter", sans-serif;
- font-weight: 700;
+ font-size: 16px;
+ font-weight: 600;
+ color: #2d3748;
`;
const CommentText = styled.div`
+ font-size: 15px;
color: #464646;
- font-size: 15.3px;
- font-family: "Inter", sans-serif;
- font-weight: 700;
+ margin-top: 4px;
`;
const CommentDate = styled.div`
- display: flex;
- align-items: center;
- color: rgba(55.95, 55.95, 55.95, 0.7);
- font-size: 15.3px;
- font-family: "Inter", sans-serif;
- font-weight: 700;
- margin-top: 4px;
- padding-left: 12px;
+ font-size: 14px;
+ color: #718096;
`;
-const ActionButtons = styled.div`
+const CommentDateWrapper = styled.div`
display: flex;
align-items: center;
- margin-left: 3px;
+ justify-content: space-between;
+ margin-top: 8px;
`;
-const IconButton = styled.button`
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px;
- color: rgba(55.95, 55.95, 55.95, 0.7);
- font-size: 10px;
+const ActionButtons = styled.div`
display: flex;
- align-items: center;
- &:hover {
- color: #000;
- }
+ margin-right: auto;
+ font-size: 9px;
`;
export default function FriendDetailPage() {
@@ -176,7 +185,7 @@ export default function FriendDetailPage() {
{
onSuccess: () => {
alert("수정이 완료되었습니다.");
- setEditingCommentId(null); // 수정 후 수정 모드 종료
+ setEditingCommentId(null);
setEditContent("");
},
},
@@ -201,98 +210,101 @@ export default function FriendDetailPage() {
return (
-
-
-
-
-
- setNewComment(e.target.value)}
- placeholder="댓글을 입력하세요."
- aria-label="댓글 입력"
- onKeyPress={(e) => {
- if (e.key === "Enter") handleSubmitComment();
- }}
- disabled={createCommentMutation.isPending}
- />
-
-
-
-
-
-
- {comments.map((comment) => (
-
-
-
-
- {editingCommentId === comment.id ? (
-
- setEditContent(e.target.value)}
- onKeyPress={(e) => {
- if (e.key === "Enter") handleUpdateComment(comment.id);
- }}
- disabled={updateCommentMutation.isPending}
- />
- handleUpdateComment(comment.id)}
- disabled={updateCommentMutation.isPending}
- >
-
-
-
- ) : (
- <>
-
- {comment.writerNickname}
+
+ {friendName ? `${friendName}님의 계획표` : "계획표"}
+
+
+
+ 댓글
+
+
+
+
+ setNewComment(e.target.value)}
+ placeholder="댓글을 입력하세요."
+ aria-label="댓글 입력"
+ onKeyPress={(e) => {
+ if (e.key === "Enter") handleSubmitComment();
+ }}
+ disabled={createCommentMutation.isPending}
+ />
+
+
+
+
+
+
+ {comments.map((comment) => (
+
+
+
+ {comment.writerNickname}
+ {editingCommentId === comment.id ? (
+
+ setEditContent(e.target.value)}
+ onKeyPress={(e) => {
+ if (e.key === "Enter")
+ handleUpdateComment(comment.id);
+ }}
+ disabled={updateCommentMutation.isPending}
+ />
+ handleUpdateComment(comment.id)}
+ disabled={updateCommentMutation.isPending}
+ >
+
+
+
+ ) : (
+ <>
{comment.content}
-
-
- {formatDate(comment.createdAt)}
- {comment.writerId === userId && (
-
- {
- setEditingCommentId(comment.id);
- setEditContent(comment.content);
- }}
- disabled={
- updateCommentMutation.isPending ||
- deleteCommentMutation.isPending
- }
- >
-
-
- handleDeleteComment(comment.id)}
- disabled={
- updateCommentMutation.isPending ||
- deleteCommentMutation.isPending
- }
- >
-
-
-
- )}
-
- >
- )}
-
-
-
- ))}
-
+
+
+ {formatDate(comment.createdAt)}
+
+ {comment.writerId === userId && (
+
+ {
+ setEditingCommentId(comment.id);
+ setEditContent(comment.content);
+ }}
+ disabled={
+ updateCommentMutation.isPending ||
+ deleteCommentMutation.isPending
+ }
+ >
+
+
+ handleDeleteComment(comment.id)}
+ disabled={
+ updateCommentMutation.isPending ||
+ deleteCommentMutation.isPending
+ }
+ >
+
+
+
+ )}
+
+ >
+ )}
+
+
+ ))}
+
+
+
);
}
diff --git a/src/pages/Friend/FriendPage.tsx b/src/pages/Friend/FriendPage.tsx
index 51fd87a..0037e7e 100644
--- a/src/pages/Friend/FriendPage.tsx
+++ b/src/pages/Friend/FriendPage.tsx
@@ -1,9 +1,9 @@
-/** @jsxImportSource @emotion/react */
import { useState, useMemo } from "react";
-import { css } from "@emotion/react";
+import styled from "@emotion/styled";
import { useNavigate } from "react-router-dom";
import { Search } from "@mui/icons-material";
-import List from "@/components/common/List/List";
+import ProfileImage from "@/components/common/ProfileImage/ProfileImage";
+import UserInfo from "@/components/common/UserInfo/UserInfo";
import {
useGetFriends,
useGetReceivedRequests,
@@ -17,7 +17,6 @@ import {
useCancelFriendRequest,
} from "@/api/hooks/useFriendRequest";
import useDeleteFriend from "@/api/hooks/useDeleteFriend";
-import breakpoints from "@/variants/breakpoints";
import {
Friend,
SentRequest,
@@ -26,121 +25,341 @@ import {
} from "@/types/types";
import Button from "@/components/common/Button/Button";
import useUserData from "@/api/hooks/useUserData";
+import breakpoints from "@/variants/breakpoints";
-// Styles
-const pageStyles = css`
+// Styled Components
+const PageContainer = styled.div`
width: 100%;
max-width: 1200px;
- height: 100vh;
- padding: 10px 45px;
+ padding: 32px;
display: flex;
flex-direction: column;
align-items: center;
- font-family: "Inter", sans-serif;
box-sizing: border-box;
- @media (max-width: ${breakpoints.sm}px) {
- padding-top: 80px;
- }
+ overflow-x: hidden;
+ background-color: #ffffff;
`;
-const searchBarStyles = css`
+const Heading = styled.h1`
+ font-size: 24px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ color: #2d3748;
+ margin-right: auto;
+`;
+
+const TabsContainer = styled.div`
display: flex;
+ justify-content: flex-end;
+ gap: 28px;
+ width: 100%;
+ margin-bottom: 20px;
+ box-sizing: border-box;
+ flex-wrap: wrap;
+`;
+
+const Tab = styled.div<{ active: boolean }>`
+ font-size: 15px;
+ font-weight: ${(props) => (props.active ? 600 : 400)};
+ color: ${(props) => (props.active ? "#39a7f7" : "#9b9b9b")};
+ cursor: pointer;
+ transition: color 0.3s ease;
+`;
+
+const SearchBarWrapper = styled.div`
+ width: 100%;
+`;
+const SearchBar = styled.div`
+ display: flex;
+ flex: 1;
align-items: center;
padding: 10px;
- background: #f4f4f4;
- border-radius: 16px;
+ border-radius: 8px;
margin-bottom: 20px;
width: 100%;
box-sizing: border-box;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ background-color: #ffffff;
`;
-const searchIconStyles = css`
+const SearchIconStyled = styled(Search)`
color: #aab2c8;
font-size: 20px;
margin-right: 10px;
`;
-const searchInputStyles = css`
+const SearchInput = styled.input`
flex: 1;
border: none;
background: transparent;
font-size: 15.28px;
font-weight: 700;
- color: #aab2c8;
+ color: #4a5568;
outline: none;
&::placeholder {
color: #aab2c8;
}
`;
-const searchButtonStyles = css`
- color: #aab2c8;
+const SearchButton = styled.span`
+ color: #39a7f7;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
`;
-const tabsStyles = css`
- display: flex;
- justify-content: flex-end;
- gap: 28px;
+const FriendListContainer = styled.div`
width: 100%;
- margin-bottom: 20px;
+ display: grid;
+ gap: 16px;
box-sizing: border-box;
-`;
-
-const tabStyles = css`
- font-size: 15px;
- font-weight: 400;
- color: #9b9b9b;
- cursor: pointer;
- transition: color 0.3s ease;
+ grid-template-columns: 1fr;
- &.active {
- color: black;
- font-weight: 600;
+ @media (min-width: ${breakpoints.lg}px) {
+ grid-template-columns: 1fr 1fr;
}
`;
-const friendListStyles = css`
- width: 100%;
+const FriendItemContainer = styled.div`
display: flex;
- flex-direction: column;
- gap: 10px;
+ align-items: center;
+ background-color: #ffffff;
+ border-radius: 8px;
box-sizing: border-box;
+ padding: 16px;
+ gap: 16px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ transition: box-shadow 0.2s;
+ &:hover {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
+ }
`;
-const friendItemStyles = css`
- display: flex;
- align-items: center;
- padding: 10px;
- background: #f4f4f4;
- border-radius: 16px;
- box-sizing: border-box;
- width: 100%;
- flex-wrap: nowrap;
- min-width: 394px;
+const ProfileImageWrapper = styled.div`
+ margin-right: 16px;
`;
-const buttonContainerStyles = css`
+const ButtonContainer = styled.div`
display: flex;
gap: 10px;
margin-left: auto;
- margin-right: 20px;
`;
-// 검색 결과 아이템 컴포넌트
-const SearchResultItem = ({ friend }: { friend: SearchResult }) => {
+const EmptyMessage = styled.p`
+ text-align: center;
+ color: #999;
+ font-size: 16px;
+ margin-top: 20px;
+`;
+
+const FriendPage = () => {
+ const [activeTab, setActiveTab] = useState("friendList");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchResults, setSearchResults] = useState(null);
+ const [searched, setSearched] = useState(false);
+
+ // React Query hooks
+ const {
+ data: friendList = [],
+ isLoading: isLoadingFriends,
+ refetch: refetchFriends,
+ } = useGetFriends();
+
+ const {
+ data: receivedRequests = [],
+ isLoading: isLoadingReceived,
+ refetch: refetchReceivedRequests,
+ } = useGetReceivedRequests();
+
+ const {
+ data: sentRequests = [],
+ isLoading: isLoadingSent,
+ refetch: refetchSentRequests,
+ } = useGetSentRequests();
+
+ const { refetch: fetchFriendByNickname } =
+ useGetFriendByNickname(searchQuery);
+
+ const handleSearch = async () => {
+ setSearched(true);
+ if (searchQuery.trim()) {
+ const { data } = await fetchFriendByNickname();
+ if (data) {
+ setSearchResults({
+ id: data.id,
+ nickname: data.nickname,
+ profileImage: data.profileImage,
+ });
+ } else {
+ setSearchResults(null);
+ }
+ } else {
+ alert("검색어를 입력해주세요.");
+ }
+ };
+
+ const handleSearchInputChange = (e: React.ChangeEvent) => {
+ setSearchQuery(e.target.value);
+ setSearched(false);
+ };
+
+ const renderedFriendList = useMemo(() => {
+ if (friendList.length === 0) {
+ return 친구가 없습니다.;
+ }
+ return friendList.map((friend) => (
+
+ ));
+ }, [friendList, refetchFriends]);
+
+ const renderedSentRequests = useMemo(() => {
+ if (sentRequests.length === 0) {
+ return 보낸 요청이 없습니다.;
+ }
+ return sentRequests.map((request) => (
+
+ ));
+ }, [
+ sentRequests,
+ refetchFriends,
+ refetchReceivedRequests,
+ refetchSentRequests,
+ ]);
+
+ const renderedReceivedRequests = useMemo(() => {
+ if (receivedRequests.length === 0) {
+ return 받은 요청이 없습니다.;
+ }
+ return receivedRequests.map((request) => (
+
+ ));
+ }, [
+ receivedRequests,
+ refetchFriends,
+ refetchReceivedRequests,
+ refetchSentRequests,
+ ]);
+
+ return (
+
+ 친구
+
+ setActiveTab("friendSearch")}
+ >
+ 친구 검색
+
+ setActiveTab("friendList")}
+ >
+ 친구 목록
+
+ setActiveTab("receivedRequests")}
+ >
+ 받은 요청
+
+ setActiveTab("sentRequests")}
+ >
+ 보낸 요청
+
+
+
+ {activeTab === "friendSearch" && (
+
+
+
+
+ {
+ if (e.key === "Enter") handleSearch();
+ }}
+ >
+ 검색
+
+
+ {searched && !searchResults && (
+ 검색 결과가 없습니다.
+ )}
+ {searchResults && (
+
+ )}
+
+ )}
+
+ {activeTab !== "friendSearch" && (
+
+ {activeTab === "friendList" &&
+ !isLoadingFriends &&
+ renderedFriendList}
+ {activeTab === "sentRequests" &&
+ !isLoadingSent &&
+ renderedSentRequests}
+ {activeTab === "receivedRequests" &&
+ !isLoadingReceived &&
+ renderedReceivedRequests}
+
+ )}
+
+ );
+};
+
+// Components
+
+const SearchResultItem = ({
+ friend,
+ refetchSentRequests,
+}: {
+ friend: SearchResult;
+ refetchSentRequests: () => void;
+}) => {
const { sendFriendRequest, isLoading } = useFriendRequest();
- const handleFriendRequest = () => {
- sendFriendRequest(friend.id);
+ const handleFriendRequest = async () => {
+ await sendFriendRequest(friend.id);
+ refetchSentRequests();
};
return (
-
+
+
);
};
-// List 아이템을 렌더링하는 컴포넌트
-const FriendItem = ({ friend }: { friend: Friend }) => {
+const FriendItem = ({
+ friend,
+ refetchFriends,
+}: {
+ friend: Friend;
+ refetchFriends: () => void;
+}) => {
const navigate = useNavigate();
const deleteFriendMutation = useDeleteFriend(friend.userId);
const { userData } = useUserData();
+
const handleVisitClick = () => {
navigate(`/friend/${friend.userId}`, {
state: { friendName: friend.nickname, userId: userData.id },
@@ -167,22 +392,30 @@ const FriendItem = ({ friend }: { friend: Friend }) => {
const handleDeleteClick = () => {
if (window.confirm("정말로 삭제하시겠습니까?")) {
- deleteFriendMutation.mutate();
+ deleteFriendMutation.mutate(undefined, {
+ onSuccess: () => {
+ refetchFriends();
+ alert("친구를 삭제했습니다.");
+ },
+ });
}
};
return (
-
+
+
);
};
@@ -190,14 +423,20 @@ function isSentRequest(
request: SentRequest | ReceivedRequest,
): request is SentRequest {
return (request as SentRequest).receiverName !== undefined;
-} // 타입가드
+}
const RequestItem = ({
request,
type,
+ refetchFriends,
+ refetchReceivedRequests,
+ refetchSentRequests,
}: {
request: SentRequest | ReceivedRequest;
type: "sent" | "received";
+ refetchFriends: () => void;
+ refetchReceivedRequests: () => void;
+ refetchSentRequests: () => void;
}) => {
const acceptFriendRequestMutation = useAcceptFriendRequest(request.id);
const rejectFriendRequestMutation = useRejectFriendRequest(request.id);
@@ -205,38 +444,57 @@ const RequestItem = ({
const handleAcceptClick = () => {
if (window.confirm("이 친구 요청을 수락하시겠습니까?")) {
- acceptFriendRequestMutation.mutate();
+ acceptFriendRequestMutation.mutate(undefined, {
+ onSuccess: () => {
+ refetchFriends();
+ refetchReceivedRequests();
+ alert("친구 요청을 수락했습니다.");
+ },
+ });
}
};
const handleRejectClick = () => {
if (window.confirm("이 친구 요청을 거절하시겠습니까?")) {
- rejectFriendRequestMutation.mutate();
+ rejectFriendRequestMutation.mutate(undefined, {
+ onSuccess: () => {
+ refetchReceivedRequests();
+ alert("친구 요청을 거절했습니다.");
+ },
+ });
}
};
const handleCancelClick = () => {
if (window.confirm("이 친구 요청을 취소하시겠습니까?")) {
- cancelFriendRequestMutation.mutate();
+ cancelFriendRequestMutation.mutate(undefined, {
+ onSuccess: () => {
+ refetchSentRequests();
+ },
+ });
}
};
return (
-
-
+
+
+
+
-
+
{type === "sent" ? (
-
>
)}
-
-
+
+
);
};
-export default function FriendListPage() {
- const [activeTab, setActiveTab] = useState("friendList");
- const [searchQuery, setSearchQuery] = useState("");
- const [searchResults, setSearchResults] = useState(null);
- const [searched, setSearched] = useState(false);
-
- // React Query 훅 사용
- const { data: friendList = [], isLoading: isLoadingFriends } =
- useGetFriends();
- const { data: receivedRequests = [], isLoading: isLoadingReceived } =
- useGetReceivedRequests();
- const { data: sentRequests = [], isLoading: isLoadingSent } =
- useGetSentRequests();
- const { refetch: fetchFriendByNickname } =
- useGetFriendByNickname(searchQuery);
-
- const handleSearch = async () => {
- setSearched(true);
- if (searchQuery.trim()) {
- const { data } = await fetchFriendByNickname();
- if (data) {
- setSearchResults({
- id: data.id,
- nickname: data.nickname,
- profileImage: data.profileImage,
- });
- } else {
- setSearchResults(null);
- }
- } else {
- alert("검색어를 입력해주세요.");
- }
- };
-
- const handleSearchInputChange = (e: React.ChangeEvent) => {
- setSearchQuery(e.target.value);
- setSearched(false);
- };
-
- const renderedFriendList = useMemo(
- () =>
- friendList.map((friend) => (
-
- )),
- [friendList],
- );
- const renderedSentRequests = useMemo(
- () =>
- sentRequests.map((request) => (
-
- )),
- [sentRequests],
- );
- const renderedReceivedRequests = useMemo(
- () =>
- receivedRequests.map((request) => (
-
- )),
- [receivedRequests],
- );
-
- return (
-
-
-
setActiveTab("friendSearch")}
- role="button"
- tabIndex={0}
- >
- 친구 검색
-
-
setActiveTab("friendList")}
- role="button"
- tabIndex={0}
- >
- 친구 목록
-
-
setActiveTab("receivedRequests")}
- role="button"
- tabIndex={0}
- >
- 받은 요청
-
-
setActiveTab("sentRequests")}
- role="button"
- tabIndex={0}
- >
- 보낸 요청
-
-
-
- {activeTab === "friendSearch" && (
-
-
-
-
- {
- if (e.key === "Enter") handleSearch();
- }}
- >
- 검색
-
-
- {searched && !searchResults &&
검색 결과가 없습니다.
}
- {searchResults &&
}
-
- )}
- {activeTab === "friendList" && !isLoadingFriends && renderedFriendList}
- {activeTab === "sentRequests" && !isLoadingSent && renderedSentRequests}
- {activeTab === "receivedRequests" &&
- !isLoadingReceived &&
- renderedReceivedRequests}
-
-
- );
-}
+export default FriendPage;
diff --git a/src/pages/LoginModal/LoginModal.tsx b/src/pages/LoginModal/LoginModal.tsx
index cd018af..66b0185 100644
--- a/src/pages/LoginModal/LoginModal.tsx
+++ b/src/pages/LoginModal/LoginModal.tsx
@@ -1,56 +1,65 @@
import styled from "@emotion/styled";
import Button from "@/components/common/Button/Button";
-import NavBar from "@/components/features/Navbar/Navbar";
+import { useModal } from "@/context/LoginModalContext";
-const LoginContainer = styled.div`
+const ModalOverlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
display: flex;
- flex-direction: column;
- align-items: center;
justify-content: center;
- height: calc(100vh - 80px);
- padding: 0 20px;
- background-color: #f9f9f9;
+ align-items: center;
+ z-index: 9999;
+`;
+
+const ModalContent = styled.div`
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ width: 400px;
+ text-align: center;
`;
const Title = styled.h1`
- font-size: 2.5rem;
+ font-size: 1.3rem;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
`;
const Description = styled.p`
- font-size: 1.2rem;
+ font-size: 1rem;
color: #555;
margin-bottom: 2rem;
`;
const LoginModal: React.FC = () => {
+ const { isLoginModalOpen, closeLoginModal } = useModal();
+ const loginUrl = import.meta.env.VITE_LOGIN_URL;
+
const handleLogin = async () => {
try {
- window.location.href =
- "https://api.splanet.co.kr/oauth2/authorization/kakao";
+ window.location.href = loginUrl;
} catch (e) {
console.error("로그인 에러:", e);
}
};
- return (
- <>
- {/* Navbar */}
-
-
- {/* 로그인 페이지 컨테이너 */}
-
- 로그인 페이지
- 임시로 만든 로그인 페이지입니다.
+ if (!isLoginModalOpen) return null;
- {/* Login 버튼 */}
-
+ return (
+
+ e.stopPropagation()}>
+ 세션이 만료되었습니다.
+ 다시 로그인해주세요.
+
Login with Kakao
-
- >
+
+
);
};
diff --git a/src/pages/LoginModal/RedirectPage.tsx b/src/pages/LoginModal/RedirectPage.tsx
index c3348b3..0f398d0 100644
--- a/src/pages/LoginModal/RedirectPage.tsx
+++ b/src/pages/LoginModal/RedirectPage.tsx
@@ -1,88 +1,54 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import RouterPath from "@/router/RouterPath";
-import useSavePreviewPlan from "@/api/hooks/useSavePreviewPlan";
import useAuth from "@/hooks/useAuth";
-import { apiClient } from "@/api/instance";
-import { CalendarEvent } from "@/components/features/CustomCalendar/CustomCalendar";
+import RouterPath from "@/router/RouterPath";
const OAuthRedirectHandler = () => {
const navigate = useNavigate();
const { setAuthState, authState } = useAuth();
- const { mutate: savePreviewPlan } = useSavePreviewPlan();
- const [hasSaved, setHasSaved] = useState(false);
- // 인증 처리를 위한 useEffect
useEffect(() => {
- const handleAuth = async () => {
+ try {
const queryParams = new URLSearchParams(window.location.search);
const accessToken = queryParams.get("access");
const refreshToken = queryParams.get("refresh");
- // const deviceId = queryParams.get("deviceId");
-
- if (!accessToken || !refreshToken) {
- navigate(RouterPath.LOGIN);
- return;
+ const deviceId = queryParams.get("deviceId");
+
+ if (accessToken && refreshToken) {
+ const cookieOptions = "path=/; Secure; SameSite=Strict;";
+
+ // 토큰을 쿠키에 저장
+ document.cookie = `access_token=${accessToken}; ${cookieOptions}`;
+ document.cookie = `refresh_token=${refreshToken}; ${cookieOptions}`;
+ document.cookie = `device_id=${deviceId}; ${cookieOptions}`;
+
+ // 상태 업데이트
+ const newAuthState = {
+ isAuthenticated: true,
+ };
+ setAuthState(newAuthState);
+ localStorage.setItem("authState", JSON.stringify(newAuthState));
+ // axios 인스턴스 헤더에 토큰 추가
+ // apiClient.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
+ } else {
+ // 토큰이 없으면 메인 페이지로 리다이렉트
+ navigate(RouterPath.HOME);
}
+ } catch (error) {
+ console.error("OAuth 리다이렉트 처리 중 오류 발생:", error);
+ navigate(RouterPath.HOME);
+ }
+ }, [navigate, setAuthState]);
- const cookieOptions = "path=/; Secure; SameSite=Strict;";
- document.cookie = `access_token=${accessToken}; ${cookieOptions}`;
- document.cookie = `refresh_token=${refreshToken}; ${cookieOptions}`;
-
- setAuthState({
- isAuthenticated: true,
- accessToken,
- });
-
- apiClient.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
- };
-
- handleAuth();
- }, []); // 빈 의존성 배열
-
- // 플랜 저장을 위한 useEffect
+ // authState가 업데이트되었을 때 메인 페이지로 리다이렉트
useEffect(() => {
- const savePlans = async () => {
- if (!authState.isAuthenticated || hasSaved) return;
-
- const savedPlanData = localStorage.getItem("previewPlanData");
- if (!savedPlanData) {
- setHasSaved(true);
- navigate(RouterPath.MAIN);
- return;
- }
-
- try {
- const { selectedPlan, previewDeviceId, previewGroupId } =
- JSON.parse(savedPlanData);
-
- // 모든 플랜을 한 번에 전송
- const planDataList = selectedPlan.map((plan: CalendarEvent) => ({
- title: plan.title,
- description: plan.description,
- startDate: new Date(plan.start).toISOString(),
- endDate: new Date(plan.end).toISOString(),
- }));
-
- await savePreviewPlan({
- deviceId: previewDeviceId,
- groupId: previewGroupId,
- planDataList, // 전체 플랜 배열 전송
- });
-
- localStorage.removeItem("previewPlanData");
- setHasSaved(true);
- navigate(RouterPath.MAIN);
- } catch (error) {
- console.error("플랜 저장 실패:", error);
- navigate(RouterPath.MAIN);
- }
- };
-
- savePlans();
- }, [authState.isAuthenticated]);
+ // console.log("현재 authState:", authState);
+ if (authState.isAuthenticated) {
+ navigate(RouterPath.MAIN);
+ }
+ }, [authState, navigate]);
- return 로그인 처리 중...
;
+ return 리다이렉트 처리 중...
;
};
export default OAuthRedirectHandler;
diff --git a/src/pages/Main/MainPage.tsx b/src/pages/Main/MainPage.tsx
index 54a3845..d34cb5a 100644
--- a/src/pages/Main/MainPage.tsx
+++ b/src/pages/Main/MainPage.tsx
@@ -1,266 +1,119 @@
+import { useEffect, useRef } from "react";
import styled from "@emotion/styled";
-import { useState, useEffect } from "react";
-import CustomCalendar from "@/components/features/CustomCalendar/CustomCalendar";
+import { useNavigate, useLocation } from "react-router-dom";
+import CustomCalendar, {
+ CalendarEvent,
+} from "@/components/features/CustomCalendar/CustomCalendar";
import { useGetPlans } from "@/api/hooks/useGetPlans";
-import useCreatePlan from "@/api/hooks/useCreatePlans";
-import CircleButton from "@/components/common/CircleButton/CircleButton";
-import breakpoints from "@/variants/breakpoints";
-
-// 캘린더와 버튼을 포함하는 반응형 컨테이너
-const CalendarContainer = styled.div`
- position: relative;
- width: 100%;
- max-width: 1200px; // 최대 너비를 설정해 버튼이 중앙을 유지하도록 합니다.
- margin: 0 auto;
+import useCreatePlan from "@/api/hooks/useCreatePlan";
+import Button from "@/components/common/Button/Button";
+import RouterPath from "@/router/RouterPath";
+import { requestForToken, setupOnMessageListener } from "@/api/firebaseConfig"; // Import Firebase functions
+import { apiClient } from "@/api/instance";
+
+const PageContainer = styled.div`
+ background-color: #ffffff;
padding: 20px;
`;
-// 버튼을 캘린더 컨테이너 내의 상대적인 위치에 두도록 설정
-const ButtonWrapper = styled.div`
- position: absolute;
- top: 10%;
- display: flex;
- justify-content: flex-start;
- margin-top: 20px;
- left: calc(10% + 10px);
- z-index: 1;
-
- ${breakpoints.tablet} {
- top: 10%;
- left: 20px;
- }
-
- ${breakpoints.mobile} {
- top: 10%;
- left: 20px;
- }
-`;
-
-// 모달 스타일
-const ModalOverlay = styled.div`
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2;
-`;
-
-const ModalContent = styled.div`
- background: white;
- padding: 20px 30px 20;
- width: 500px; // 가로폭 설정
- max-width: 95%;
- border-radius: 8px;
- box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.2);
- display: flex;
- flex-direction: column;
- gap: 15px; /* 각 입력 필드 사이에 간격 추가 */
-`;
-
-const Input = styled.input`
- width: 100%;
- padding: 8px;
- margin-top: 10px;
- margin-bottom: 10px;
- border: 1px solid #ccc;
- border-radius: 4px;
-`;
-
-const Textarea = styled.textarea`
- width: 99%;
- padding: 10px;
- margin-top: 5px;
- border: 1px solid #ccc;
- border-radius: 4px;
- resize: vertical;
-`;
-
-const CloseButton = styled.button`
- margin-top: 10px;
- background-color: #39a7f7;
- color: white;
- border: none;
- padding: 10px 15px;
- border-radius: 4px;
- cursor: pointer;
-`;
-
-const ToggleWrapper = styled.label`
- display: flex;
- flex-direction: column;
- cursor: pointer;
- gap: 10px;
-`;
-
-const ToggleInput = styled.input`
- appearance: none;
- width: 40px;
- height: 20px;
- background: #ccc;
- border-radius: 20px;
- position: relative;
- outline: none;
- transition: background 0.3s;
-
- &:checked {
- background: #39a7f7;
- }
-
- &:before {
- content: "";
- position: absolute;
- width: 18px;
- height: 18px;
- border-radius: 50%;
- background: white;
- top: 1px;
- left: 1px;
- transition: transform 0.3s;
- }
-
- &:checked:before {
- transform: translateX(20px);
- }
-`;
-
-const ToggleLabel = styled.span`
- font-size: 16px;
- color: #333;
-`;
-
-const MainPage: React.FC = () => {
- const { data: plans, isLoading, error } = useGetPlans();
- const [modalOpen, setIsModalOpen] = useState(false);
- const [title, setTitle] = useState("");
- const [description, setDescription] = useState("");
- const [startDate, setStartDate] = useState("");
- const [endDate, setEndDate] = useState("");
- const [isAccessible, setIsAccessible] = useState(false);
- const [isCompleted, setIsCompleted] = useState(false);
+export default function MainPage() {
+ const location = useLocation();
+ const { data: Plans, isLoading, error, refetch } = useGetPlans();
+ const savePlanMutation = useCreatePlan();
+ const isPlanSaved = useRef(false);
+ const hasMounted = useRef(false);
+ const navigate = useNavigate();
- // useCreatePlan 훅 사용
- const createPlanMutation = useCreatePlan();
+ useEffect(() => {
+ if (location.state?.refetchNeeded) {
+ refetch();
+ }
+ }, [location, refetch]);
- const handleToggleChange =
- (setter: React.Dispatch>) => () => {
- setter((prev) => !prev);
+ useEffect(() => {
+ if (hasMounted.current) return;
+ hasMounted.current = true;
+
+ const savePlans = async () => {
+ const storedPlans = sessionStorage.getItem("plans");
+ if (storedPlans && !isPlanSaved.current) {
+ const parsedPlans: CalendarEvent[] = JSON.parse(storedPlans).map(
+ (plan: CalendarEvent) => ({
+ ...plan,
+ start: new Date(plan.start),
+ end: new Date(plan.end),
+ }),
+ );
+
+ try {
+ await Promise.all(
+ parsedPlans.map((plan) =>
+ savePlanMutation.mutateAsync({
+ plan: {
+ title: plan.title,
+ description: plan.description,
+ startDate: plan.start.toISOString(),
+ endDate: plan.end.toISOString(),
+ accessibility: plan.accessibility ?? true,
+ isCompleted: plan.complete ?? false,
+ },
+ }),
+ ),
+ );
+ sessionStorage.removeItem("plans");
+ console.log("세션의 플랜이 저장되었습니다.");
+ isPlanSaved.current = true;
+ refetch();
+ } catch (err) {
+ console.error("세션의 플랜 저장 실패:", err);
+ }
+ }
};
- const handleButtonClick = () => {
- setIsModalOpen(true);
- };
-
- // 폼 제출 핸들러
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- createPlanMutation.mutate({
- title,
- description,
- startDate,
- endDate,
- accessibility: isAccessible,
- isCompleted,
- });
- setIsModalOpen(false); // 폼 제출 후 모달 닫기
- };
+ savePlans();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ // Notification functionality
useEffect(() => {
- // 로컬 스토리지 정리 (필요한 경우)
- const savedPreviewData = localStorage.getItem("previewPlanData");
- if (savedPreviewData) {
- localStorage.removeItem("previewPlanData");
- }
+ const registerFcmToken = async () => {
+ const permission = await Notification.requestPermission();
+ if (permission === "granted") {
+ try {
+ const fcmToken = await requestForToken();
+ if (fcmToken) {
+ await apiClient.post("/api/fcm/register", { token: fcmToken });
+ console.log("FCM 토큰이 성공적으로 등록되었습니다.");
+ }
+ } catch (err) {
+ console.error("FCM 토큰 등록 중 오류 발생:", err);
+ }
+ } else {
+ console.log("알림 권한이 거부되었습니다.");
+ }
+ };
+
+ registerFcmToken();
+ setupOnMessageListener(); // Set up the listener for foreground messages
}, []);
- if (isLoading) {
- return Loading...
; // 로딩 상태 처리
- }
+ const handleModifyClick = () => {
+ navigate(RouterPath.MAIN_MODIFY, { state: { plans: Plans } });
+ };
- if (error) {
- return Error: {error.message}
; // 에러 처리
- }
+ if (isLoading) return 로딩 중...
;
+ if (error) return 데이터를 불러오지 못했습니다. 오류: {error.message}
;
return (
-
-
- +
-
-
-
- {modalOpen && (
-
-
- 새 일정 추가
-
-
-
- )}
-
+
+
+
+ 수정하기
+
+
);
-};
-export default MainPage;
+}
diff --git a/src/pages/Main/MainPageModify.tsx b/src/pages/Main/MainPageModify.tsx
new file mode 100644
index 0000000..3320620
--- /dev/null
+++ b/src/pages/Main/MainPageModify.tsx
@@ -0,0 +1,250 @@
+import { useState } from "react";
+import styled from "@emotion/styled";
+import { useLocation, useNavigate } from "react-router-dom";
+import ReactDatePicker from "@/components/features/DatePicker/DatePicker";
+import CustomCalendar, {
+ CalendarEvent,
+} from "@/components/features/CustomCalendar/CustomCalendar";
+import { apiClient } from "@/api/instance";
+import useCreatePlan from "@/api/hooks/useCreatePlan";
+import useDeletePlan from "@/api/hooks/useDeletePlan";
+import Button from "@/components/common/Button/Button";
+import Modal from "@/components/common/Modal/Modal";
+import RouterPath from "@/router/RouterPath";
+
+const ModalContainer = styled.div`
+ padding: 20px;
+ background-color: white;
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+`;
+
+const Title = styled.h2`
+ font-size: 1.8rem;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 20px;
+`;
+
+const StyledInput = styled.input`
+ width: 100%;
+ padding: 12px;
+ margin-bottom: 10px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ font-size: 1rem;
+ &:focus {
+ outline: none;
+ border-color: #6c63ff;
+ box-shadow: 0 0 0 2px rgba(108, 99, 255, 0.3);
+ }
+`;
+
+const PageContainer = styled.div`
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 16px;
+`;
+
+const ButtonGroup = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 20px;
+`;
+
+export default function PlanModifyPage() {
+ const location = useLocation();
+ const { plans = [], teamName } = location.state || {};
+ const [modifiedPlans, setModifiedPlans] = useState(plans);
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+ const [newPlanData, setNewPlanData] = useState({
+ title: "",
+ description: "",
+ startDate: "",
+ endDate: "",
+ accessibility: true,
+ isCompleted: false,
+ });
+ const navigate = useNavigate();
+
+ const [pendingPlans, setPendingPlans] = useState(false);
+ const { mutate: createPlan } = useCreatePlan();
+ const { mutate: deletePlan } = useDeletePlan();
+
+ const handleAddPlan = () => setIsAddModalOpen(true);
+
+ const handleAddPlanSubmit = () => {
+ const {
+ title,
+ description,
+ startDate,
+ endDate,
+ accessibility,
+ isCompleted,
+ } = newPlanData;
+ const utcStartDate = new Date(`${startDate}Z`).toISOString();
+ const utcEndDate = new Date(`${endDate}Z`).toISOString();
+
+ createPlan(
+ {
+ plan: {
+ title,
+ description,
+ startDate: utcStartDate,
+ endDate: utcEndDate,
+ accessibility,
+ isCompleted,
+ },
+ },
+ {
+ onSuccess: (response) => {
+ const newPlanId = response.data.id;
+ alert("플랜이 추가되었습니다.");
+ setModifiedPlans([
+ ...modifiedPlans,
+ {
+ ...newPlanData,
+ id: newPlanId,
+ start: new Date(utcStartDate),
+ end: new Date(utcEndDate),
+ complete: isCompleted,
+ },
+ ]);
+ setIsAddModalOpen(false);
+ setNewPlanData({
+ title: "",
+ description: "",
+ startDate: "",
+ endDate: "",
+ accessibility: true,
+ isCompleted: false,
+ });
+ },
+ onError: (error) => {
+ alert(`추가 중 오류 발생: ${error.message}`);
+ },
+ },
+ );
+ };
+
+ const handleDeletePlan = (planId: string) => {
+ if (window.confirm("정말로 삭제하시겠습니까?")) {
+ deletePlan(parseInt(planId, 10), {
+ onSuccess: () => {
+ alert("플랜이 삭제되었습니다.");
+ setModifiedPlans((prevPlans) =>
+ prevPlans.filter((plan) => plan.id !== planId),
+ );
+ },
+ onError: (error) => {
+ alert(`삭제 중 오류 발생: ${error.message}`);
+ },
+ });
+ }
+ };
+
+ const handlePlanChange = (updatedPlans: CalendarEvent[]) => {
+ setModifiedPlans(updatedPlans);
+ };
+
+ const handleSaveAll = () => {
+ setPendingPlans(true);
+ Promise.all(
+ modifiedPlans
+ .filter((plan) => plan.id && !Number.isNaN(Number(plan.id)))
+ .map((plan) =>
+ apiClient.put(`/api/plans/${plan.id}`, {
+ title: plan.title,
+ description: plan.description,
+ startDate: new Date(plan.start).toISOString(),
+ endDate: new Date(plan.end).toISOString(),
+ accessibility: plan.accessibility ?? true,
+ isCompleted: plan.complete ?? false,
+ }),
+ ),
+ )
+ .then(() => {
+ alert("수정사항이 저장되었습니다.");
+ setPendingPlans(false);
+ navigate(RouterPath.MAIN, { state: { refetchNeeded: true } });
+ })
+ .catch((error) => {
+ alert(`저장 중 오류 발생: ${error.message}`);
+ setPendingPlans(false);
+ });
+ };
+
+ return (
+
+
+ {pendingPlans && 저장 중...
}
+
+
+ 플랜 추가
+
+ 저장
+
+
+ {isAddModalOpen && (
+ setIsAddModalOpen(false)}>
+
+ 새로운 플랜 추가
+
+ setNewPlanData({ ...newPlanData, title: e.target.value })
+ }
+ />
+
+ setNewPlanData({ ...newPlanData, description: e.target.value })
+ }
+ />
+
+ setNewPlanData((prevData) => ({
+ ...prevData,
+ startDate: date ? date.toISOString().slice(0, 16) : "",
+ }))
+ }
+ showTimeSelect
+ dateFormat="yyyy/MM/dd HH:mm"
+ />
+
+ setNewPlanData((prevData) => ({
+ ...prevData,
+ endDate: date ? date.toISOString().slice(0, 16) : "",
+ }))
+ }
+ showTimeSelect
+ dateFormat="yyyy/MM/dd HH:mm"
+ />
+ 추가
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/Mypage/Mypage.tsx b/src/pages/Mypage/Mypage.tsx
index e226ecb..c3a557c 100644
--- a/src/pages/Mypage/Mypage.tsx
+++ b/src/pages/Mypage/Mypage.tsx
@@ -1,76 +1,105 @@
+// src/pages/MyPage.tsx
import { useState } from "react";
import styled from "@emotion/styled";
+import { motion } from "framer-motion";
import CreditCardIcon from "@mui/icons-material/CreditCard";
import NotificationsIcon from "@mui/icons-material/Notifications";
import Switch from "@mui/material/Switch";
+import { useNavigate } from "react-router-dom";
import List from "@/components/common/List/List";
import Button from "@/components/common/Button/Button";
import useUserData from "@/api/hooks/useUserData";
+import useAuth from "@/hooks/useAuth";
+import RouterPath from "@/router/RouterPath";
const PageWrapper = styled.div`
+ display: flex;
min-height: 100vh;
- padding: 2rem;
+ background-color: #ffffff;
+ overflow: hidden;
`;
-const ContentWrapper = styled.div`
- max-width: 800px;
- margin: 0 auto;
- display: flex;
- flex-direction: column;
- gap: 2rem;
+const ContentWrapper = styled.main`
+ flex-grow: 1;
+ padding: 32px; /* p-8 */
+ overflow: auto;
+`;
+
+const Heading = styled.h1`
+ font-size: 24px; /* text-3xl */
+ font-weight: 600; /* font-semibold */
+ margin-bottom: 24px; /* mb-6 */
+ color: #2d3748; /* text-gray-800 */
`;
-const Card = styled.div`
- background-color: #f4f4f4;
- border-radius: 16px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
+const GridLayout = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px; /* gap-6 */
+
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr;
+ }
+`;
+
+const Card = styled(motion.div)`
+ background-color: #ffffff; /* bg-white */
+ border-radius: 8px; /* rounded-lg */
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* shadow-md */
+ padding: 24px; /* p-6 */
+ transition: box-shadow 0.2s; /* transition-shadow duration-200 */
+ margin-bottom: 18px;
+ &:hover {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); /* hover:shadow-lg */
+ }
+`;
+
+const ProfileCard = styled(motion.div)`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #ffffff; /* bg-white */
+ border-radius: 8px; /* rounded-lg */
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* shadow-md */
+ padding: 24px; /* p-6 */
+ transition: box-shadow 0.2s; /* transition-shadow duration-200 */
+ margin-bottom: 18px;
+ &:hover {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); /* hover:shadow-lg */
+ }
`;
const CardHeader = styled.div`
display: flex;
align-items: center;
- margin-bottom: 1rem;
+ margin-bottom: 16px; /* mb-4 */
`;
const CardTitle = styled.h3`
margin: 0;
- margin-left: 0.5rem;
- font-size: 1.2rem;
- font-weight: bold;
+ margin-left: 8px; /* ml-2 */
+ font-size: 18px; /* text-xl */
+ font-weight: 600; /* font-semibold */
+ color: #4a5568; /* text-gray-700 */
`;
const CardContent = styled.div`
- ul {
- list-style-type: none;
- padding: 0;
- margin: 0;
- }
-
- li {
- margin-bottom: 0.5rem;
- }
-`;
-
-const GridLayout = styled.div`
- display: grid;
- grid-template-columns: 1fr;
- gap: 2rem;
-
- @media (min-width: 768px) {
- grid-template-columns: 1fr 1fr;
- }
+ font-size: 14px;
+ color: #4a5568; /* text-gray-700 */
`;
const DeleteButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
- margin-top: 2rem;
+ margin-top: 24px; /* mt-6 */
+ gap: 10px;
`;
export default function MyPage() {
const [isNotificationEnabled, setNotificationEnabled] = useState(false);
const { userData, handleDeleteAccount, handleSubscription } = useUserData();
+ const { setAuthState } = useAuth();
+ const navigate = useNavigate();
const handleNotificationToggle = () => {
setNotificationEnabled(!isNotificationEnabled);
@@ -84,23 +113,51 @@ export default function MyPage() {
}
};
+ const handleLogout = () => {
+ // 로그아웃 시 상태와 로컬 스토리지 초기화
+ setAuthState({ isAuthenticated: false });
+ localStorage.removeItem("authState");
+
+ // 헤더의 인증 토큰 제거
+ navigate(RouterPath.HOME);
+ };
+
+ const handleVisitClick = () => {
+ navigate(`/friend/${userData.id}`, {
+ state: { friendName: userData.nickname, userId: userData.id },
+ });
+ };
+
return (
+ 마이페이지
+
{/* 프로필 카드 */}
-
+
-
+
+ 방문
+
+
{/* 정보 카드 그리드 */}
- {/* 결제정보 카드 */}
-
+ {/* 구독정보 카드 */}
+
-
+
구독정보
@@ -111,25 +168,36 @@ export default function MyPage() {
{/* 알림설정 카드 */}
-
+
-
+
알림설정
-
-
+
+ 알림 켜기
+
+
{/* 회원 탈퇴 버튼 */}
+
+ 로그아웃
+
회원 탈퇴
diff --git a/src/pages/Plan/PlanPage.tsx b/src/pages/Plan/PlanPage.tsx
index 86373dd..91a4666 100644
--- a/src/pages/Plan/PlanPage.tsx
+++ b/src/pages/Plan/PlanPage.tsx
@@ -1,56 +1,53 @@
+// src/pages/Plan/PlanPage.tsx
import styled from "@emotion/styled";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import { motion, AnimatePresence } from "framer-motion";
import Input from "@/components/common/Input/Input";
-import MicrophoneButton from "@/components/features/MicrophoneButton/MicrophoneButton";
import Button from "@/components/common/Button/Button";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
import useVoiceHook from "@/hooks/useVoiceHook";
-import useGptRequest from "@/api/hooks/useGptRequest";
-import useGenerateDeviceId from "@/api/hooks/useGenerateDeviceId";
-import useSavePlan from "@/api/hooks/useSavePlan";
+import MicrophoneButton from "@/components/features/MicrophoneButton/MicrophoneButton";
const PlanPageContainer = styled.div`
- width: 60%
- display: grid;
- justify-content: center;
- align-items: center;
-`;
-const InputWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
- gap: 40px;
- width: 100%;
+ padding: 32px;
+ box-sizing: border-box;
`;
-const Title = styled.p`
- font-size: 30px;
+
+const Title = styled.h1`
+ font-size: 32px;
font-weight: bold;
- color: #938e8e;
+ color: #2d3748;
text-align: center;
- margin: 50px 0 0 0;
+ margin-bottom: 24px;
+
${breakpoints.tablet} {
- font-size: 20px;
+ font-size: 28px;
}
`;
-const SubTitle = styled.p`
- font-size: 30px;
- font-weight: bold;
- color: #000;
+
+const SubTitle = styled.h2`
+ font-size: 24px;
+ font-weight: normal;
+ color: #4a5568;
text-align: center;
- margin: 0;
+ margin-bottom: 32px;
${breakpoints.tablet} {
- font-size: 18px;
+ font-size: 20px;
}
`;
+
const ButtonContainer = styled.div`
display: flex;
- gap: 130px;
- margin-bottom: 40px;
+ gap: 120px;
+ margin-top: 32px;
`;
-function MessageSilderWithAnimation() {
+
+function MessageSliderWithAnimation() {
const messages = [
"일정의 예상 소요 시간을 말해주시면 더 정확해요.",
"고정된 일정이 있나요?",
@@ -62,11 +59,23 @@ function MessageSilderWithAnimation() {
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % messages.length);
- }, 3000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
- return {messages[currentMessageIndex]};
+ return (
+
+
+ {messages[currentMessageIndex]}
+
+
+ );
}
const PlanPage: React.FC = () => {
@@ -79,74 +88,36 @@ const PlanPage: React.FC = () => {
} = useVoiceHook();
const navigate = useNavigate();
- const { data: deviceId } = useGenerateDeviceId();
- const gptRequestMutation = useGptRequest();
- const savePlanMutation = useSavePlan();
-
- const handleSaveClick = async () => {
- if (!deviceId) {
- alert("Device ID를 생성하는 중입니다. 잠시 후 다시 시도해주세요.");
+ const handleNextClick = async () => {
+ if (!transcript || transcript.trim() === "") {
+ alert("입력값이 필요합니다.");
return;
}
-
- try {
- // GPT 요청 보내기
- const gptResponses = await gptRequestMutation.mutateAsync({
- deviceId,
- text: transcript,
- });
-
- console.log("GPT 응답 데이터:", gptResponses);
-
- // GPT 응답 데이터를 그대로 save API 호출에 전달
- await Promise.all(
- gptResponses.map((response) => {
- const { groupId, planCards } = response;
- return savePlanMutation.mutateAsync({
- deviceId,
- groupId,
- planCards: planCards.map((card) => ({
- ...card, // title, description, startDate, endDate를 그대로 유지
- accessibility: card.accessibility || true,
- isCompleted: card.isCompleted || false,
- })),
- });
- }),
- );
-
- navigate(RouterPath.PLAN_SELECT);
- } catch (error) {
- console.error("GPT 요청 또는 플랜 저장 실패:", error);
- }
+ navigate(RouterPath.PLAN_SELECT, { state: { transcript } });
};
return (
-
-
-
- setTranscript(e.target.value)}
- />
-
-
-
- 다음
-
- navigate(-1)}
- >
- 취소
-
-
-
+ 플랜을 생성해보세요.
+
+ ) =>
+ setTranscript(e.target.value)
+ }
+ placeholder="원하는 일정을 자유롭게 입력해보세요."
+ />
+
+
+ 다음
+ navigate(-1)} theme="secondary">
+ 취소
+
+
);
};
diff --git a/src/pages/Plan/PlanSelectPage.tsx b/src/pages/Plan/PlanSelectPage.tsx
index c53ff43..340f7d0 100644
--- a/src/pages/Plan/PlanSelectPage.tsx
+++ b/src/pages/Plan/PlanSelectPage.tsx
@@ -1,144 +1,183 @@
+// src/pages/Plan/PlanSelectPage.tsx
+import React, { useState, useEffect } from "react";
import styled from "@emotion/styled";
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
+import Cookies from "js-cookie";
+import { AxiosError } from "axios";
import CustomCalendar, {
CalendarEvent,
} from "@/components/features/CustomCalendar/CustomCalendar";
-import NumberButton from "@/components/common/NumberButton/NumberButton";
+import {
+ useGptLight,
+ useGptModerate,
+ useGptStrong,
+} from "@/api/hooks/useGeneratePlans";
import Button from "@/components/common/Button/Button";
+import NumberButton from "@/components/common/NumberButton/NumberButton";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
-import useGetPlanCard from "@/api/hooks/useGetPlanCard";
-import useGenerateDeviceId from "@/api/hooks/useGenerateDeviceId";
-const PlanSelectPageContainer = styled.div`
- display: grid;
+const PageContainer = styled.div`
+ display: flex;
+ flex-direction: column;
align-items: center;
- margin: 0 auto;
- margin-top: 20px;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-const CalendarSection = styled.div`
- margin-bottom: 40px;
- ${breakpoints.mobile} {
- margin-bottom: -50px;
- }
-`;
-
-const SidebarSection = styled.div`
- font-size: 30px;
+const Title = styled.h1`
+ font-size: 28px;
font-weight: bold;
- ${breakpoints.mobile} {
- font-size: 18px;
- }
-`;
-
-const StyledText = styled.p`
- text-align: center;
+ color: #2d3748;
+ margin-bottom: 24px;
`;
-const NumberButtonContainer = styled.div`
+const ButtonContainer = styled.div`
display: flex;
justify-content: center;
- align-items: center;
- gap: 10px;
+ gap: 16px;
+ margin-bottom: 24px;
`;
-const ButtonContainer = styled.div`
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 130px;
- margin-bottom: 30px;
- ${breakpoints.mobile} {
- gap: 30px;
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
+
+ @media (max-width: ${breakpoints.sm}px) {
+ padding: 0 20px;
}
`;
-const PlanSelectPage = () => {
- const { data: deviceId } = useGenerateDeviceId();
- const { data: plans } = useGetPlanCard(deviceId);
+const ActionButtonContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 120px;
+`;
- const [clickedNumber, setClickedNumber] = useState(null);
+const PlanSelectPage: React.FC = () => {
+ const { state } = useLocation();
+ const { transcript } = state || {};
const navigate = useNavigate();
+ const deviceId = Cookies.get("device_id") || "defaultDeviceId";
+
+ const [planCache, setPlanCache] = useState>({
+ light: [],
+ moderate: [],
+ strong: [],
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [selectedLevel, setSelectedLevel] = useState<
+ "light" | "moderate" | "strong"
+ >("light");
+
+ const { mutate: fetchLightPlans } = useGptLight();
+ const { mutate: fetchModeratePlans } = useGptModerate();
+ const { mutate: fetchStrongPlans } = useGptStrong();
- const handleNumberButtonClick = (number: number) => {
- setClickedNumber(number);
+ const handleNextClick = async () => {
+ navigate(RouterPath.PLAN_UPDATE, {
+ state: { plans: planCache[selectedLevel] },
+ });
};
- // 선택된 플랜 그룹의 이벤트 변환
- const selectedPlanGroup = plans?.find(
- (plan) => plan.groupId === String(clickedNumber),
- );
+ const handleFetchPlans = (level: "light" | "moderate" | "strong") => {
+ if (planCache[level].length > 0) {
+ setSelectedLevel(level);
+ return;
+ }
+ setIsLoading(true);
+
+ let fetchFn;
+ if (level === "light") {
+ fetchFn = fetchLightPlans;
+ } else if (level === "moderate") {
+ fetchFn = fetchModeratePlans;
+ } else {
+ fetchFn = fetchStrongPlans;
+ }
+
+ fetchFn(
+ { deviceId, text: transcript || "기본 추천 텍스트" },
+ {
+ onSuccess: (data) => {
+ if (typeof data === "string") {
+ alert(`잘못된 입력값입니다.\n응답데이터: ${data}`);
+ navigate(-1); // 이전 페이지로 이동
+ } else {
+ setPlanCache((prevCache) => ({
+ ...prevCache,
+ [level]: data.planCards,
+ }));
+ setSelectedLevel(level);
+ }
+ setIsLoading(false);
+ },
+ onError: (error: AxiosError) => {
+ if (error.response) {
+ if (error.response.status === 400) {
+ alert("잘못된 요청입니다.");
+ } else if (error.response.status === 500) {
+ alert("서버 내부 오류입니다.");
+ } else {
+ alert("플랜 요청 실패: 알 수 없는 오류");
+ }
+ } else {
+ alert(
+ "네트워크 오류가 발생했습니다. 유효한 입력값인지 확인해주세요.",
+ );
+ }
+ setIsLoading(false);
+ navigate(-1); // 이전 페이지로 이동
+ },
+ },
+ );
+ };
- const calendarEvents: CalendarEvent[] = selectedPlanGroup
- ? selectedPlanGroup.planCards.map((planCard) => ({
- id: planCard.cardId,
- title: planCard.title,
- description: planCard.description,
- start: new Date(planCard.startDate),
- end: new Date(planCard.endDate),
- accessibility: true, // 기본값 설정
- complete: false, // 기본값 설정
- }))
- : [];
+ useEffect(() => {
+ if (planCache.light.length === 0) {
+ handleFetchPlans("light");
+ } else {
+ setSelectedLevel("light");
+ }
+ }, []);
return (
-
-
- 원하는 플랜을 선택하세요.
-
- handleNumberButtonClick(1)}
- />
- handleNumberButtonClick(2)}
- />
- handleNumberButtonClick(3)}
- />
-
-
-
-
-
+ 원하는 플랜을 선택하세요
+
+ handleFetchPlans("light")}
+ />
+ handleFetchPlans("moderate")}
/>
-
+ handleFetchPlans("strong")}
+ />
+
-
-
- navigate(RouterPath.PREVIEW_PLAN_UPDATE, {
- state: {
- selectedPlan: calendarEvents,
- deviceId, // 실제 deviceId 값
- groupId: selectedPlanGroup?.groupId, // 선택된 그룹의 groupId },
- },
- })
- }
- >
- 확인
-
- navigate(-1)}
- >
+ {isLoading ? (
+ 로딩 중...
+ ) : (
+
+
+
+ )}
+
+ 다음
+ navigate(-1)} theme="secondary">
취소
-
-
+
+
);
};
diff --git a/src/pages/Plan/PlanUpdate.tsx b/src/pages/Plan/PlanUpdate.tsx
index 6152bf5..64a4660 100644
--- a/src/pages/Plan/PlanUpdate.tsx
+++ b/src/pages/Plan/PlanUpdate.tsx
@@ -1,156 +1,138 @@
+// src/pages/TeamPlan/TeamPlanUpdatePage.tsx
import styled from "@emotion/styled";
import { useNavigate, useLocation } from "react-router-dom";
-import { keyframes } from "@emotion/react";
import { useState, useEffect } from "react";
-import CustomCalendar from "@/components/features/CustomCalendar/CustomCalendar";
+import { motion, AnimatePresence } from "framer-motion";
+import CustomCalendar, {
+ CalendarEvent,
+} from "@/components/features/CustomCalendar/CustomCalendar";
import Button from "@/components/common/Button/Button";
-import breakpoints from "@/variants/breakpoints";
+import useCreatePlan from "@/api/hooks/useCreatePlan";
import RouterPath from "@/router/RouterPath";
-
-// 슬라이드 애니메이션
-const slideDown = keyframes`
- 0% {
- opacity: 0;
- transform: translateY(-20px);
- }
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
-`;
+import breakpoints from "@/variants/breakpoints";
const PlanUpdateContainer = styled.div`
- display: grid;
- align-items: center;
- margin: 0 auto;
- margin-top: 20px;
-`;
-
-const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
- justify-content: center;
+ align-items: center;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-const CalendarContainer = styled.div`
- margin-bottom: 40px;
- ${breakpoints.mobile} {
- margin-bottom: -50px;
- }
+const TitleContainer = styled.div`
+ height: 70px;
+ margin-bottom: 24px;
`;
-const StyledText = styled.p`
- font-size: 30px;
+const StyledText = styled.h1`
+ font-size: 24px;
font-weight: bold;
text-align: center;
- &.animate {
- animation: ${slideDown} 1s ease-in-out;
- }
${breakpoints.mobile} {
- font-size: 18px;
- white-space: pre-line;
+ font-size: 20px;
+ margin-top: 20px;
}
`;
-const StyledTextContainer = styled.div`
- height: 70px;
- margin-bottom: 20px;
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
`;
const ButtonContainer = styled.div`
display: flex;
- justify-content: center;
- align-items: center;
- gap: 130px;
- margin-bottom: 30px;
- ${breakpoints.mobile} {
- gap: 30px;
- }
+ gap: 16px;
`;
+const convertToSavePlanFormat = (event: CalendarEvent) => ({
+ title: event.title,
+ description: event.description,
+ startDate: event.start.toISOString(),
+ endDate: event.end.toISOString(),
+ accessibility: event.accessibility ?? true,
+ isCompleted: event.complete ?? false,
+});
+
const PlanUpdate = () => {
const TitleMessages = [
"플랜을 수정하거나, 바로 저장하세요.",
- "일정을 옮기고 크기를 조정하여\n원하는대로 플랜을 수정해보세요",
+ "일정을 옮기고 크기를 조정하여 원하는대로 플랜을 수정해보세요.",
];
-
+ const { state } = useLocation();
+ const { plans: initialPlans } = state || {};
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
- const [animate, setAnimate] = useState(false);
+
+ const [modifiedPlans, setModifiedPlans] = useState(
+ initialPlans || [],
+ );
+
+ const savePlanMutation = useCreatePlan();
+ const navigate = useNavigate();
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) =>
prevIndex === TitleMessages.length - 1 ? 0 : prevIndex + 1,
);
- setAnimate(true);
- setTimeout(() => {
- setAnimate(false);
- }, 1000);
- }, 5000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
- const navigate = useNavigate();
- const location = useLocation();
- const selectedPlan = location.state?.selectedPlan || [];
- const previewDeviceId = location.state?.deviceId;
- const previewGroupId = location.state?.groupId;
+ const handlePlanChange = (plans: CalendarEvent[]) => {
+ setModifiedPlans(plans);
+ };
const handleSave = async () => {
try {
- if (!selectedPlan || selectedPlan.length === 0) {
- alert("저장할 플랜이 없습니다.");
- return;
- }
-
- if (!previewDeviceId || !previewGroupId) {
- alert("디바이스 ID 또는 그룹 ID가 없습니다.");
- return;
- }
-
- // 플랜 데이터를 로컬 스토리지에 저장
- localStorage.setItem(
- "previewPlanData",
- JSON.stringify({
- selectedPlan,
- previewDeviceId,
- previewGroupId,
+ const savePlanPromises = modifiedPlans.map((plan) =>
+ savePlanMutation.mutateAsync({
+ plan: convertToSavePlanFormat(plan),
}),
);
- // 카카오 로그인 페이지로 리다이렉트
- window.location.href = RouterPath.MAIN;
+ await Promise.all(savePlanPromises);
+
+ alert("저장이 완료되었습니다!");
+ navigate(RouterPath.MAIN);
} catch (error) {
- console.error("플랜 저장 중 오류 발생:", error);
- alert("플랜 저장에 실패했습니다. 다시 시도해주세요.");
+ console.error("저장 실패:", error);
+ alert("저장 중 오류가 발생했습니다.");
}
};
return (
-
-
-
- {TitleMessages[currentMessageIndex]}
-
-
-
-
-
-
-
- 저장
- navigate(-1)}>
- 취소
-
-
-
+
+
+
+ {TitleMessages[currentMessageIndex]}
+
+
+
+
+
+
+
+
+
+ 저장
+ navigate(-1)}>
+ 취소
+
+
);
};
diff --git a/src/pages/PreviewPlan/PreviewPlanPage.tsx b/src/pages/PreviewPlan/PreviewPlanPage.tsx
index b9afd05..5179b0d 100644
--- a/src/pages/PreviewPlan/PreviewPlanPage.tsx
+++ b/src/pages/PreviewPlan/PreviewPlanPage.tsx
@@ -1,56 +1,54 @@
import styled from "@emotion/styled";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import { motion, AnimatePresence } from "framer-motion";
import Input from "@/components/common/Input/Input";
-import MicrophoneButton from "@/components/features/MicrophoneButton/MicrophoneButton";
import Button from "@/components/common/Button/Button";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
import useVoiceHook from "@/hooks/useVoiceHook";
-import useGptTrial from "@/api/hooks/useGptTrial";
+import MicrophoneButton from "@/components/features/MicrophoneButton/MicrophoneButton";
import useGenerateDeviceId from "@/api/hooks/useGenerateDeviceId";
-import useSavePlan from "@/api/hooks/useSavePlan";
const PlanPageContainer = styled.div`
- width: 60%
- display: grid;
- justify-content: center;
- align-items: center;
-`;
-const InputWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
- gap: 40px;
- width: 100%;
+ padding: 32px;
+ box-sizing: border-box;
`;
-const Title = styled.p`
- font-size: 30px;
+
+const Title = styled.h1`
+ font-size: 32px;
font-weight: bold;
- color: #938e8e;
+ color: #2d3748;
text-align: center;
- margin: 50px 0 0 0;
+ margin-bottom: 24px;
+
${breakpoints.tablet} {
- font-size: 20px;
+ font-size: 28px;
}
`;
-const SubTitle = styled.p`
- font-size: 30px;
- font-weight: bold;
- color: #000;
+
+const SubTitle = styled.h2`
+ font-size: 24px;
+ font-weight: normal;
+ color: #4a5568;
text-align: center;
- margin: 0;
+ margin-bottom: 32px;
+
${breakpoints.tablet} {
- font-size: 18px;
+ font-size: 20px;
}
`;
+
const ButtonContainer = styled.div`
display: flex;
- gap: 130px;
- margin-bottom: 40px;
+ gap: 120px;
+ margin-top: 32px;
`;
-function MessageSilderWithAnimation() {
+
+function MessageSliderWithAnimation() {
const messages = [
"일정의 예상 소요 시간을 말해주시면 더 정확해요.",
"고정된 일정이 있나요?",
@@ -62,11 +60,23 @@ function MessageSilderWithAnimation() {
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % messages.length);
- }, 3000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
- return {messages[currentMessageIndex]};
+ return (
+
+
+ {messages[currentMessageIndex]}
+
+
+ );
}
const PreviewPlanPage: React.FC = () => {
@@ -77,76 +87,43 @@ const PreviewPlanPage: React.FC = () => {
handleStartRecording,
handleStopRecording,
} = useVoiceHook();
+ const { data: deviceId, error } = useGenerateDeviceId();
const navigate = useNavigate();
- const { data: deviceId } = useGenerateDeviceId();
- const gptRequestMutation = useGptTrial();
- const savePlanMutation = useSavePlan();
-
- const handleSaveClick = async () => {
- if (!deviceId) {
- alert("Device ID를 생성하는 중입니다. 잠시 후 다시 시도해주세요.");
+ const handleNextClick = async () => {
+ if (!transcript || transcript.trim() === "") {
+ alert("입력값이 필요합니다.");
return;
}
-
- try {
- // GPT 요청 보내기
- const gptResponses = await gptRequestMutation.mutateAsync({
- deviceId,
- text: transcript,
- });
-
- console.log("GPT 응답 데이터:", gptResponses);
-
- // GPT 응답 데이터를 그대로 save API 호출에 전달
- await Promise.all(
- gptResponses.map((response) => {
- const { groupId, planCards } = response;
- return savePlanMutation.mutateAsync({
- deviceId,
- groupId,
- planCards: planCards.map((card) => ({
- ...card, // title, description, startDate, endDate를 그대로 유지
- accessibility: card.accessibility || true,
- isCompleted: card.isCompleted || false,
- })),
- });
- }),
- );
-
- navigate(RouterPath.PREVIEW_PLAN_SELECT);
- } catch (error) {
- console.error("GPT 요청 또는 플랜 저장 실패:", error);
- }
+ // 에러가 있으면 기본값을 사용
+ const safeDeviceId = error ? "defaultDeviceId" : deviceId;
+ navigate(RouterPath.PREVIEW_PLAN_SELECT, {
+ state: { transcript, deviceId: safeDeviceId },
+ });
};
return (
-
-
-
- setTranscript(e.target.value)}
- />
-
-
-
- 다음
-
- navigate(-1)}
- >
- 취소
-
-
-
+ 플랜을 생성해보세요.
+
+ ) =>
+ setTranscript(e.target.value)
+ }
+ placeholder="원하는 일정을 자유롭게 입력해보세요."
+ />
+
+
+ 다음
+ navigate(-1)} theme="secondary">
+ 취소
+
+
);
};
diff --git a/src/pages/PreviewPlan/PreviewPlanSelectPage.tsx b/src/pages/PreviewPlan/PreviewPlanSelectPage.tsx
index 16c90c8..ef98f9d 100644
--- a/src/pages/PreviewPlan/PreviewPlanSelectPage.tsx
+++ b/src/pages/PreviewPlan/PreviewPlanSelectPage.tsx
@@ -1,144 +1,180 @@
+import React, { useState, useEffect } from "react";
import styled from "@emotion/styled";
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
+import { AxiosError } from "axios";
import CustomCalendar, {
CalendarEvent,
} from "@/components/features/CustomCalendar/CustomCalendar";
-import NumberButton from "@/components/common/NumberButton/NumberButton";
+import {
+ useGptTrialLight,
+ useGptTrialModerate,
+ useGptTrialStrong,
+} from "@/api/hooks/useGeneratePlans";
import Button from "@/components/common/Button/Button";
+import NumberButton from "@/components/common/NumberButton/NumberButton";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
-import useGetPlanCard from "@/api/hooks/useGetPlanCard";
-import useGenerateDeviceId from "@/api/hooks/useGenerateDeviceId";
-const PreviewPlanSelectPageContainer = styled.div`
- display: grid;
+const PageContainer = styled.div`
+ display: flex;
+ flex-direction: column;
align-items: center;
- margin: 0 auto;
- margin-top: 20px;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-const CalendarSection = styled.div`
- margin-bottom: 40px;
- ${breakpoints.mobile} {
- margin-bottom: -50px;
- }
-`;
-
-const SidebarSection = styled.div`
- font-size: 30px;
+const Title = styled.h1`
+ font-size: 28px;
font-weight: bold;
- ${breakpoints.mobile} {
- font-size: 18px;
- }
-`;
-
-const StyledText = styled.p`
- text-align: center;
+ color: #2d3748;
+ margin-bottom: 24px;
`;
-const NumberButtonContainer = styled.div`
+const ButtonContainer = styled.div`
display: flex;
justify-content: center;
- align-items: center;
- gap: 10px;
+ gap: 16px;
+ margin-bottom: 24px;
`;
-const ButtonContainer = styled.div`
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 130px;
- margin-bottom: 30px;
- ${breakpoints.mobile} {
- gap: 30px;
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
+
+ @media (max-width: ${breakpoints.sm}px) {
+ padding: 0 20px;
}
`;
-const PreviewPlanSelectPage = () => {
- const { data: deviceId } = useGenerateDeviceId();
- const { data: plans } = useGetPlanCard(deviceId);
+const ActionButtonContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 120px;
+`;
- const [clickedNumber, setClickedNumber] = useState(null);
+const PreviewPlanSelectPage: React.FC = () => {
+ const { state } = useLocation();
+ const { transcript, deviceId } = state || {};
const navigate = useNavigate();
- const handleNumberButtonClick = (number: number) => {
- setClickedNumber(number);
+ const [planCache, setPlanCache] = useState>({
+ light: [],
+ moderate: [],
+ strong: [],
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [selectedLevel, setSelectedLevel] = useState<
+ "light" | "moderate" | "strong"
+ >("light");
+
+ const { mutate: fetchLightPlans } = useGptTrialLight();
+ const { mutate: fetchModeratePlans } = useGptTrialModerate();
+ const { mutate: fetchStrongPlans } = useGptTrialStrong();
+
+ const handleNextClick = async () => {
+ navigate(RouterPath.PREVIEW_PLAN_UPDATE, {
+ state: { plans: planCache[selectedLevel] },
+ });
};
- // 선택된 플랜 그룹의 이벤트 변환
- const selectedPlanGroup = plans?.find(
- (plan) => plan.groupId === String(clickedNumber),
- );
+ const handleFetchPlans = (level: "light" | "moderate" | "strong") => {
+ if (planCache[level].length > 0) {
+ setSelectedLevel(level);
+ return;
+ }
+ setIsLoading(true);
+
+ let fetchFn;
+ if (level === "light") {
+ fetchFn = fetchLightPlans;
+ } else if (level === "moderate") {
+ fetchFn = fetchModeratePlans;
+ } else {
+ fetchFn = fetchStrongPlans;
+ }
+
+ fetchFn(
+ { deviceId, text: transcript || "기본 추천 텍스트" },
+ {
+ onSuccess: (data) => {
+ if (typeof data === "string") {
+ alert(`잘못된 입력값입니다.\n응답데이터: ${data}`);
+ navigate(-1); // 이전 페이지로 이동
+ } else {
+ setPlanCache((prevCache) => ({
+ ...prevCache,
+ [level]: data.planCards,
+ }));
+ setSelectedLevel(level);
+ }
+ setIsLoading(false);
+ },
+ onError: (error: AxiosError) => {
+ if (error.response) {
+ if (error.response.status === 400) {
+ alert("잘못된 요청입니다.");
+ } else if (error.response.status === 500) {
+ alert("서버 내부 오류입니다.");
+ } else {
+ alert("플랜 요청 실패: 알 수 없는 오류");
+ }
+ } else {
+ alert(
+ "네트워크 오류가 발생했습니다. 유효한 입력값인지 확인해주세요.",
+ );
+ }
+ setIsLoading(false);
+ navigate(-1); // 이전 페이지로 이동
+ },
+ },
+ );
+ };
- const calendarEvents: CalendarEvent[] = selectedPlanGroup
- ? selectedPlanGroup.planCards.map((planCard) => ({
- id: planCard.cardId,
- title: planCard.title,
- description: planCard.description,
- start: new Date(planCard.startDate),
- end: new Date(planCard.endDate),
- accessibility: true, // 기본값 설정
- complete: false, // 기본값 설정
- }))
- : [];
+ useEffect(() => {
+ if (planCache.light.length === 0) {
+ handleFetchPlans("light");
+ } else {
+ setSelectedLevel("light");
+ }
+ }, []);
return (
-
-
- 원하는 플랜을 선택하세요.
-
- handleNumberButtonClick(1)}
- />
- handleNumberButtonClick(2)}
- />
- handleNumberButtonClick(3)}
- />
-
-
-
-
-
+ 원하는 플랜을 선택하세요
+
+ handleFetchPlans("light")}
+ />
+ handleFetchPlans("moderate")}
/>
-
+ handleFetchPlans("strong")}
+ />
+
-
-
- navigate(RouterPath.PREVIEW_PLAN_UPDATE, {
- state: {
- selectedPlan: calendarEvents,
- deviceId, // 실제 deviceId 값
- groupId: selectedPlanGroup?.groupId, // 선택된 그룹의 groupId },
- },
- })
- }
- >
- 확인
-
- navigate(-1)}
- >
+ {isLoading ? (
+ 로딩 중...
+ ) : (
+
+
+
+ )}
+
+ 다음
+ navigate(-1)} theme="secondary">
취소
-
-
+
+
);
};
diff --git a/src/pages/PreviewPlan/PreviewPlanUpdate.tsx b/src/pages/PreviewPlan/PreviewPlanUpdate.tsx
index ab6014e..2a52ce2 100644
--- a/src/pages/PreviewPlan/PreviewPlanUpdate.tsx
+++ b/src/pages/PreviewPlan/PreviewPlanUpdate.tsx
@@ -1,163 +1,118 @@
+// src/pages/TeamPlan/TeamPlanUpdatePage.tsx
import styled from "@emotion/styled";
import { useNavigate, useLocation } from "react-router-dom";
-import { keyframes } from "@emotion/react";
import { useState, useEffect } from "react";
-import CustomCalendar from "@/components/features/CustomCalendar/CustomCalendar";
+import { motion, AnimatePresence } from "framer-motion";
+import CustomCalendar, {
+ CalendarEvent,
+} from "@/components/features/CustomCalendar/CustomCalendar";
import Button from "@/components/common/Button/Button";
-
import breakpoints from "@/variants/breakpoints";
-// 슬라이드 애니메이션
-const slideDown = keyframes`
- 0% {
- opacity: 0;
- transform: translateY(-20px);
- }
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
-`;
-
-// 전체 컨테이너
const PlanUpdateContainer = styled.div`
- display: grid;
- align-items: center;
- margin: 0 auto;
- margin-top: 20px;
-`;
-
-// 내용 래퍼
-const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
- justify-content: center;
+ align-items: center;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-// 캘린더 컨테이너
-const CalendarContainer = styled.div`
- margin-bottom: 40px;
- ${breakpoints.mobile} {
- margin-bottom: -50px;
- }
+const TitleContainer = styled.div`
+ height: 70px;
+ margin-bottom: 24px;
`;
-// 텍스트 스타일
-const StyledText = styled.p`
- font-size: 30px; /* 기본 폰트 크기 */
+const StyledText = styled.h1`
+ font-size: 24px;
font-weight: bold;
text-align: center;
- &.animate {
- animation: ${slideDown} 1s ease-in-out;
- }
${breakpoints.mobile} {
- font-size: 18px;
- white-space: pre-line;
+ font-size: 20px;
+ margin-top: 20px;
}
`;
-const StyledTextContainer = styled.div`
- height: 70px;
- margin-bottom: 20px;
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
`;
-// 버튼 섹션
+
const ButtonContainer = styled.div`
display: flex;
- justify-content: center;
- align-items: center;
- gap: 130px; /* 버튼 간 간격 */
- margin-bottom: 30px;
- ${breakpoints.mobile} {
- gap: 30px;
- }
+ gap: 16px;
`;
+const loginUrl = import.meta.env.VITE_LOGIN_URL;
+
const PreviewPlanUpdate = () => {
const TitleMessages = [
"플랜을 수정하거나, 바로 저장하세요.",
- "일정을 옮기고 크기를 조정하여\n원하는대로 플랜을 수정해보세요",
+ "일정을 옮기고 크기를 조정하여 원하는대로 플랜을 수정해보세요.",
];
-
- // 메시지 애니메이션과 인덱스 상태
+ const { state } = useLocation();
+ const { plans: initialPlans } = state || {};
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
- const [animate, setAnimate] = useState(false);
- // 메시지와 애니메이션 설정
+ const [modifiedPlans, setModifiedPlans] = useState(
+ initialPlans || [],
+ );
+
+ const navigate = useNavigate();
+
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) =>
prevIndex === TitleMessages.length - 1 ? 0 : prevIndex + 1,
);
- setAnimate(true);
- setTimeout(() => {
- setAnimate(false);
- }, 1000);
- }, 5000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
- const navigate = useNavigate();
- const location = useLocation();
- const selectedPlan = location.state?.selectedPlan || [];
- const previewDeviceId = location.state?.deviceId;
- const previewGroupId = location.state?.groupId;
+ const handlePlanChange = (plans: CalendarEvent[]) => {
+ setModifiedPlans(plans);
+ };
const handleSave = async () => {
- try {
- if (!selectedPlan || selectedPlan.length === 0) {
- alert("저장할 플랜이 없습니다.");
- return;
- }
-
- if (!previewDeviceId || !previewGroupId) {
- alert("디바이스 ID 또는 그룹 ID가 없습니다.");
- return;
- }
-
- // 플랜 데이터를 로컬 스토리지에 저장
- localStorage.setItem(
- "previewPlanData",
- JSON.stringify({
- selectedPlan,
- previewDeviceId,
- previewGroupId,
- }),
- );
-
- // 카카오 로그인 페이지로 리다이렉트
- window.location.href =
- "https://api.splanet.co.kr/oauth2/authorization/kakao";
- } catch (error) {
- console.error("플랜 저장 중 오류 발생:", error);
- alert("플랜 저장에 실패했습니다. 다시 시도해주세요.");
- }
+ // plans 데이터를 sessionStorage에 저장
+ sessionStorage.setItem("plans", JSON.stringify(modifiedPlans));
+ window.location.href = loginUrl;
};
return (
-
-
-
- {TitleMessages[currentMessageIndex]}
-
-
-
-
-
-
-
- 저장
- navigate(-1)}>
- 취소
-
-
-
+
+
+
+ {TitleMessages[currentMessageIndex]}
+
+
+
+
+
+
+
+
+
+
+ 카카오로 계속하기
+
+ navigate(-1)}>
+ 취소
+
+
);
};
diff --git a/src/pages/TeamPlan/TeamPlan.tsx b/src/pages/TeamPlan/TeamPlan.tsx
index f711cfd..377b6a8 100644
--- a/src/pages/TeamPlan/TeamPlan.tsx
+++ b/src/pages/TeamPlan/TeamPlan.tsx
@@ -1,127 +1,155 @@
-/** @jsxImportSource @emotion/react */
+// src/pages/TeamPlan/TeamPlanPage.tsx
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import styled from "@emotion/styled";
+import { Close } from "@mui/icons-material";
+import { useQueries } from "@tanstack/react-query";
import {
useFetchTeams,
useDeleteTeam,
useLeaveTeam,
useRespondToInvitation,
useFetchInvitations,
+ useCancelTeamInvitation,
} from "@/api/hooks/useTeam";
import useUserData from "@/api/hooks/useUserData";
import Button from "@/components/common/Button/Button";
import breakpoints from "@/variants/breakpoints";
import { apiClient } from "@/api/instance";
+import { TeamInvitation } from "@/types/types";
-// Styles
const PageContainer = styled.div`
- width: 100%;
- max-width: 1200px;
- height: 100vh;
- padding: 10px 45px;
display: flex;
- flex-direction: column;
- font-family: "Inter", sans-serif;
+ min-height: 100vh;
+ background-color: #ffffff;
+ width: 100%;
box-sizing: border-box;
- @media (max-width: ${breakpoints.sm}px) {
- padding-top: 80px;
- }
- gap: 10px;
`;
-const PageTitle = styled.div`
- margin-right: auto;
- color: black;
- font-size: 23px;
+const ContentWrapper = styled.main`
+ flex-grow: 1;
+ padding: 32px;
+ overflow: auto;
+ box-sizing: border-box;
+`;
+
+const Heading = styled.h1`
+ font-size: 24px;
font-weight: 600;
+ margin-bottom: 24px;
+ color: #2d3748;
`;
const ButtonWrapper = styled.div`
- margin-left: auto;
- padding: 8px;
- justify-content: flex-end;
display: flex;
+ justify-content: flex-end;
+ margin-bottom: 24px;
gap: 8px;
`;
-const PlanCard = styled.div`
- width: 100%;
- height: 99px;
- padding: 12.8px;
- background: #f5f5f5;
+const TabsContainer = styled.div`
display: flex;
- justify-content: space-between;
- border-radius: 16px;
- align-items: center;
+ gap: 28px;
+ margin-bottom: 24px;
+ flex-wrap: wrap;
`;
-const PlanTitleContainer = styled.div`
+const Tab = styled.div<{ active: boolean }>`
+ font-size: 15px;
+ font-weight: ${(props) => (props.active ? 600 : 400)};
+ color: ${(props) => (props.active ? "#39a7f7" : "#9b9b9b")};
+ cursor: pointer;
+ transition: color 0.3s ease;
+`;
+
+const CardGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px;
+
+ @media (min-width: ${breakpoints.lg}px) {
+ grid-template-columns: 1fr 1fr;
+ }
+`;
+
+const PlanCard = styled.div`
+ background-color: #ffffff;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ padding: 24px;
+ transition: box-shadow 0.2s;
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
+ &:hover {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
+ }
`;
-const PlanTitle = styled.div`
- color: black;
+const PlanTitle = styled.h2`
font-size: 20px;
font-weight: 700;
+ color: #2d3748;
+ margin-bottom: 12px;
display: flex;
align-items: center;
`;
+const RoleBadge = styled.div<{ isAdmin: boolean }>`
+ background-color: ${(props) => (props.isAdmin ? "#ffc002" : "#a6caec")};
+ color: white;
+ font-size: 13px;
+ font-weight: 600;
+ padding: 4px 8px;
+ border-radius: 4px;
+ margin-left: 8px;
+`;
+
const Participants = styled.div`
- height: 32px;
- color: #aab2c8;
+ color: #4a5568;
font-size: 15px;
- font-weight: 700;
+ margin-bottom: 16px;
`;
-const TabsContainer = styled.div`
+const ButtonGroup = styled.div`
display: flex;
+ gap: 8px;
justify-content: flex-end;
- gap: 28px;
- width: 100%;
- margin-bottom: 20px;
- box-sizing: border-box;
+ align-items: center;
`;
-const Tab = styled.div<{ active: boolean }>`
- font-size: 15px;
- font-weight: ${(props) => (props.active ? 600 : 400)};
- color: ${(props) => (props.active ? "black" : "#9b9b9b")};
+const EmptyMessage = styled.div`
+ text-align: center;
+ color: #999;
+ font-size: 16px;
+ margin-top: 20px;
+`;
+
+const CancelIcon = styled(Close)`
cursor: pointer;
- transition: color 0.3s ease;
+ color: #ff4d4f;
+ &:hover {
+ color: #ff7875;
+ }
`;
-const RoleBadge = styled.div<{ isAdmin: boolean }>`
- width: 55px;
- height: 20px;
- background-color: ${(props) => (props.isAdmin ? "#a6caec" : "#ffc002")};
- color: white;
- font-size: 13px;
- font-weight: 600;
+const ParticipantList = styled.div`
display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 3px;
- margin-left: 8px; // PlanTitle과 간격 조정
+ flex-wrap: wrap;
+ gap: 8px;
`;
-const InviteeItem = styled.div`
+const ParticipantItem = styled.div`
display: flex;
- justify-content: space-between;
align-items: center;
+ background-color: #f0f4fa;
padding: 8px 12px;
- background: #f4f4f4;
border-radius: 8px;
- margin-bottom: 8px;
- font-size: 14px;
`;
-// InviteeItem 내부 텍스트 스타일링 (닉네임 & 상태)
-const NicknameText = styled.span`
- font-weight: bold;
- color: #333;
+const ParticipantName = styled.span`
+ margin-right: 4px;
+ color: #2d3748;
`;
export default function TeamPlanPage() {
@@ -133,26 +161,39 @@ export default function TeamPlanPage() {
const deleteTeamMutation = useDeleteTeam();
const leaveTeamMutation = useLeaveTeam();
const respondToInvitationMutation = useRespondToInvitation();
-
+ const cancelInvitationMutation = useCancelTeamInvitation();
const [activeTab, setActiveTab] = useState("teamList");
const [teamMembers, setTeamMembers] = useState<{ [key: number]: any[] }>({});
- const [sentInvitations, setSentInvitations] = useState<{
- [key: number]: any[];
- }>({});
- // 팀 멤버 정보 가져오기
+ const adminTeams = teams.filter((team) => {
+ const members = teamMembers[team.id] || [];
+ return members.some(
+ (member) => member.role === "ADMIN" && member.userId === userData.id,
+ );
+ });
+
+ const sentInvitationsQueries = useQueries({
+ queries: adminTeams.map((team) => ({
+ queryKey: ["sentInvitations", team.id],
+ queryFn: () =>
+ apiClient
+ .get(`/api/teams/${team.id}/invitations`)
+ .then((res) => res.data),
+ enabled: !!teamMembers[team.id],
+ })),
+ });
+
useEffect(() => {
const fetchAllMembers = async () => {
const memberData: { [key: number]: any[] } = {};
- // 모든 팀 멤버 데이터를 가져오는 API 호출
await Promise.all(
teams.map(async (team) => {
try {
const response = await apiClient.get(
`/api/teams/${team.id}/members`,
);
- memberData[team.id] = response.data; // 팀 ID를 키로 멤버 데이터 저장
+ memberData[team.id] = response.data;
} catch (error) {
console.error(`Error fetching members for team ${team.id}:`, error);
}
@@ -165,36 +206,15 @@ export default function TeamPlanPage() {
if (teams.length > 0) fetchAllMembers();
}, [teams]);
- // 보낸 초대 정보 가져오기
- useEffect(() => {
- const fetchSentInvitations = async () => {
- const invitationsData: { [key: number]: any[] } = {};
-
- await Promise.all(
- teams.map(async (team) => {
- try {
- const response = await apiClient.get(
- `/api/teams/${team.id}/invitations`,
- );
- invitationsData[team.id] = response.data;
- } catch (error) {
- console.error(
- `Error fetching sent invitations for team ${team.id}:`,
- error,
- );
- }
- }),
- );
-
- setSentInvitations(invitationsData);
- };
-
- if (teams.length > 0) fetchSentInvitations();
- }, [teams]);
-
- const handleVisitClick = (teamId: number, teamName: string) => {
- const members = teamMembers[teamId] || []; // 해당 팀의 멤버 목록을 가져옴
- navigate(`/team-plan/${teamId}`, { state: { teamName, teamId, members } });
+ const handleVisitClick = (
+ teamId: number,
+ teamName: string,
+ isAdmin: boolean,
+ ) => {
+ const members = teamMembers[teamId] || [];
+ navigate(`/team-plan/${teamId}`, {
+ state: { teamName, teamId, members, isAdmin, myId: userData.id },
+ });
};
const handleVisitMaking = () => {
@@ -209,136 +229,187 @@ export default function TeamPlanPage() {
respondToInvitationMutation.mutate({ invitationId, isAccepted: false });
};
- const renderedTeamList = teams.map((team) => {
- const members = teamMembers[team.id] || [];
- const isAdmin = members.some(
- (member) => member.role === "ADMIN" && member.userId === userData.id,
+ // 팀 목록 렌더링
+ const renderedTeamList =
+ teams.length > 0 ? (
+
+ {teams.map((team) => {
+ const members = teamMembers[team.id] || [];
+ const isAdmin = members.some(
+ (member) =>
+ member.role === "ADMIN" && member.userId === userData.id,
+ );
+
+ return (
+
+
+
+ {team.teamName}
+
+ {isAdmin ? "관리자" : "멤버"}
+
+
+
+ 참여자: {members.map((member) => member.nickname).join(", ")}
+
+
+
+
+ handleVisitClick(team.id, team.teamName, isAdmin)
+ }
+ >
+ 방문
+
+ {isAdmin ? (
+ deleteTeamMutation.mutate(team.id)}
+ >
+ 삭제
+
+ ) : (
+ leaveTeamMutation.mutate(team.id)}
+ >
+ 나가기
+
+ )}
+
+
+ );
+ })}
+
+ ) : (
+ 팀이 없습니다.
);
- return (
-
-
-
- {team.teamName}
-
- {isAdmin ? "관리자" : "멤버"}
-
-
-
-
- 참여자: {members.map((member) => member.nickname).join(", ")}
-
-
-
- handleVisitClick(team.id, team.teamName)}
- >
- 방문
-
- {isAdmin ? (
- deleteTeamMutation.mutate(team.id)}
- >
- 삭제
-
- ) : (
- leaveTeamMutation.mutate(team.id)}
- >
- 나가기
-
- )}
-
-
+ // 받은 요청 렌더링
+ const renderedInvitations =
+ invitations.length > 0 ? (
+
+ {invitations.map((invite) => (
+
+ {invite.teamName}
+
+ handleAcceptInvitation(invite.invitationId)}
+ >
+ 수락
+
+ handleRejectInvitation(invite.invitationId)}
+ >
+ 거절
+
+
+
+ ))}
+
+ ) : (
+ 받은 요청이 없습니다.
);
- });
- const renderedInvitations = invitations.map((invite) => (
-
-
- {invite.teamName}
-
-
- handleAcceptInvitation(invite.invitationId)}
- >
- 수락
-
- handleRejectInvitation(invite.invitationId)}
- >
- 거절
-
-
- ));
-
- const renderedSentInvitations = teams.map((team) => {
- const members = teamMembers[team.id] || [];
- const isAdmin = members.some(
- (member) => member.role === "ADMIN" && member.userId === userData.id,
- );
- if (!isAdmin) return null;
-
- const teamInvitations = sentInvitations[team.id] || [];
-
- return (
-
-
{team.teamName}
-
- {teamInvitations.map((invite) => (
-
- {invite.nickname}
-
- ))}
-
-
+ // 보낸 요청 렌더링
+ const renderedSentInvitations =
+ adminTeams.length > 0 ? (
+
+ {adminTeams.map((team, index) => {
+ const { data: teamInvitations = [], isLoading } =
+ sentInvitationsQueries[index];
+
+ if (isLoading) {
+ return (
+
+ 로딩 중...
+
+ );
+ }
+
+ if (teamInvitations.length === 0) return null;
+
+ const handleCancelInvitation = (invitationId: number) => {
+ if (window.confirm("초대를 취소하시겠습니까?")) {
+ cancelInvitationMutation.mutate(
+ { invitationId, teamId: team.id },
+ {
+ onSuccess: () => {
+ alert("초대가 취소되었습니다.");
+ },
+ },
+ );
+ }
+ };
+
+ return (
+
+
+
{team.teamName}
+
+ 초대한 멤버:
+
+ {teamInvitations.map((invite: TeamInvitation) => (
+
+ {invite.nickname}
+
+ handleCancelInvitation(invite.invitationId)
+ }
+ />
+
+ ))}
+
+
+
+
+ );
+ })}
+
+ ) : (
+ 보낸 요청이 없습니다.
);
- });
if (isLoadingTeams || isLoadingInvitations) return Loading...
;
return (
- 팀 플랜
-
-
- 팀 플랜 추가하기
-
-
-
-
- setActiveTab("teamList")}
- >
- 팀 목록
-
- setActiveTab("invitations")}
- >
- 받은 요청
-
- setActiveTab("sentInvitations")}
- >
- 보낸 요청
-
-
-
- {activeTab === "teamList" && renderedTeamList}
- {activeTab === "invitations" && renderedInvitations}
- {activeTab === "sentInvitations" && renderedSentInvitations}
+
+ 팀 플랜
+
+
+ 팀 플랜 추가하기
+
+
+
+
+ setActiveTab("teamList")}
+ >
+ 팀 목록
+
+ setActiveTab("invitations")}
+ >
+ 받은 요청
+
+ setActiveTab("sentInvitations")}
+ >
+ 보낸 요청
+
+
+
+ {activeTab === "teamList" && renderedTeamList}
+ {activeTab === "invitations" && renderedInvitations}
+ {activeTab === "sentInvitations" && renderedSentInvitations}
+
);
}
diff --git a/src/pages/TeamPlan/TeamPlanDetail.tsx b/src/pages/TeamPlan/TeamPlanDetail.tsx
index c3452df..db3f7bc 100644
--- a/src/pages/TeamPlan/TeamPlanDetail.tsx
+++ b/src/pages/TeamPlan/TeamPlanDetail.tsx
@@ -1,102 +1,214 @@
+// src/pages/TeamPlan/TeamPlanDetailPage.tsx
import styled from "@emotion/styled";
import { useLocation, useNavigate } from "react-router-dom";
+import { useQueryClient } from "@tanstack/react-query";
import CustomCalendar from "@/components/features/CustomCalendar/CustomCalendar";
import useGetTeamPlans from "@/api/hooks/useGetTeamPlan";
import { TeamMember } from "@/types/types";
import Button from "@/components/common/Button/Button";
import RouterPath from "@/router/RouterPath";
+import {
+ useRemoveUserFromTeam,
+ useUpdateUserRole,
+ useGetTeamMembers,
+} from "@/api/hooks/useTeam";
const PageContainer = styled.div`
- width: 100vh;
- max-width: 1200px;
- margin: 0 auto;
- padding: 16px;
- overflow-x: hidden;
+ display: flex;
+ min-height: 100vh;
+ background-color: #ffffff;
+`;
+
+const ContentWrapper = styled.main`
+ flex-grow: 1;
+ padding: 32px;
+ overflow: auto;
+ box-sizing: border-box;
`;
-const PageTitle = styled.div`
- align-self: stretch;
- color: black;
- font-size: 28.8px;
- font-family: "Inter", sans-serif;
+const Heading = styled.h1`
+ font-size: 24px;
font-weight: 600;
- line-height: 22.52px;
- letter-spacing: 0.36px;
- word-wrap: break-word;
+ margin-bottom: 24px;
+ color: #2d3748;
`;
-const SectionTitle = styled(PageTitle)`
- font-size: 1.2rem;
- margin-top: 24px;
- margin-bottom: 13px;
+const CalendarWrapper = styled.div`
+ margin-bottom: 32px;
`;
-export const ParticipantsContainer = styled.div`
- padding: 8px;
- border-radius: 12.8px;
- overflow: hidden;
- border: 2.4px #d5d5d5 solid;
- justify-content: flex-start;
- align-items: center;
- gap: 24px;
+const ParticipantsContainer = styled.div`
display: flex;
- margin-bottom: 13px;
- margin-right: 8px;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 24px;
`;
-export const Participant = styled.div`
- padding: 8px;
- background: #f4f4f4;
- border-radius: 12.8px;
- overflow: hidden;
- justify-content: center;
+const Participant = styled.div`
+ display: flex;
align-items: center;
- gap: 8px;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: #f0f4fa;
+ border-radius: 8px;
+ color: #2d3748;
+ font-size: 15px;
+ font-weight: 600;
+`;
+
+const ParticipantInfo = styled.div`
display: flex;
+ align-items: center;
+`;
+
+const RoleBadge = styled.span<{ isAdmin: boolean }>`
+ background-color: ${(props) => (props.isAdmin ? "#ffc002" : "#a6caec")};
+ color: #fff;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 12px;
+ margin-left: 8px;
`;
-export const ParticipantName = styled.div`
- color: black;
- font-size: 15.3px;
- font-family: "Inter", sans-serif;
- font-weight: 700;
- word-wrap: break-word;
+const AdminActions = styled.div`
+ display: flex;
+ gap: 8px;
+`;
+
+const ActionButton = styled.button`
+ background: none;
+ border: none;
+ color: #2d3748;
+ cursor: pointer;
+ padding: 4px;
+ font-size: 12px;
+ &:hover {
+ color: #3182ce;
+ }
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 16px;
+ gap: 16px;
`;
export default function TeamPlanDetailPage() {
const location = useLocation();
- const { teamName, teamId, members = [] } = location.state || {}; // 넘겨받은 멤버 목록
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const {
+ teamName,
+ teamId,
+
+ myId,
+ } = location.state || {};
const { data: teamPlans, isLoading, error } = useGetTeamPlans(teamId);
- const navigate = useNavigate();
+ const { data: members = [], refetch: refetchMembers } =
+ useGetTeamMembers(teamId);
+
+ const isAdmin = members.some(
+ (member: TeamMember) => member.userId === myId && member.role === "ADMIN",
+ );
+
+ const { mutate: removeUser } = useRemoveUserFromTeam({
+ onSuccess: () => {
+ // Invalidate and refetch team members
+ queryClient.invalidateQueries({ queryKey: ["teamPlans", teamId] });
+ refetchMembers();
+ },
+ onError: (err) => {
+ alert(`사용자를 제거하는 중 오류가 발생했습니다: ${err.message}`);
+ },
+ });
+
+ const { mutate: updateUserRole } = useUpdateUserRole({
+ onSuccess: () => {
+ // Invalidate and refetch team members
+ queryClient.invalidateQueries({ queryKey: ["teamPlans", teamId] });
+ refetchMembers();
+ },
+ onError: (err) => {
+ alert(
+ `사용자 역할을 업데이트하는 중 오류가 발생했습니다: ${err.message}`,
+ );
+ },
+ });
if (isLoading) return 로딩 중...
;
if (error) return 데이터를 불러오지 못했습니다. 오류: {error.message}
;
+ const handleRemoveUser = (userId: number) => {
+ if (window.confirm("정말로 이 사용자를 팀에서 제거하시겠습니까?")) {
+ removeUser({ teamId, userId });
+ }
+ };
+
+ const handleToggleUserRole = (userId: number) => {
+ updateUserRole({ teamId, userId, role: "toggle" });
+ };
+
return (
-
-
- 참여자
-
- {members.map((member: TeamMember) => (
-
- {member.nickname}
-
- ))}
-
-
- navigate(RouterPath.TEAM_PLAN_CHANGE, {
- state: { plans: teamPlans, teamId, teamName },
- })
- }
- >
- 수정하기
-
+
+ {teamName ? `${teamName} 계획표` : "팀 플랜"}
+
+
+
+
+ 참여자
+
+ {members.map((member: TeamMember) => (
+
+
+ {member.nickname}
+
+ {member.role === "ADMIN" ? "관리자" : "멤버"}
+
+
+ {isAdmin && member.userId !== myId && (
+
+ handleToggleUserRole(member.userId)}
+ >
+ {member.role === "ADMIN" ? "멤버로 변경" : "관리자 승급"}
+
+ handleRemoveUser(member.userId)}>
+ 제거
+
+
+ )}
+
+ ))}
+
+
+ {isAdmin && (
+
+
+ navigate(RouterPath.TEAM_PLAN_MODIFY, {
+ state: { plans: teamPlans, teamId, teamName },
+ })
+ }
+ >
+ 수정하기
+
+
+ navigate(RouterPath.TEAM_PLAN_INVITE, {
+ state: { teamId, teamName },
+ })
+ }
+ >
+ 초대하기
+
+
+ )}
+
);
}
diff --git a/src/pages/TeamPlan/TeamPlanInvite.tsx b/src/pages/TeamPlan/TeamPlanInvite.tsx
index ea58eb6..790322b 100644
--- a/src/pages/TeamPlan/TeamPlanInvite.tsx
+++ b/src/pages/TeamPlan/TeamPlanInvite.tsx
@@ -1,75 +1,93 @@
+// src/pages/TeamPlan/TeamInvitePage.tsx
/** @jsxImportSource @emotion/react */
import { useState } from "react";
-import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { useLocation, useNavigate } from "react-router-dom";
import Button from "@/components/common/Button/Button";
import { apiClient } from "@/api/instance";
import RouterPath from "@/router/RouterPath";
-// Styles
const PageContainer = styled.div`
- width: 100%;
- max-width: 1200px;
- padding: 20px;
- margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-const Title = styled.h2`
- font-size: 24px;
+const Title = styled.h1`
+ font-size: 28px;
font-weight: bold;
+ margin-bottom: 24px;
+ text-align: center;
+`;
+
+const TeamName = styled.h2`
+ font-size: 24px;
+ font-weight: normal;
+ margin-bottom: 16px;
text-align: center;
- margin-bottom: 20px;
+ color: #4a5568;
`;
-const searchBarStyles = css`
+const SearchBar = styled.div`
display: flex;
align-items: center;
- padding: 10px;
- background: #f4f4f4;
- border-radius: 16px;
- margin-bottom: 20px;
+ padding: 12px 16px;
+ background: #f0f4fa;
+ border-radius: 8px;
width: 100%;
- box-sizing: border-box;
+ max-width: 400px;
+ margin-bottom: 16px;
`;
-const searchInputStyles = css`
+const SearchInput = styled.input`
flex: 1;
border: none;
background: transparent;
- font-size: 15.28px;
- font-weight: 700;
- color: #aab2c8;
+ font-size: 16px;
+ color: #2d3748;
outline: none;
+
&::placeholder {
- color: #aab2c8;
+ color: #a0aec0;
}
`;
-const errorMessageStyles = css`
+const ErrorMessage = styled.div`
color: red;
- margin-top: 10px;
+ margin-bottom: 16px;
text-align: center;
`;
const InviteList = styled.div`
- margin-top: 20px;
- font-weight: bold;
+ width: 100%;
+ max-width: 400px;
+ margin-top: 24px;
`;
const InviteeItem = styled.div`
- margin-top: 5px;
+ padding: 8px 12px;
+ background: #f0f4fa;
+ border-radius: 8px;
+ margin-bottom: 8px;
font-size: 16px;
+ color: #2d3748;
+`;
+
+const FinishButton = styled(Button)`
+ margin-top: 32px;
`;
export default function TeamInvitePage() {
const { state } = useLocation();
- const { teamId } = state || {};
+ const { teamId, teamName } = state || {};
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const [errorMessage, setErrorMessage] = useState(null);
- const [inviteList, setInviteList] = useState([]); // 초대된 닉네임 목록
+ const [inviteList, setInviteList] = useState([]);
const handleInvite = async () => {
if (!searchQuery.trim()) {
@@ -80,14 +98,16 @@ export default function TeamInvitePage() {
await apiClient.post(`/api/teams/${teamId}/invite`, null, {
params: { nickname: searchQuery.trim() },
});
- setInviteList((prevList) => [...prevList, searchQuery.trim()]); // 초대된 인원 목록 업데이트
- setSearchQuery(""); // 입력창 초기화
- setErrorMessage(null); // 에러 메시지 초기화
+ setInviteList((prevList) => [...prevList, searchQuery.trim()]);
+ setSearchQuery("");
+ setErrorMessage(null);
} catch (error: any) {
if (error.response) {
switch (error.response.status) {
case 400:
- setErrorMessage("해당 유저는 이미 팀에 속해 있습니다.");
+ setErrorMessage(
+ "해당 유저는 이미 초대하였거나 팀에 속해 있습니다.",
+ );
break;
case 403:
setErrorMessage("권한이 없습니다.");
@@ -106,15 +126,15 @@ export default function TeamInvitePage() {
const handleSearchInputChange = (e: React.ChangeEvent) => {
setSearchQuery(e.target.value);
- setErrorMessage(null); // 입력할 때 에러 메시지 초기화
+ setErrorMessage(null);
};
return (
초대할 팀원을 검색해주세요
-
- 팀 이름: {teamName}}
+
+
추가
-
+
- {errorMessage && {errorMessage}
}
+ {errorMessage && {errorMessage}}
초대한 사람들:
@@ -137,7 +157,9 @@ export default function TeamInvitePage() {
)}
- navigate(RouterPath.TEAM_PLAN)}>마침
+ navigate(RouterPath.TEAM_PLAN)}>
+ 마침
+
);
}
diff --git a/src/pages/TeamPlan/TeamPlanMaking.tsx b/src/pages/TeamPlan/TeamPlanMaking.tsx
index 053c937..9446142 100644
--- a/src/pages/TeamPlan/TeamPlanMaking.tsx
+++ b/src/pages/TeamPlan/TeamPlanMaking.tsx
@@ -1,59 +1,54 @@
+// src/pages/TeamPlan/TeamPlanMaking.tsx
import styled from "@emotion/styled";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import { motion, AnimatePresence } from "framer-motion";
import Input from "@/components/common/Input/Input";
import Button from "@/components/common/Button/Button";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
import useVoiceHook from "@/hooks/useVoiceHook";
import MicrophoneButton from "@/components/features/MicrophoneButton/MicrophoneButton";
-// import useGenerateDeviceId from "@/api/hooks/useGenerateDeviceId";
const PlanPageContainer = styled.div`
- width: 60%;
- display: grid;
- justify-content: center;
- align-items: center;
-
-`;
-
-const InputWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
- gap: 40px;
- width: 100%;
+ padding: 32px;
+ box-sizing: border-box;
`;
-const Title = styled.p`
- font-size: 30px;
+const Title = styled.h1`
+ font-size: 32px;
font-weight: bold;
- color: #938e8e;
+ color: #2d3748;
text-align: center;
- margin: 50px 0 0 0;
+ margin-bottom: 24px;
+
${breakpoints.tablet} {
- font-size: 20px;
+ font-size: 28px;
}
`;
-const SubTitle = styled.p`
- font-size: 30px;
- font-weight: bold;
- color: #000;
+
+const SubTitle = styled.h2`
+ font-size: 24px;
+ font-weight: normal;
+ color: #4a5568;
text-align: center;
- margin: 0;
+ margin-bottom: 32px;
+
${breakpoints.tablet} {
- font-size: 18px;
+ font-size: 20px;
}
`;
const ButtonContainer = styled.div`
display: flex;
- gap: 130px;
- margin-bottom: 40px;
+ gap: 120px;
+ margin-top: 32px;
`;
-function MessageSilderWithAnimation() {
+function MessageSliderWithAnimation() {
const messages = [
"일정의 예상 소요 시간을 말해주시면 더 정확해요.",
"고정된 일정이 있나요?",
@@ -62,18 +57,29 @@ function MessageSilderWithAnimation() {
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
- // 타이머 실행
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % messages.length);
- }, 3000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
- return {messages[currentMessageIndex]};
+
+ return (
+
+
+ {messages[currentMessageIndex]}
+
+
+ );
}
const TeamPlanMakingPage: React.FC = () => {
- // const { data: deviceId } = useGenerateDeviceId();
const {
transcript,
setTranscript,
@@ -89,27 +95,26 @@ const TeamPlanMakingPage: React.FC = () => {
return (
-
-
-
- ) =>
- setTranscript(e.target.value)
- }
- />
-
-
- 다음
- navigate(-1)} theme="secondary">
- 취소
-
-
-
+ 팀 플랜 만들기
+
+ ) =>
+ setTranscript(e.target.value)
+ }
+ placeholder="원하는 일정을 입력하세요."
+ />
+
+
+ 다음
+ navigate(-1)} theme="secondary">
+ 취소
+
+
);
};
diff --git a/src/pages/TeamPlan/TeamPlanChange.tsx b/src/pages/TeamPlan/TeamPlanModify.tsx
similarity index 90%
rename from src/pages/TeamPlan/TeamPlanChange.tsx
rename to src/pages/TeamPlan/TeamPlanModify.tsx
index b9cc317..449c32d 100644
--- a/src/pages/TeamPlan/TeamPlanChange.tsx
+++ b/src/pages/TeamPlan/TeamPlanModify.tsx
@@ -1,6 +1,6 @@
+import { useState } from "react";
import styled from "@emotion/styled";
import { useLocation, useNavigate } from "react-router-dom";
-import { useState } from "react";
import CustomCalendar, {
CalendarEvent,
} from "@/components/features/CustomCalendar/CustomCalendar";
@@ -16,7 +16,14 @@ const PageContainer = styled.div`
padding: 16px;
`;
-export default function TeamPlanChangePage() {
+const ButtonGroup = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 20px;
+`;
+
+export default function TeamPlanModifyPage() {
const location = useLocation();
const { teamId, teamName, plans = [] } = location.state || {};
const [modifiedPlans, setModifiedPlans] = useState(plans);
@@ -74,6 +81,14 @@ export default function TeamPlanChangePage() {
},
]);
setIsAddModalOpen(false);
+ setNewPlanData({
+ title: "",
+ description: "",
+ startDate: "",
+ endDate: "",
+ accessibility: true,
+ isCompleted: false,
+ });
},
onError: (error) => {
alert(`추가 중 오류 발생: ${error.message}`);
@@ -129,7 +144,6 @@ export default function TeamPlanChangePage() {
return (
- 플랜 추가
- 저장
+
+
+ 플랜 추가
+
+ 저장
+
{isAddModalOpen && (
setIsAddModalOpen(false)}>
diff --git a/src/pages/TeamPlan/TeamPlanSelectPage.tsx b/src/pages/TeamPlan/TeamPlanSelectPage.tsx
index 6c146ed..677d10e 100644
--- a/src/pages/TeamPlan/TeamPlanSelectPage.tsx
+++ b/src/pages/TeamPlan/TeamPlanSelectPage.tsx
@@ -1,30 +1,58 @@
-import { useState, useEffect } from "react";
+// src/pages/TeamPlan/TeamPlanSelectPage.tsx
+import React, { useState, useEffect } from "react";
import styled from "@emotion/styled";
import { useLocation, useNavigate } from "react-router-dom";
import Cookies from "js-cookie";
import CustomCalendar, {
CalendarEvent,
-} from "../../components/features/CustomCalendar/CustomCalendar";
+} from "@/components/features/CustomCalendar/CustomCalendar";
import {
useGptLight,
useGptModerate,
useGptStrong,
-} from "@/api/hooks/useTeamPlan";
+} from "@/api/hooks/useGeneratePlans";
import Button from "@/components/common/Button/Button";
+import NumberButton from "@/components/common/NumberButton/NumberButton";
import RouterPath from "@/router/RouterPath";
+import breakpoints from "@/variants/breakpoints";
const PageContainer = styled.div`
- width: 100%;
- justify-content: center;
+ display: flex;
+ flex-direction: column;
align-items: center;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
+`;
+
+const Title = styled.h1`
+ font-size: 28px;
+ font-weight: bold;
+ color: #2d3748;
+ margin-bottom: 24px;
`;
const ButtonContainer = styled.div`
display: flex;
justify-content: center;
- gap: 10px;
- margin-bottom: 20px;
- margin-top: 20px;
+ gap: 16px;
+ margin-bottom: 24px;
+`;
+
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
+
+ @media (max-width: ${breakpoints.sm}px) {
+ padding: 0 20px;
+ }
+`;
+
+const ActionButtonContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ gap: 120px;
`;
const TeamPlanSelectPage: React.FC = () => {
@@ -32,7 +60,7 @@ const TeamPlanSelectPage: React.FC = () => {
const { transcript } = state || {};
const navigate = useNavigate();
const deviceId = Cookies.get("device_id") || "defaultDeviceId";
- // 각 레벨별 플랜을 저장하는 객체 상태
+
const [planCache, setPlanCache] = useState>({
light: [],
moderate: [],
@@ -49,15 +77,14 @@ const TeamPlanSelectPage: React.FC = () => {
const handleNextClick = async () => {
navigate(RouterPath.TEAM_PLAN_UPDATE, {
- state: { plans: planCache[selectedLevel] }, // 현재 선택된 플랜 전달
+ state: { plans: planCache[selectedLevel] },
});
};
- // 레벨에 따라 요청을 보내는 함수
const handleFetchPlans = (level: "light" | "moderate" | "strong") => {
if (planCache[level].length > 0) {
setSelectedLevel(level);
- return; // 이미 캐싱된 값이 있을 경우 요청 생략
+ return;
}
setIsLoading(true);
@@ -74,14 +101,11 @@ const TeamPlanSelectPage: React.FC = () => {
{ deviceId, text: transcript || "기본 추천 텍스트" },
{
onSuccess: (data) => {
- console.log("응답 데이터:", data.planCards);
setPlanCache((prevCache) => ({
...prevCache,
- [level]: data.planCards, // 각 레벨별 결과를 캐시에 저장
+ [level]: data.planCards,
}));
- console.log(data.planCards);
setSelectedLevel(level);
- console.log(level);
setIsLoading(false);
},
onError: (error) => {
@@ -92,46 +116,52 @@ const TeamPlanSelectPage: React.FC = () => {
);
};
- // 첫 화면 진입 시 자동으로 light 요청
useEffect(() => {
if (planCache.light.length === 0) {
- handleFetchPlans("light"); // 첫 진입 시 light 데이터 요청
+ handleFetchPlans("light");
} else {
- setSelectedLevel("light"); // 캐싱된 light 데이터가 있을 경우 바로 설정
+ setSelectedLevel("light");
}
- }, [planCache.light]);
+ }, []);
return (
+ 원하는 플랜을 선택하세요
- handleFetchPlans("light")}>
- 1 (Light)
-
- handleFetchPlans("moderate")}>
- 2 (Moderate)
-
- handleFetchPlans("strong")}>
- 3 (Strong)
-
+ handleFetchPlans("light")}
+ />
+ handleFetchPlans("moderate")}
+ />
+ handleFetchPlans("strong")}
+ />
{isLoading ? (
로딩 중...
) : (
-
+
+
+
)}
-
-
- 다음
-
- navigate(-1)} theme="secondary" size="small">
+
+ 다음
+ navigate(-1)} theme="secondary">
취소
-
+
);
};
diff --git a/src/pages/TeamPlan/TeamPlanUpdate.tsx b/src/pages/TeamPlan/TeamPlanUpdate.tsx
index 23f616e..7fe1166 100644
--- a/src/pages/TeamPlan/TeamPlanUpdate.tsx
+++ b/src/pages/TeamPlan/TeamPlanUpdate.tsx
@@ -1,7 +1,8 @@
+// src/pages/TeamPlan/TeamPlanUpdatePage.tsx
import styled from "@emotion/styled";
import { useNavigate, useLocation } from "react-router-dom";
-import { keyframes } from "@emotion/react";
import { useState, useEffect } from "react";
+import { motion, AnimatePresence } from "framer-motion";
import CustomCalendar, {
CalendarEvent,
} from "@/components/features/CustomCalendar/CustomCalendar";
@@ -11,73 +12,65 @@ import useSaveTeamPlan from "@/api/hooks/useTeamPlanSave";
import RouterPath from "@/router/RouterPath";
import breakpoints from "@/variants/breakpoints";
-// 슬라이드 애니메이션
-const slideDown = keyframes`
- 0% {
- opacity: 0;
- transform: translateY(-20px);
- }
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
-`;
-
-// 전체 컨테이너
const PlanUpdateContainer = styled.div`
- display: grid;
- align-items: center;
- max-width: 1440px;
- margin: 0 auto;
- margin-top: 20px;
-`;
-
-// 내용 래퍼
-const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
- justify-content: center;
align-items: center;
- gap: 40px;
+ padding: 32px;
+ min-height: 100vh;
+ box-sizing: border-box;
`;
-// 캘린더 컨테이너
-const CalendarContainer = styled.div`
- width: 100%;
+const TitleContainer = styled.div`
+ height: 70px;
+ margin-bottom: 24px;
`;
-// 텍스트 스타일
-const StyledText = styled.p`
- font-size: 23px;
+const StyledText = styled.h1`
+ font-size: 24px;
font-weight: bold;
text-align: center;
- &.animate {
- animation: ${slideDown} 1s ease-in-out;
- }
${breakpoints.mobile} {
- font-size: 16px;
+ font-size: 20px;
margin-top: 20px;
}
`;
-const StyledTextContainer = styled.div`
- height: 70px;
-`;
-
-// 버튼 섹션
-const ButtonContainer = styled.div`
+const TeamNameInputContainer = styled.div`
display: flex;
- justify-content: center;
align-items: center;
- gap: 130px;
- margin-bottom: 30px;
+ margin-bottom: 24px;
+`;
+
+const TeamNameLabel = styled.span`
+ font-size: 18px;
+ margin-right: 8px;
`;
const TeamNameInput = styled.input`
- font-size: 20px;
+ font-size: 18px;
padding: 8px;
- margin-left: 10px;
+ border: 1px solid #cbd5e0;
+ border-radius: 4px;
+ outline: none;
+ width: 200px;
+
+ &:focus {
+ border-color: #39a7f7;
+ box-shadow: 0 0 0 2px rgba(57, 167, 247, 0.2);
+ }
+`;
+
+const CalendarContainer = styled.div`
+ width: 100%;
+ max-width: 800px;
+ margin-bottom: 24px;
+`;
+
+const ButtonContainer = styled.div`
+ display: flex;
+ gap: 16px;
`;
const convertToSavePlanFormat = (event: CalendarEvent) => ({
@@ -92,17 +85,13 @@ const convertToSavePlanFormat = (event: CalendarEvent) => ({
const PlanUpdate = () => {
const TitleMessages = [
"플랜을 수정하거나, 바로 저장하세요.",
- "일정을 옮기고 크기를 조정하여 원하는대로 플랜을 수정해보세요",
+ "일정을 옮기고 크기를 조정하여 원하는대로 플랜을 수정해보세요.",
];
const { state } = useLocation();
- const { plans: initialPlans } = state || {}; // 이전 페이지에서 전달된 plans
- // 메시지 애니메이션과 인덱스 상태
+ const { plans: initialPlans } = state || {};
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
- const [animate, setAnimate] = useState(false);
- // 상태 정의
const [teamName, setTeamName] = useState("");
- // 초기 modifiedPlans에 전달된 plans 데이터 설정
const [modifiedPlans, setModifiedPlans] = useState(
initialPlans || [],
);
@@ -110,17 +99,12 @@ const PlanUpdate = () => {
const savePlanMutation = useSaveTeamPlan();
const navigate = useNavigate();
- // 메시지와 애니메이션 설정
useEffect(() => {
const interval = setInterval(() => {
setCurrentMessageIndex((prevIndex) =>
prevIndex === TitleMessages.length - 1 ? 0 : prevIndex + 1,
);
- setAnimate(true);
- setTimeout(() => {
- setAnimate(false);
- }, 1000);
- }, 5000);
+ }, 4000);
return () => clearInterval(interval);
}, []);
@@ -139,28 +123,18 @@ const PlanUpdate = () => {
}
try {
- // 팀 생성 요청
const teamResponse = await createTeamMutation.mutateAsync(teamName);
const teamId = teamResponse.data.id;
- // 모든 플랜을 저장하는 비동기 작업 생성
const savePlanPromises = modifiedPlans.map((plan) =>
savePlanMutation.mutateAsync({
teamId,
- plan: convertToSavePlanFormat(plan), // 변환 후 전달
+ plan: convertToSavePlanFormat(plan),
}),
);
- // 모든 비동기 작업을 병렬로 실행
await Promise.all(savePlanPromises);
- // for (const plan of modifiedPlans) {
- // await savePlanMutation.mutateAsync({
- // teamId,
- // plan: convertToSavePlanFormat(plan), // 변환 후 전달
- // });
- // }
-
alert("저장이 완료되었습니다!");
navigate(RouterPath.TEAM_PLAN_INVITE, { state: { teamId } });
} catch (error) {
@@ -171,37 +145,43 @@ const PlanUpdate = () => {
return (
-
-
-
- {TitleMessages[currentMessageIndex]}
-
-
-
-
- 팀의 이름을 정해주세요:
-
-
-
-
-
-
-
-
- 저장
- navigate(-1)}>
- 취소
-
-
-
+
+
+
+ {TitleMessages[currentMessageIndex]}
+
+
+
+
+
+ 팀의 이름을 정해주세요:
+
+
+
+
+
+
+
+
+ 저장
+ navigate(-1)}>
+ 취소
+
+
);
};
diff --git a/src/provider/AuthProvider.tsx b/src/provider/AuthProvider.tsx
index bd7f624..8ba3180 100644
--- a/src/provider/AuthProvider.tsx
+++ b/src/provider/AuthProvider.tsx
@@ -1,8 +1,14 @@
-import { createContext, useState, useMemo, PropsWithChildren } from "react";
+import {
+ createContext,
+ useState,
+ useMemo,
+ PropsWithChildren,
+ useEffect,
+} from "react";
+import { authEventEmitter } from "@/api/instance";
interface AuthState {
isAuthenticated: boolean;
- accessToken: string | null;
}
interface AuthContextProps {
@@ -13,12 +19,33 @@ interface AuthContextProps {
const AuthContext = createContext(undefined);
export const AuthProvider = ({ children }: PropsWithChildren