Skip to content

Commit

Permalink
Merge pull request #293 from boostcampwm2023/feature/update-member-st…
Browse files Browse the repository at this point in the history
…atus

feat: 멤버 상태 변경 기능
  • Loading branch information
surinkwon authored Jun 6, 2024
2 parents 0d9cb06 + 837e7a4 commit 651c633
Show file tree
Hide file tree
Showing 19 changed files with 423 additions and 84 deletions.
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"react-router-dom": "^6.22.1",
"reusify": "^1.0.4",
"socket.io-client": "^4.7.5",
"tailwind-scrollbar-hide": "^1.1.7"
"tailwind-scrollbar-hide": "^1.1.7",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/preset-env": "^7.23.9",
Expand Down
62 changes: 46 additions & 16 deletions frontend/src/components/landing/member/LandingMember.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
import { useOutletContext } from "react-router-dom";
import { Socket } from "socket.io-client";
import { USER_STATUS_WORD, USER_WORD_STATUS } from "../../../constants/landing";
import UserBlock from "./UserBlock";
import useDropdown from "../../../hooks/common/dropdown/useDropdown";
import { memberResponse } from "../../../types/DTO/authDTO";
import { LandingMemberDTO } from "../../../types/DTO/landingDTO";
import useUpdateUserStatus from "../../../hooks/common/member/useUpdateUserStatus";
import { DEFAULT_MEMBER } from "../../../constants/projects";
import { memberResponse } from "../../../types/DTO/authDTO";
import emitMemberStatusUpdate from "../../../utils/emitMemberStatusUpdate";

const LandingMember = ({
member,
myInfo,
inviteLinkIdRef,
projectTitle,
}: {
member: LandingMemberDTO[];
myInfo: LandingMemberDTO;
inviteLinkIdRef: React.MutableRefObject<string>;
interface LandingMemberProps {
projectTitle: string;
}) => {
const { Dropdown, selectedOption } = useDropdown({
}

interface useOutletContextValues {
socket: Socket;
addUserStatusEventListener: () => void;
removeUserStatusEventListener: () => void;
handleCanAddStatusEventListener: (havePurpose: boolean) => void;
}

const LandingMember = ({ projectTitle }: LandingMemberProps) => {
const {
socket,
addUserStatusEventListener,
removeUserStatusEventListener,
handleCanAddStatusEventListener,
}: useOutletContextValues = useOutletContext();
const { Dropdown, selectedOption, handleChangeSelectedOption } = useDropdown({
placeholder: "내 상태",
options: ["접속 중", "부재 중", "자리비움"],
defaultOption: USER_STATUS_WORD[myInfo.status],
defaultOption: "접속 중",
});
const { myInfo, memberList, inviteLinkIdRef } = useUpdateUserStatus(
socket,
handleChangeSelectedOption
);

const userData: memberResponse = JSON.parse(
window.localStorage.getItem("member") ?? DEFAULT_MEMBER
);
const imageUrl = myInfo.imageUrl ?? userData.imageUrl;
const username = myInfo.username ?? userData.username;
const { imageUrl, username } = myInfo.imageUrl ? myInfo : userData;

const handleInviteButtonClick = () => {
window.navigator.clipboard
Expand All @@ -41,6 +55,21 @@ const LandingMember = ({
});
};

function selectStatusOption(option: string) {
emitMemberStatusUpdate(socket, {
...myInfo,
status: USER_WORD_STATUS[option],
});

if (option === USER_STATUS_WORD.away || option === USER_STATUS_WORD.off) {
handleCanAddStatusEventListener(false);
removeUserStatusEventListener();
} else {
handleCanAddStatusEventListener(true);
addUserStatusEventListener();
}
}

return (
<div className="w-full px-6 py-6 overflow-y-scroll rounded-lg shadow-box bg-gradient-to-tr to-light-green-linear-from from-light-green scrollbar-thin scrollbar-thumb-light-green scrollbar-track-transparent scrollbar-thumb-rounded-full">
<div className="flex flex-col gap-3">
Expand All @@ -52,6 +81,7 @@ const LandingMember = ({
containerClassName="w-[6rem] bg-white rounded-b-lg overflow-hidden"
itemClassName="w-full text-xxxs text-center font-semibold py-2 hover:bg-middle-green hover:text-white hover:font-semibold"
iconSize="w-[12px] h-[12px]"
selectOption={selectStatusOption}
/>
</div>
<UserBlock
Expand All @@ -68,7 +98,7 @@ const LandingMember = ({
초대링크 복사
</button>
</div>
{member.map((memberData: LandingMemberDTO) => (
{memberList.map((memberData: LandingMemberDTO) => (
<UserBlock {...memberData} key={memberData.id} />
))}
</div>
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/hooks/common/dropdown/useDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface DropdownProps {
containerClassName?: string;
itemClassName?: string;
iconSize?: string;
selectOption?: (option: string) => void;
}

const useDropdown = ({
Expand All @@ -24,12 +25,16 @@ const useDropdown = ({
defaultOption ? defaultOption : ""
);
const dropdownRef = useRef<HTMLButtonElement>(null);
const handleChangeSelectedOption = (option: string) => {
setSelectedOption(option);
};

const Dropdown = ({
buttonClassName = "",
containerClassName = "",
itemClassName = "",
iconSize = "24",
selectOption,
}: DropdownProps) => {
const [open, setOpen] = useState<boolean>(false);

Expand All @@ -38,7 +43,10 @@ const useDropdown = ({
};

const handleOptionClick = (option: string) => {
setSelectedOption(option);
if (selectOption) {
selectOption(option);
}
handleChangeSelectedOption(option);
setOpen(false);
};

Expand Down Expand Up @@ -106,7 +114,7 @@ const useDropdown = ({
);
};

return { Dropdown, selectedOption };
return { Dropdown, selectedOption, handleChangeSelectedOption };
};

export default useDropdown;
8 changes: 5 additions & 3 deletions frontend/src/hooks/common/landing/useLandingLinkSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ const useLandingLinkSocket = (socket: Socket) => {
};

const handleOnLanding = ({ domain, content }: LandingSocketData) => {
if (domain !== LandingSocketDomain.INIT) return;
if (domain !== LandingSocketDomain.INIT) {
return;
}
handleInitEvent(content);
};

useEffect(() => {
socket.on("landing", handleOnLanding);

return () => {
socket.off("landing");
socket.off("landing", handleOnLanding);
};
});
}, []);

return { link };
};
Expand Down
28 changes: 13 additions & 15 deletions frontend/src/hooks/common/landing/useLandingMemoSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,22 @@ const useLandingMemoSocket = (socket: Socket) => {
) => {
switch (action) {
case LandingSocketMemoAction.CREATE:
setMemoList((memoList: LandingMemoDTO[]) => {
return [content, ...memoList];
});
setMemoList((memoList: LandingMemoDTO[]) => [content, ...memoList]);
break;
case LandingSocketMemoAction.DELETE:
setMemoList((memoList: LandingMemoDTO[]) => {
return memoList.filter(
(memo: LandingMemoDTO) => memo.id !== content.id
);
});
setMemoList((memoList: LandingMemoDTO[]) =>
memoList.filter((memo: LandingMemoDTO) => memo.id !== content.id)
);
break;
case LandingSocketMemoAction.COLOR_UPDATE:
setMemoList((memoList: LandingMemoDTO[]) => {
return memoList.map((memo: LandingMemoDTO) => {
if (memo.id !== content.id) return memo;
setMemoList((memoList: LandingMemoDTO[]) =>
memoList.map((memo: LandingMemoDTO) => {
if (memo.id !== content.id) {
return memo;
}
return { ...memo, color: content.color };
});
});
})
);
}
};
const handleOnMemoLanding = ({
Expand Down Expand Up @@ -75,9 +73,9 @@ const useLandingMemoSocket = (socket: Socket) => {
socket.on("landing", handleOnMemoLanding);

return () => {
socket.off("landing");
socket.off("landing", handleOnMemoLanding);
};
});
}, []);

return {
memoList,
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/hooks/common/landing/useLandingProjectSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ const useLandingProjectSocket = (socket: Socket) => {
};

const handleOnLanding = ({ domain, content }: LandingSocketData) => {
if (domain !== LandingSocketDomain.INIT) return;
if (domain !== LandingSocketDomain.INIT) {
return;
}
handleInitEvent(content);
};

useEffect(() => {
socket.on("landing", handleOnLanding);

return () => {
socket.off("landing");
socket.off("landing", handleOnLanding);
};
});
}, []);
return { project };
};

Expand Down
8 changes: 5 additions & 3 deletions frontend/src/hooks/common/landing/useLandingSprintSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ const useLandingSprintSocket = (socket: Socket) => {
setSprint(sprint);
};
const handleOnLanding = ({ domain, content }: LandingSocketData) => {
if (domain !== LandingSocketDomain.INIT) return;
if (domain !== LandingSocketDomain.INIT) {
return;
}
handleInitEvent(content);
};
useEffect(() => {
socket.on("landing", handleOnLanding);

return () => {
socket.off("landing");
socket.off("landing", handleOnLanding);
};
});
}, []);
return { sprint };
};

Expand Down
85 changes: 85 additions & 0 deletions frontend/src/hooks/common/member/useAwayUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useRef, useState } from "react";
import { Socket } from "socket.io-client";
import useMemberStore from "../../../stores/useMemberStore";
import useThrottle from "../throttle/useThrottle";
import emitMemberStatusUpdate from "../../../utils/emitMemberStatusUpdate";

const useAwayUser = (socket: Socket) => {
const timerRef = useRef<NodeJS.Timeout | null>(null);
const myInfo = useMemberStore((state) => state.myInfo);
const [canAddStatusEventListener, setCanAddStatusEventListener] =
useState(true);
const throttle = useThrottle();
const TIME = 1000 * 60 * 10;
const THROTTLE_TIME = 3000;

const clearTimer = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};

const resetTimer = () => {
timerRef.current = setTimeout(() => {
emitMemberStatusUpdate(socket, {
...myInfo,
status: "away",
});

clearTimer();
}, TIME);
};

const handleUserStatus = () => {
throttle(THROTTLE_TIME, () => {
clearTimer();

if (myInfo.status === "away") {
emitMemberStatusUpdate(socket, {
...myInfo,
status: "on",
});
}

resetTimer();
});
};

const handleCanAddStatusEventListener = (can: boolean) => {
setCanAddStatusEventListener(can);
};

const addUserStatusEventListener = () => {
window.addEventListener("mousemove", handleUserStatus);
window.addEventListener("keydown", handleUserStatus);
window.addEventListener("scroll", handleUserStatus);
window.addEventListener("click", handleUserStatus);
resetTimer();
};

const removeUserStatusEventListener = () => {
window.removeEventListener("mousemove", handleUserStatus);
window.removeEventListener("keydown", handleUserStatus);
window.removeEventListener("scroll", handleUserStatus);
window.removeEventListener("click", handleUserStatus);
clearTimer();
};

useEffect(() => {
if (canAddStatusEventListener) {
addUserStatusEventListener();
}

return () => {
removeUserStatusEventListener();
};
}, [myInfo]);

return {
addUserStatusEventListener,
removeUserStatusEventListener,
handleCanAddStatusEventListener,
};
};

export default useAwayUser;
Loading

0 comments on commit 651c633

Please sign in to comment.