diff --git a/src/api/@types/Groups.ts b/src/api/@types/Groups.ts index f50ac48..cc2b53b 100644 --- a/src/api/@types/Groups.ts +++ b/src/api/@types/Groups.ts @@ -1,4 +1,4 @@ -// get +// get all groups export interface GetRetrospectiveGroupsNodes { id: number; title: string; @@ -27,3 +27,78 @@ export interface GetRetrospectiveGroupsRequest { keyword: string; isBookmarked: boolean; } + +// post +export interface PostRetrospectivesGroupRequest { + title: string; + status: string; + thumbnail: string | null; + description: string; +} + +export interface PostRetrospectivesGroupResponse { + code: number; + message: string; + data: Array; +} + +export interface PostRetrospectivesGroupNodes { + id: number; + title: string; + userId: number; + description: string; + status: string; + thumbnail: string; +} + +// delete +export interface DeleteRetrospectiveRequest { + retrospectiveGroupId: number; +} + +// put +export interface PutRetrospectiveGroupRequest { + title: string; + status: string; + thumbnail: string | null; + description: string; +} + +export interface PutRetrospectiveGroupResponse { + code: number; + title: string; + data: Array; +} + +export interface PutRetrospectivesGroupNodes { + id: number; + title: string; + userId: number; + userName: string; + status: string; + isBookmarked: boolean; + thumbnail: string; + description: string; + updatedDate: Date | string; +} + +// get a group +export interface GetRetrospectiveGroupRequest { + retrospectiveGroupId: number; +} + +export interface GetRetrospectiveGroupResponse { + code: number; + message: string; + data: Array; +} + +export interface GetRetrospectiveGroupNodes { + title: string; + userId: number; + userName: string; + description: string; + thumnail: string | null; + status: string; + id: number; +} diff --git a/src/api/@types/Notification.ts b/src/api/@types/Notification.ts index 5e92d48..fe7b67a 100644 --- a/src/api/@types/Notification.ts +++ b/src/api/@types/Notification.ts @@ -15,6 +15,8 @@ export interface NotificationData { thumbnail: string; notificationType: TNotificationType; dateTime: string; + retrospectiveId: number; + teamId: number; } export interface PostNotificationRequest { diff --git a/src/api/@types/Retrospectives.ts b/src/api/@types/Retrospectives.ts index 312d6cd..330cd2c 100644 --- a/src/api/@types/Retrospectives.ts +++ b/src/api/@types/Retrospectives.ts @@ -132,11 +132,34 @@ export interface PatchRetrospectiveResponse { data: boolean; } +export interface PostTransferLeaderRequest { + newLeaderId: number; + retrospectiveId: number; +} + +export interface PostTransferLeaderResponse { + code: number; + message: string; +} + +export interface LeaderData { + id: number; + title: string; + teamId: number; + userId: number; + templateId: number; + status: keyof TStatus; + thumbnail: string; + startedDate: string; + description: string; +} + export interface RetrospectivesClient { onlyGet(request: onlyGetRetrospectiveRequest): Promise; create(request: PostRetrospectivesRequest): Promise; get(request: GetRetrospectiveRequest): Promise; delete(request: DeleteRetrospectiveRequest): Promise; put(request: PutTeamRetrospectiveRequest): Promise; + leaderPost(request: PostTransferLeaderRequest): Promise; patch(request: PatchRetrospectiveRequest): Promise; } diff --git a/src/api/@types/Section.ts b/src/api/@types/Section.ts index bb67d2f..d4f76f0 100644 --- a/src/api/@types/Section.ts +++ b/src/api/@types/Section.ts @@ -28,11 +28,14 @@ export interface ActionItemData { } export interface CommentData { + sectionId: number; commentId: number; userId: number; content: string; username: string; thumbnail: string; + lastModifiedDate: string; + createdDate: string; } export interface AddedImageCommentData extends CommentData { diff --git a/src/api/@types/Users.ts b/src/api/@types/Users.ts index 1fa61db..2ccf7bf 100644 --- a/src/api/@types/Users.ts +++ b/src/api/@types/Users.ts @@ -14,6 +14,12 @@ export interface UserData { updatedDate: string; } +export interface PostAdminRequest { + email: string; + admin: boolean; +} + export interface UserClient { get(): Promise; + adminPost(reuest: PostAdminRequest): Promise; } diff --git a/src/api/imageApi/postImageToS3.tsx b/src/api/imageApi/postImageToS3.tsx index 10cb724..cee0e53 100644 --- a/src/api/imageApi/postImageToS3.tsx +++ b/src/api/imageApi/postImageToS3.tsx @@ -5,7 +5,6 @@ import { PostImageToS3Request, PostImageToS3Response } from '@/api/@types/Thumbn const postImageToS3 = async (requestData: PostImageToS3Request): Promise => { try { const response = await axiosInstance.post('/s3/pre-signed-url', requestData); - console.log('사진 s3 요청 성공', response.data); return response.data; } catch (error) { throw new Error('s3 요청 실패'); diff --git a/src/api/retroGroupsApi/deleteGroup.tsx b/src/api/retroGroupsApi/deleteGroup.tsx new file mode 100644 index 0000000..09856e2 --- /dev/null +++ b/src/api/retroGroupsApi/deleteGroup.tsx @@ -0,0 +1,11 @@ +import { DeleteRetrospectiveRequest } from '@/api/@types/Groups'; +import axiosInstance from '@/api/axiosConfig'; + +export const DeleteGroup = async ({ retrospectiveGroupId }: DeleteRetrospectiveRequest): Promise => { + try { + const response = await axiosInstance.delete(`/retrospectiveGroups/${retrospectiveGroupId}`); + return response.data; + } catch (error) { + throw new Error(error as string); + } +}; diff --git a/src/api/retroGroupsApi/getGroup.tsx b/src/api/retroGroupsApi/getGroup.tsx new file mode 100644 index 0000000..2abb48a --- /dev/null +++ b/src/api/retroGroupsApi/getGroup.tsx @@ -0,0 +1,19 @@ +import { GetRetrospectiveGroupRequest, GetRetrospectiveGroupResponse } from '@/api/@types/Groups'; +import axiosInstance from '@/api/axiosConfig'; + +export const GetRetrospectiveGroup = async ({ + retrospectiveGroupId, + ...request +}: GetRetrospectiveGroupRequest): Promise => { + try { + const response = await axiosInstance.get( + `/retrospectiveGroups/${retrospectiveGroupId}`, + request, + ); + return response.data; + } catch (error) { + throw new Error('단일 그룹 조회 실패'); + } +}; + +export default GetRetrospectiveGroup; diff --git a/src/api/retroGroupsApi/postGroup.tsx b/src/api/retroGroupsApi/postGroup.tsx new file mode 100644 index 0000000..894e780 --- /dev/null +++ b/src/api/retroGroupsApi/postGroup.tsx @@ -0,0 +1,13 @@ +import { PostRetrospectivesGroupRequest, PostRetrospectivesGroupResponse } from '@/api/@types/Groups'; +import axiosInstance from '@/api/axiosConfig'; + +const postGroup = async (requestData: PostRetrospectivesGroupRequest): Promise => { + try { + const response = await axiosInstance.post('/retrospectiveGroups', requestData); + return response.data; + } catch (error) { + throw new Error('그룹 생성 실패'); + } +}; + +export default postGroup; diff --git a/src/components/projectRetro/EditModal.tsx b/src/api/retroGroupsApi/putGroup.tsx similarity index 100% rename from src/components/projectRetro/EditModal.tsx rename to src/api/retroGroupsApi/putGroup.tsx diff --git a/src/api/services/Retrospectives.ts b/src/api/services/Retrospectives.ts index c82bfc1..7b3bf6c 100644 --- a/src/api/services/Retrospectives.ts +++ b/src/api/services/Retrospectives.ts @@ -6,6 +6,8 @@ import { onlyGetRetrospectiveResponse, PostRetrospectivesRequest, PostRetrospectivesResponse, + PostTransferLeaderRequest, + PostTransferLeaderResponse, RetrospectivesClient, } from '../@types/Retrospectives'; import axiosInstance from '../axiosConfig'; @@ -57,6 +59,19 @@ export const RetrospectiveService: RetrospectivesClient = { throw new Error(error as string); } }, + leaderPost: async ({ + retrospectiveId, + newLeaderId, + }: PostTransferLeaderRequest): Promise => { + try { + const response = await axiosInstance.post( + `${ROUTE}/${retrospectiveId}/transferLeadership?newLeaderId=${newLeaderId}`, + ); + return response.data; + } catch (error) { + throw new Error(error as string); + } + }, patch: async (retrospectiveId, ...request) => { return await axiosInstance.patch(`${ROUTE}/${retrospectiveId}/bookmark`, request); diff --git a/src/api/services/User.ts b/src/api/services/User.ts index 97d76ea..bb75abd 100644 --- a/src/api/services/User.ts +++ b/src/api/services/User.ts @@ -1,4 +1,4 @@ -import { UserClient } from '../@types/Users'; +import { PostAdminRequest, UserClient } from '../@types/Users'; import axiosInstance from '../axiosConfig'; const ROUTE = 'users'; @@ -12,4 +12,12 @@ export const UserServices: UserClient = { throw new Error(error as string); } }, + adminPost: async (request: PostAdminRequest) => { + try { + const response = await axiosInstance.post(`/${ROUTE}/me/admin-status`, request); + return response.data; + } catch (error) { + throw new Error(error as string); + } + }, }; diff --git a/src/components/alarm/Alarm.tsx b/src/components/alarm/Alarm.tsx index 75a6b0a..8505f48 100644 --- a/src/components/alarm/Alarm.tsx +++ b/src/components/alarm/Alarm.tsx @@ -39,18 +39,21 @@ const Alarm = () => { const filterNotification = (notification: NotificationData[]) => { const todayFiltered: NotificationData[] = []; const otherFiltered: NotificationData[] = []; - notification.forEach(item => { - const dateTime = new Date(item.dateTime); - if (dateTime.toISOString().slice(0, 10) === today.toISOString().slice(0, 10)) { - todayFiltered.push(item); - } else { - otherFiltered.push(item); - } - setTodayNotification(todayFiltered); - setOtherNotification(otherFiltered); - }); + notification + .filter(item => item.receiverId === user?.userId) + .forEach(item => { + const dateTime = new Date(item.dateTime); + if (dateTime.toISOString().slice(0, 10) === today.toISOString().slice(0, 10)) { + todayFiltered.push(item); + } else { + otherFiltered.push(item); + } + + setTodayNotification(todayFiltered); + setOtherNotification(otherFiltered); + }); }; - console.log('today', todayNotification); + const fetchUser = async () => { try { const data = await UserServices.get(); @@ -107,13 +110,15 @@ const Alarm = () => { {notification && notification.length > 0 && ( <> - {notification.length} + + {notification.filter(item => item.receiverId === user?.userId).length} + )} - + @@ -128,69 +133,87 @@ const Alarm = () => { 최근에 받은 알림 - {todayNotification && todayNotification.length > 0 ? ( - todayNotification.map(item => ( - navigate(`/`)}> - - - [{item.retrospectiveTitle}]에서 알림{' '} - - - - - { - ReadNotification(item.notificationId); - }} - /> - - {item.senderName}님이 {NOTIFICATION_TYPE_LABEL[item.notificationType]} - {item.notificationType === 'COMMENT' ? '을' : '를'}{' '} - {item.notificationType === 'COMMENT' ? '작성했습니다.' : '남겼습니다'} - - - - - {convertToLocalTime(item.dateTime)} - - - )) + {notification && todayNotification.length > 0 ? ( + todayNotification + .filter(item => item.receiverId === user?.userId) + .map(item => ( + + navigate(`/sections?retrospectiveId=${item.retrospectiveId}&teamId=${item.teamId}`) + } + > + + + [{item.retrospectiveTitle}]에서 알림{' '} + + + + + { + ReadNotification(item.notificationId); + }} + /> + + {item.senderName}님이 {NOTIFICATION_TYPE_LABEL[item.notificationType]} + {item.notificationType === 'COMMENT' ? '을' : '를'}{' '} + {item.notificationType === 'COMMENT' ? '작성했습니다.' : '남겼습니다'} + + + + + {convertToLocalTime(item.dateTime)} + + + )) ) : ( 알림 없음 )} - 저번에 받은 알림 + 저번에 받은 알림 {otherNotification && otherNotification.length > 0 ? ( - otherNotification.map(item => ( - navigate(`/`)}> - - - [{item.retrospectiveTitle}]에서 알림{' '} - - - - - { - ReadNotification(item.notificationId); - }} - /> - - {item.senderName}님이 {NOTIFICATION_TYPE_LABEL[item.notificationType]} - {item.notificationType === 'COMMENT' ? '을' : '를'}{' '} - {item.notificationType === 'COMMENT' ? '작성했습니다.' : '남겼습니다.'} - - - - - {convertToLocalTime(item.dateTime)} - - - )) + otherNotification + .filter(item => item.receiverId === user?.userId) + .map(item => ( + + navigate(`/sections?retrospectiveId=${item.retrospectiveId}&teamId=${item.teamId}`) + } + > + + + [{item.retrospectiveTitle}]에서 알림{' '} + + + + + { + ReadNotification(item.notificationId); + }} + /> + + {item.senderName}님이 {NOTIFICATION_TYPE_LABEL[item.notificationType]} + {item.notificationType === 'COMMENT' ? '을' : '를'}{' '} + {item.notificationType === 'COMMENT' ? '작성했습니다.' : '남겼습니다.'} + + + + + {convertToLocalTime(item.dateTime)} + + + )) ) : ( 알림 없음 )} diff --git a/src/components/createRetro/modal/CreateModal.tsx b/src/components/createRetro/modal/CreateModal.tsx index 5b226d9..44e1594 100644 --- a/src/components/createRetro/modal/CreateModal.tsx +++ b/src/components/createRetro/modal/CreateModal.tsx @@ -152,6 +152,7 @@ const CreateModal: React.FC = ({ isOpen, onClose, templateId, setRequestData({ ...requestData, thumbnail: imageUUID }); setImage(file); }} + text="PC에서 이미지 선택" /> diff --git a/src/components/createRetro/modal/ImageUpload.tsx b/src/components/createRetro/modal/ImageUpload.tsx index 3c1e797..62079ae 100644 --- a/src/components/createRetro/modal/ImageUpload.tsx +++ b/src/components/createRetro/modal/ImageUpload.tsx @@ -4,9 +4,10 @@ import { v4 as uuidv4 } from 'uuid'; interface ImageUploadProps { onChange: (file: File | null, uuid: string) => void; // 파일 객체, uuid 함께 전달 + text: string; } -const ImageUpload: React.FC = ({ onChange }) => { +const ImageUpload: React.FC = ({ onChange, text }) => { const [imagePreview, setImagePreview] = useState(null); const [_, setImageUUID] = useState(null); // 상태를 활용할 수 있도록 수정 @@ -41,7 +42,7 @@ const ImageUpload: React.FC = ({ onChange }) => {
diff --git a/src/components/layout/parts/MainNavBar.tsx b/src/components/layout/parts/MainNavBar.tsx index a3a0221..cc506ee 100644 --- a/src/components/layout/parts/MainNavBar.tsx +++ b/src/components/layout/parts/MainNavBar.tsx @@ -27,7 +27,7 @@ const MainNavBar = () => {
{ - return createmodal; -}; - -export default CreateModal; diff --git a/src/components/projectRetro/DeleteModal.tsx b/src/components/projectRetro/DeleteModal.tsx new file mode 100644 index 0000000..7143813 --- /dev/null +++ b/src/components/projectRetro/DeleteModal.tsx @@ -0,0 +1,55 @@ +import { IoMdClose } from 'react-icons/io'; +import { RiFolder6Fill } from 'react-icons/ri'; +import { DeleteGroup } from '@/api/retroGroupsApi/deleteGroup'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import * as S from '@/styles/projectRetro/DeleteModal.styles'; + +interface DeleteModalProps { + isClose: () => void; + modalClose: () => void; + groupId: number; +} + +const DeleteModal: React.FC = ({ isClose, modalClose, groupId }) => { + const toast = useCustomToast(); + const handleDeleteGroup = async () => { + try { + await DeleteGroup({ retrospectiveGroupId: groupId }); + setTimeout(() => { + isClose(); + modalClose(); + toast.info('그룹이 삭제되었습니다.'); + }, 1000); + } catch (e) { + toast.error('그룹 삭제에 실패했습니다.'); + } + }; + + return ( + + + + + 회고 프로젝트 삭제 + + +
+ +
+ + 프로젝트1 {/* 프로젝트 이름 */} + 를 삭제하시겠습니까? +
+ Delete +
+
+
+
+ ); +}; + +export default DeleteModal; diff --git a/src/components/projectRetro/DescriptionInput.tsx b/src/components/projectRetro/DescriptionInput.tsx new file mode 100644 index 0000000..5e8c8c4 --- /dev/null +++ b/src/components/projectRetro/DescriptionInput.tsx @@ -0,0 +1,16 @@ +import * as S from '@/styles/projectRetro/Modal.styles'; + +interface DescriptionInputProps { + onChange: (description: string) => void; + placeholder: string; +} + +const DescriptionInput: React.FC = ({ onChange, placeholder }) => { + return ( + <> + onChange(e.target.value)}> + + ); +}; + +export default DescriptionInput; diff --git a/src/components/projectRetro/GroupList.tsx b/src/components/projectRetro/GroupList.tsx index a32278a..a3b1593 100644 --- a/src/components/projectRetro/GroupList.tsx +++ b/src/components/projectRetro/GroupList.tsx @@ -1,3 +1,6 @@ +import { useState } from 'react'; +import { MdOutlineMoreHoriz } from 'react-icons/md'; +import Modal from '@/components/projectRetro/Modal'; import * as S from '@/styles/projectRetro/GroupList.styles'; export interface RetroGroup { @@ -12,15 +15,24 @@ interface GroupListProps { } const GroupList: React.FC = ({ groups }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const handleModal = () => { + setIsModalOpen(true); + }; + return ( - - {groups.map(group => ( -
-

{group.title}

-

{group.description}

-
- ))} -
+ <> + {isModalOpen && setIsModalOpen(false)} type="edit" groupId={6} />} + + {groups.map(group => ( +
+

{group.title}

+

{group.description}

+
+ ))} + +
+ ); }; diff --git a/src/components/projectRetro/Modal.tsx b/src/components/projectRetro/Modal.tsx new file mode 100644 index 0000000..159b17b --- /dev/null +++ b/src/components/projectRetro/Modal.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState } from 'react'; +import { CgTimelapse } from 'react-icons/cg'; // ing +import { FaRegCircleCheck } from 'react-icons/fa6'; // done +import { IoMdClose } from 'react-icons/io'; +import { IoIosArrowDown } from 'react-icons/io'; +import axios from 'axios'; +import { + PostRetrospectivesGroupRequest, + GetRetrospectiveGroupRequest, + GetRetrospectiveGroupResponse, +} from '@/api/@types/Groups'; +import postImageToS3 from '@/api/imageApi/postImageToS3'; +import { GetRetrospectiveGroup } from '@/api/retroGroupsApi/getGroup'; +import postGroup from '@/api/retroGroupsApi/postGroup'; +import ImageUpload from '@/components/createRetro/modal/ImageUpload'; +import DeleteModal from '@/components/projectRetro/DeleteModal'; +import DescriptionInput from '@/components/projectRetro/DescriptionInput'; +import TitleInput from '@/components/projectRetro/TitleInput'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import * as S from '@/styles/projectRetro/Modal.styles'; + +interface ModalProps { + isClose: () => void; + type: string; + groupId?: number; +} + +const Modal: React.FC = ({ isClose, type, groupId }) => { + const toast = useCustomToast(); + const [isOpen, setIsOpen] = useState(false); + const [statusOption, setStatusOption] = useState('ING'); + const statusList = ['ING', 'DONE']; + const statusObj: { [key: string]: string } = { ING: 'IN_PROGRESS', DONE: 'COMPLETED' }; + const availableOption = statusList.filter(statusList => statusList !== statusOption); + const [deleteModal, setDeleteModal] = useState(false); + + const handleDeleteModal = () => { + setDeleteModal(true); + }; + + const [requestData, setRequestData] = useState({ + title: '', + status: statusObj.ING, + thumbnail: null, + description: '', + }); + + const [image, setImage] = useState(null); + + const handleCreateGroup = async () => { + try { + if (!requestData.title) { + alert('그룹 이름을 입력해 주세요.'); + return; + } + if (!requestData.description) { + requestData.description = ''; + return; + } + + if (requestData.thumbnail) { + const imageResponse = await postImageToS3({ + filename: requestData.thumbnail, + method: 'PUT', + }); + + const imageURL = imageResponse.data.preSignedUrl; + + const uploadResponse = await axios.put(imageURL, image, { + headers: { + 'Content-Type': image?.type, + }, + }); + + if (uploadResponse.status === 200) { + console.log('사진 form-data 성공', uploadResponse); + } else { + console.error('사진 업로드 실패'); + } + } + + // put 요청 전송 + await postGroup({ + ...requestData, + thumbnail: requestData.thumbnail || null, + }); + + isClose(); + } catch (error) { + toast.error('그룹 생성에 실패했습니다'); + } + }; + + const [groupData, setGroupData] = useState([]); + + // 단일 그룹 조회 + useEffect(() => { + if (type === 'edit' && groupId !== undefined) { + const fetchGroup = async () => { + try { + const requestData: GetRetrospectiveGroupRequest = { + retrospectiveGroupId: groupId, + }; + const responseData = await GetRetrospectiveGroup(requestData); + setGroupData(responseData.data); + console.log('단일 그룹 불러오기', responseData); + } catch (error) { + toast.error('그룹 불러오기에 실패했습니다'); + } + }; + fetchGroup(); + } + }, []); + + const handleEditGroup = async () => {}; + + const handleToggle = () => { + setIsOpen(!isOpen); + }; + + const handleStatusOption = (option: string) => { + setStatusOption(option); + setIsOpen(false); + }; + + return ( + + + + + + Project + {type === 'create' ? '생성하기' : '수정하기'} + + + {type === 'create' && ( + { + setRequestData({ ...requestData, thumbnail: imageUUID }); + setImage(file); + }} + text="PC에서 이미지 선택" + /> + )} + {type === 'edit' && ( + <> + {!requestData.thumbnail && 변경 전 사진이 없으면 보이지 않습니다.} + { + setRequestData({ ...requestData, thumbnail: imageUUID }); + setImage(file); + }} + text="변경하기" + /> + + )} + + 0 ? `${groupData[0].title}` : 'Project Name *'} + onChange={title => setRequestData({ ...requestData, title })} + /> + + 프로젝트 설명 + 0 ? `${groupData[0].description}` : '프로젝트에 대한 설명을 입력해 주세요.' + } + onChange={description => setRequestData({ ...requestData, description })} + /> + + + 프로젝트 상태 + + {statusOption === 'ING' ? ( + + ) : ( + + )} +   {statusOption} + {statusOption === 'ING' &&  (default)} + + + {isOpen && ( +
+ {availableOption.map((option, index) => ( + { + handleStatusOption(option); + setRequestData({ ...requestData, status: statusObj[option] }); + }} + > + {option === 'ING' ? ( + + ) : ( + + )} +  {option} + + ))} +
+ )} +
+ + + {type === 'create' && 'Create'} + {type === 'edit' && 'Edit'} + + {type === 'edit' && Delete} + +
+
+ {deleteModal && setDeleteModal(false)} modalClose={isClose} />}{' '} + {/* groupId 받아오기 */} +
+ ); +}; + +export default Modal; diff --git a/src/components/projectRetro/TitleInput.tsx b/src/components/projectRetro/TitleInput.tsx new file mode 100644 index 0000000..6bb6906 --- /dev/null +++ b/src/components/projectRetro/TitleInput.tsx @@ -0,0 +1,16 @@ +import * as S from '@/styles/projectRetro/Modal.styles'; + +interface TitleInputProps { + onChange: (title: string) => void; + placeholder: string; +} + +const TitleInput: React.FC = ({ onChange, placeholder }) => { + return ( + <> + onChange(e.target.value)}> + + ); +}; + +export default TitleInput; diff --git a/src/components/writeRetro/explainModal/explainRetro.tsx b/src/components/writeRetro/explainModal/explainRetro.tsx index f62c7fe..1afa562 100644 --- a/src/components/writeRetro/explainModal/explainRetro.tsx +++ b/src/components/writeRetro/explainModal/explainRetro.tsx @@ -5,7 +5,7 @@ import { IoPersonSharp } from 'react-icons/io5'; import { IoPeopleSharp } from 'react-icons/io5'; import { IoPersonCircleSharp } from 'react-icons/io5'; -import { Modal, ModalOverlay, ModalContent, ModalCloseButton, useDisclosure } from '@chakra-ui/react'; +import { Modal, ModalOverlay, ModalContent, ModalCloseButton, useDisclosure, Flex } from '@chakra-ui/react'; import * as S from '@/styles/writeRetroStyles/Explain.style'; interface ExplainButtonProps { @@ -29,7 +29,7 @@ export const ExplainButton = ({ templateId }: ExplainButtonProps) => { - + {modalBody} {/* */} { return ( <> -
-
- - 개인 단위 회고에 추천! -
- KPT 회고 더 잘 활용하기 -
-
+ + + + + 개인 단위 회고에 추천! + + KPT 회고 더 잘 활용하기 + + + {/* ExplainKPT */} 진행 방법 @@ -156,7 +158,7 @@ export const ExplainKPT = () => { -
+
); diff --git a/src/components/writeRetro/layout/Title.tsx b/src/components/writeRetro/layout/Title.tsx index 88cd6a0..9b1929f 100644 --- a/src/components/writeRetro/layout/Title.tsx +++ b/src/components/writeRetro/layout/Title.tsx @@ -65,19 +65,20 @@ const Title: FC = ({ name, description, retro, user }) => { )} - +
{user.userId === retro.userId && ( { navigate(`/revise?retrospectiveId=${retrospectiveId}&teamId=${teamId}`); }} @@ -88,7 +89,11 @@ const Title: FC = ({ name, description, retro, user }) => { {teamId ? ( <> - setInviteModalOpen(true)}> + setInviteModalOpen(true)} + > 팀원 초대 링크 링크는 2시간 후에 만료됩니다. diff --git a/src/components/writeRetro/revise/ManageTeamMembers.tsx b/src/components/writeRetro/revise/ManageTeamMembers.tsx index dac9420..725ad8f 100644 --- a/src/components/writeRetro/revise/ManageTeamMembers.tsx +++ b/src/components/writeRetro/revise/ManageTeamMembers.tsx @@ -10,17 +10,20 @@ import { TableContainer, Flex, Button, + PopoverHeader, + PopoverBody, Popover, PopoverTrigger, PopoverContent, PopoverArrow, PopoverCloseButton, } from '@chakra-ui/react'; +import { RetrospectiveData } from '@/api/@types/Retrospectives'; import { TeamMembersData } from '@/api/@types/TeamController'; import { UserData } from '@/api/@types/Users'; import postImageToS3 from '@/api/imageApi/postImageToS3'; +import { RetrospectiveService } from '@/api/services/Retrospectives'; import { TeamControllerServices } from '@/api/services/TeamController'; -import { UserServices } from '@/api/services/User'; import { convertToLocalTime } from '@/components/RetroList/ContentsList'; import InviteTeamModal from '@/components/inviteTeam/InviteTeamModal'; import { useCustomToast } from '@/hooks/useCustomToast'; @@ -30,24 +33,17 @@ import * as S from '@/styles/writeRetroStyles/ReviseLayout.style'; interface Props { teamId: number; members: TeamMembersData[]; + user: UserData; + retro: RetrospectiveData; } -const ManageTeamMembers: FC = ({ teamId, members }) => { +const ManageTeamMembers: FC = ({ teamId, members, user, retro }) => { const [searchTerm, setSearchTerm] = useState(''); const [isInviteModalOpen, setInviteModalOpen] = useState(false); - const [user, setUser] = useState(); const [image, setImage] = useState<{ [key: number]: string }>({}); const toast = useCustomToast(); const filterData = members.filter(members => members.username.includes(searchTerm)); - - const fetchUser = async () => { - try { - const data = await UserServices.get(); - setUser(data.data); - } catch (error) { - toast.error(error); - } - }; + // const navigate = useNavigate(); const DeleteTeamMember = async (id: number) => { try { @@ -71,12 +67,23 @@ const ManageTeamMembers: FC = ({ teamId, members }) => { })); } } catch (e) { - toast.error(e); + console.error(e); } }; + const PostAdminStatus = async (member: number) => { + try { + await RetrospectiveService.leaderPost({ newLeaderId: member, retrospectiveId: retro.retrospectiveId }); + toast.success('리더 권한을 양도하였습니다.'); + // navigate('/'); + } catch (e) { + console.error(e); + } + }; + + console.log('members', members); + useEffect(() => { - fetchUser(); members.forEach(item => fetchImage(item)); }, []); @@ -86,7 +93,38 @@ const ManageTeamMembers: FC = ({ teamId, members }) => { 팀원 관리 - setInviteModalOpen(true)}>팀원 초대 링크 + + + + 리더 권한 양도 + + + + + + 리더 권한을 양도할 팀원을 선택하세요. + + + {members + .filter(member => user.userId !== member.userId) + .map(item => ( + PostAdminStatus(item.userId)}> + {item.profileImage ? ( + + ) : ( + + )} + +

{item.username ?? '닉네임 없음'}

+
+ ))} +
+
+
+
+ setInviteModalOpen(true)}> + 팀원 참여 링크 + 링크는 2시간 후에 만료됩니다.
diff --git a/src/components/writeRetro/task/taskMessage/TeamTaskMessage.tsx b/src/components/writeRetro/task/taskMessage/TeamTaskMessage.tsx index 6b4a8d7..f2321db 100644 --- a/src/components/writeRetro/task/taskMessage/TeamTaskMessage.tsx +++ b/src/components/writeRetro/task/taskMessage/TeamTaskMessage.tsx @@ -29,6 +29,7 @@ const TeamTaskMessage: FC = ({ section, setRendering, user, teamId }) => setValue(e.target.value); }; + console.log('section', section.comments); const handlePostComment = async () => { try { await CommentService.post({ sectionId: section.sectionId, commentContent: value }); @@ -53,6 +54,8 @@ const TeamTaskMessage: FC = ({ section, setRendering, user, teamId }) => } }; + console.log('section', section); + const fetchImage = async (item: CommentData) => { try { if (item.thumbnail) { @@ -137,9 +140,12 @@ const TeamTaskMessage: FC = ({ section, setRendering, user, teamId }) => )} -
- {section.content} -
+ + {section.content} {' '} + {section.createdDate !== section.lastModifiedDate && ( + (수정됨) + )} + ))} diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx index c2a8985..1f07acf 100644 --- a/src/pages/MyPage.tsx +++ b/src/pages/MyPage.tsx @@ -20,7 +20,6 @@ const MyPage = () => { const [userNickname, _] = useRecoilState(userNicknameState); const [image, setImage] = useState(null); const [userProfile, setUserProfile] = useState<{ [key: string]: string }>({}); - const [imageURL, setImageURL] = useState(''); const toast = useCustomToast(); const navigate = useNavigate(); @@ -37,18 +36,15 @@ const MyPage = () => { method: 'PUT', }); - const uploadResponse = await axios.put(response.data.preSignedUrl, image, { + await axios.put(response.data.preSignedUrl, image, { headers: { 'Content-Type': image?.type, }, }); - console.log(uploadResponse.status); - const requestData: PutUsersRequest = { thumbnail: response.data.filename, username: userNickname, }; - // 이미지 업로드가 완료된 후, 새로운 이미지 URL을 받아와서 미리보기 업데이트 if (userData) { setUserProfile(prevImage => ({ @@ -71,7 +67,6 @@ const MyPage = () => { const fetchUserData = async () => { try { const response = await getUser(); - console.log('유저 정보', response); setUserData(response); } catch (error) { console.error('에러', error); diff --git a/src/pages/ProjectRetroPage.tsx b/src/pages/ProjectRetroPage.tsx index c1c2872..6c16478 100644 --- a/src/pages/ProjectRetroPage.tsx +++ b/src/pages/ProjectRetroPage.tsx @@ -4,9 +4,9 @@ import { FiPlusCircle } from 'react-icons/fi'; import { RiFolder6Fill } from 'react-icons/ri'; import { GetRetrospectiveGroups, GetRetrospectiveGroupsRequest } from '@/api/@types/Groups'; import { queryGetRetrospectiveAllGroups } from '@/api/retroGroupsApi/getAllGroups'; -import CreateModal from '@/components/projectRetro/CreateModal'; import DescriptionModal from '@/components/projectRetro/DescriptionModal'; import GroupList from '@/components/projectRetro/GroupList'; +import Modal from '@/components/projectRetro/Modal'; import StatusFilter from '@/components/projectRetro/StatusFilter'; import { useCustomToast } from '@/hooks/useCustomToast'; import * as S from '@/styles/projectRetro/ProjectRetroPage.styles'; @@ -20,8 +20,8 @@ export interface RetroGroup { const ProjectRetro = () => { // const [selectedFilter, setSelectedFilter] = useState<'ALL' | 'ING' | 'DONE'>('ALL'); - const [descriptionOpen, setDescriptionOpen] = useState(false); - const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isDescriptionOpen, setIsDescriptionOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const toast = useCustomToast(); const [data, setData] = useState({ totalCount: 0, nodes: [] }); @@ -40,14 +40,14 @@ const ProjectRetro = () => { setData(responseData.data); console.log(data); } catch (error) { - toast.error(error); + toast.error('그룹 불러오기에 실패했습니다'); } }; fetchGroup(); }, [query]); - const handleCreateModal = () => { - setIsCreateOpen(true); + const handleModal = () => { + setIsModalOpen(true); }; // 데이터 가져오기 const groups: RetroGroup[] = [ @@ -78,19 +78,17 @@ const ProjectRetro = () => { setDescriptionOpen(true)} + onClick={() => setIsDescriptionOpen(true)} /> 사용법 -
- - - {isCreateOpen && } - - -
- {descriptionOpen ? setDescriptionOpen(false)} /> : null} + + + + + {isDescriptionOpen ? setIsDescriptionOpen(false)} /> : null} + {isModalOpen && setIsModalOpen(false)} type="create" />} ); }; diff --git a/src/pages/RevisePage.tsx b/src/pages/RevisePage.tsx index 3060735..d203154 100644 --- a/src/pages/RevisePage.tsx +++ b/src/pages/RevisePage.tsx @@ -1,10 +1,12 @@ import { useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; import { RetrospectiveData } from '@/api/@types/Retrospectives'; import { TeamMembersData } from '@/api/@types/TeamController'; +import { UserData } from '@/api/@types/Users'; import { RetrospectiveService } from '@/api/services/Retrospectives'; import { TeamControllerServices } from '@/api/services/TeamController'; +import { UserServices } from '@/api/services/User'; import RetroTitle from '@/components/writeRetro/layout/RetroTitle'; import BackButton from '@/components/writeRetro/revise/BackButton'; import ManageTeamMembers from '@/components/writeRetro/revise/ManageTeamMembers'; @@ -21,6 +23,8 @@ const RetroRevisePage = () => { const [retro, setRetro] = useState(); const [members, setMembers] = useState([]); const [status, setStatus] = useState('NOT_STARTED'); + const [user, setUser] = useState(); + const navigate = useNavigate(); const toast = useCustomToast(); const fetchRetrospective = async () => { @@ -47,12 +51,26 @@ const RetroRevisePage = () => { } }; + const fetchUser = async () => { + try { + const data = await UserServices.get(); + setUser(data.data); + } catch (error) { + toast.error(error); + } + }; + useEffect(() => { fetchTeamMembers(); fetchRetrospective(); + fetchUser(); }, [retro?.status, members.values]); if (!retro) return; + if (user?.userId !== retro.userId) { + toast.error('리더 권한이 없습니다.'); + navigate('/'); + } return ( <> @@ -77,7 +95,11 @@ const RetroRevisePage = () => { - {members && } + {user && ( + + {members && } + + )} diff --git a/src/styles/layout/layout.style.ts b/src/styles/layout/layout.style.ts index 918bb04..3a2dc16 100644 --- a/src/styles/layout/layout.style.ts +++ b/src/styles/layout/layout.style.ts @@ -94,6 +94,7 @@ export const SideBarBGContainer = styled.div` color: white; background-color: #f8f8f8; z-index: 999; + overflow: auto; `; export const LogoBox = styled.div``; @@ -171,6 +172,7 @@ export const MenuText = styled.a` color: #111b47; font-weight: 600; text-decoration: none; + margin: 5px 0; `; export const AllDeleteText = styled.button` diff --git a/src/styles/projectRetro/CreateModal.styles.ts b/src/styles/projectRetro/CreateModal.styles.ts deleted file mode 100644 index 0d9b7b6..0000000 --- a/src/styles/projectRetro/CreateModal.styles.ts +++ /dev/null @@ -1,7 +0,0 @@ -import styled from 'styled-components'; - -export const Container = styled.div` - border: 1px solid black; - width: 200px; - height: 150px; -`; diff --git a/src/styles/projectRetro/DeleteModal.styles.ts b/src/styles/projectRetro/DeleteModal.styles.ts new file mode 100644 index 0000000..485f2fb --- /dev/null +++ b/src/styles/projectRetro/DeleteModal.styles.ts @@ -0,0 +1,66 @@ +import styled from 'styled-components'; + +export const Background = styled.div` + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.35); + z-index: 350; +`; + +export const Container = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 400; +`; + +export const Modal = styled.div` + margin-top: 20px; + display: flex; + flex-direction: column; + background-color: white; + border-radius: 5px; + width: 600px; + height: auto; +`; + +export const Top = styled.div` + padding: 10px; + display: flex; + align-items: center; + + justify-content: space-between; +`; + +export const Title = styled.p` + color: #6c6c6c; +`; + +export const Bottom = styled.div` + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; +`; + +export const ProjectName = styled.span` + color: #111b47; + font-size: large; + margin: 0px 5px; + font-weight: bold; +`; + +export const Text = styled.span` + color: #111b47; +`; + +export const Button = styled.button` + color: white; + background-color: #111b47; + border-radius: 5px; + width: auto; + margin-left: auto; + padding: 3px 20px; +`; diff --git a/src/styles/projectRetro/Modal.styles.ts b/src/styles/projectRetro/Modal.styles.ts new file mode 100644 index 0000000..b376add --- /dev/null +++ b/src/styles/projectRetro/Modal.styles.ts @@ -0,0 +1,136 @@ +import styled from 'styled-components'; + +export const Background = styled.div` + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.35); + z-index: 250; +`; + +export const Container = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 300; +`; + +export const Modal = styled.div` + display: flex; + flex-direction: column; + background-color: white; + border-radius: 5px; + width: 500px; + height: auto; + padding: 25px; +`; + +export const TitleBox = styled.div` + display: flex; + align-items: center; +`; + +export const ProjectText = styled.div` + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + background-color: #111b47; + color: white; + padding: 2px 20px; +`; + +export const TitleText = styled.div` + color: #6c6c6c; + padding-left: 10px; + padding-right: 10px; +`; + +export const ImageBox = styled.div` + display: flex; + flex-direction: column; + align-items: center; + height: auto; + margin: 10px 0px; +`; + +export const ImageText = styled.p` + color: #676767; + font-size: small; +`; +export const NameInput = styled.input` + margin: 10px 0px; + border-bottom: 1px solid #949494; + outline: none; + width: 200px; + height: 30px; + &::placeholder { + color: #676767; + font-weight: bold; + } +`; + +export const Text = styled.p` + color: #8d8d8d; +`; + +export const DescriptionBox = styled.div` + margin: 10px 0px; +`; + +export const DescriptionInput = styled.input` + width: 100%; + border-bottom: 1px solid #949494; + outline: none; + height: 30px; + &::placeholder { + color: #676767; + } +`; + +export const StatusBox = styled.div` + margin: 10px 0px; +`; + +export const Button = styled.button` + border: 1px solid #8d8d8d; + border-radius: 3px; + margin-right: auto; + width: 160px; + display: flex; + align-items: center; + padding: 0px 10px; +`; + +interface StatusTextProps { + option: string; +} + +export const StatusText = styled.p` + color: ${props => (props.option === 'ING' ? '#57AD5A' : '#FF1818')}; + font-weight: bold; +`; + +export const DefaultText = styled.p` + color: #8d8d8d; +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: row-reverse; +`; + +export const DeleteButton = styled.button` + color: #ff0000; + border-radius: 5px; + border: 1px solid #ff0000; + padding: 3px 20px; +`; +export const SubmitButton = styled.button` + margin-left: 20px; + color: white; + background-color: #4991e4; + padding: 3px 20px; + border-radius: 5px; +`; diff --git a/src/styles/writeRetroStyles/Explain.style.ts b/src/styles/writeRetroStyles/Explain.style.ts index 24c705b..cec21ae 100644 --- a/src/styles/writeRetroStyles/Explain.style.ts +++ b/src/styles/writeRetroStyles/Explain.style.ts @@ -14,9 +14,8 @@ export const ExplainButtonStyle = styled.button` // 흰색 바탕 export const ExplainStyle = styled.div` - width: 934px; - height: 523px; border-radius: 10px; + overflow: auto; position: relative; padding: 29px 11px; `; @@ -36,8 +35,6 @@ export const ExplainTitleBox = styled.div` `; export const ExplainSideTitle = styled.div` - width: 200px; - display: inline-block; margin-left: 5px; /* position: absolute; margin-top: 15px; */ @@ -53,28 +50,27 @@ export const RecommendMessage = styled.span` `; export const ExplainTitle = styled.div` - width: 317px; - height: 42px; - font-size: 24px; + font-size: 20px; + padding: 10px; font-weight: 500; + margin: 10px 0; color: #ffffff; line-height: 42px; text-align: center; background-color: #111b47; border-radius: 8px; - margin: 0 auto; `; export const ExplainBody = styled.div` - width: 438px; - height: 388px; + /* width: 438px; */ + /* height: 388px; */ /* background-color: gray; */ padding-left: 13px; - margin: 25px 9px 0px; + margin: 25px auto; `; export const ExplainLine = styled.div` - height: 388px; + /* height: 388px; */ border-right: 1px dashed #b6b6b6; margin-top: 25px; `; diff --git a/src/styles/writeRetroStyles/Layout.style.ts b/src/styles/writeRetroStyles/Layout.style.ts index 68b4f91..59926b0 100644 --- a/src/styles/writeRetroStyles/Layout.style.ts +++ b/src/styles/writeRetroStyles/Layout.style.ts @@ -257,13 +257,12 @@ export const reviseTitleText = styled.p` export const TaskText = styled.p` font-size: 15px; - min-width: 200px; - max-width: 500px; font-weight: 700; color: #6b7a99; line-height: 20px; vertical-align: top; margin: 20px 0; + margin-right: 10px; margin-top: 20px; &:hover { cursor: pointer; diff --git a/src/styles/writeRetroStyles/ReviseLayout.style.ts b/src/styles/writeRetroStyles/ReviseLayout.style.ts index 7e67f05..6f70826 100644 --- a/src/styles/writeRetroStyles/ReviseLayout.style.ts +++ b/src/styles/writeRetroStyles/ReviseLayout.style.ts @@ -237,15 +237,15 @@ export const ManageTitleStyle = styled.p` } `; -export const InvitationLinkButton = styled.button` +export const InvitationLinkButton = styled.button<{ backgroundColor: string; color: string }>` width: 130px; height: 33px; font-size: 14px; font-weight: 600; - color: #ffffff; + color: ${props => props.color}; line-height: 33px; text-align: center; - background-color: #2f4dce; + background-color: ${props => props.backgroundColor}; border-radius: 5px; margin: auto 0; margin-left: 35px;