diff --git a/frontend/src/components/landing/member/LandingMember.tsx b/frontend/src/components/landing/member/LandingMember.tsx
index b26ed7a..4dd8245 100644
--- a/frontend/src/components/landing/member/LandingMember.tsx
+++ b/frontend/src/components/landing/member/LandingMember.tsx
@@ -94,12 +94,20 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => {
/>
| 함께하는 사람들
-
+ {myInfo.role === "LEADER" && (
+
+
+ |
+
+
+ )}
{memberList.map((memberData: LandingMemberDTO) => (
diff --git a/frontend/src/components/setting/InformationInput.tsx b/frontend/src/components/setting/InformationInput.tsx
new file mode 100644
index 0000000..13efe75
--- /dev/null
+++ b/frontend/src/components/setting/InformationInput.tsx
@@ -0,0 +1,39 @@
+import { ChangeEvent } from "react";
+
+interface InformationInputProps {
+ label: string;
+ inputId: string;
+ inputValue: string;
+ errorMessage: string;
+ onChange: (event: ChangeEvent
) => void;
+}
+
+const InformationInput = ({
+ label,
+ inputId,
+ inputValue,
+ errorMessage,
+ onChange,
+}: InformationInputProps) => (
+
+);
+
+export default InformationInput;
diff --git a/frontend/src/components/setting/InformationSettingSection.tsx b/frontend/src/components/setting/InformationSettingSection.tsx
new file mode 100644
index 0000000..551ea9e
--- /dev/null
+++ b/frontend/src/components/setting/InformationSettingSection.tsx
@@ -0,0 +1,116 @@
+import { ChangeEvent, useEffect, useMemo, useState } from "react";
+import InformationInput from "./InformationInput";
+import { useOutletContext } from "react-router-dom";
+import { Socket } from "socket.io-client";
+import useSettingProjectInfoSocket from "../../hooks/pages/setting/useSettingProjectInfoSocket";
+
+interface InformationSettingSectionProps {
+ title: string;
+ subject: string;
+}
+
+const InformationSettingSection = ({
+ title,
+ subject,
+}: InformationSettingSectionProps) => {
+ const { socket }: { socket: Socket } = useOutletContext();
+ const { emitProjectInfoUpdateEvent } = useSettingProjectInfoSocket(socket);
+ const [titleValue, setTitleValue] = useState(title);
+ const [titleErrorMessage, setTitleErrorMessage] = useState("");
+ const [subjectValue, setSubjectValue] = useState(subject);
+ const [subjectErrorMessage, setSubjectErrorMessage] = useState("");
+ const submitActivated = useMemo(
+ () =>
+ !(
+ !titleValue ||
+ !subjectValue ||
+ (titleValue === title && subjectValue === subject)
+ ),
+ [title, subject, titleValue, subjectValue]
+ );
+
+ const handleTitleChange = (event: ChangeEvent) => {
+ const { value } = event.target;
+
+ setTitleValue(value);
+
+ if (!value.trim()) {
+ setTitleErrorMessage("프로젝트 이름을 입력해주세요.");
+ return;
+ }
+
+ if (value.length > 255) {
+ setTitleErrorMessage("프로젝트 이름이 너무 깁니다.");
+ return;
+ }
+
+ setTitleErrorMessage("");
+ };
+
+ const handleSubjectChange = (event: ChangeEvent) => {
+ const { value } = event.target;
+
+ setSubjectValue(value);
+
+ if (!value.trim()) {
+ setSubjectErrorMessage("프로젝트 주제를 입력해주세요.");
+ return;
+ }
+
+ if (value.length > 255) {
+ setSubjectErrorMessage("프로젝트 주제가 너무 깁니다.");
+ return;
+ }
+
+ setSubjectErrorMessage("");
+ };
+
+ const handleSubmit = () => {
+ emitProjectInfoUpdateEvent({ title: titleValue, subject: subjectValue });
+ };
+
+ useEffect(() => {
+ setTitleValue(title);
+ setSubjectValue(subject);
+ }, [title, subject]);
+
+ return (
+ <>
+
+
+
+
+
+ {!submitActivated && (
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default InformationSettingSection;
diff --git a/frontend/src/components/setting/MemberBlock.tsx b/frontend/src/components/setting/MemberBlock.tsx
new file mode 100644
index 0000000..e4c319f
--- /dev/null
+++ b/frontend/src/components/setting/MemberBlock.tsx
@@ -0,0 +1,32 @@
+import useMemberStore from "../../stores/useMemberStore";
+import { LandingMemberDTO } from "../../types/DTO/landingDTO";
+
+interface MemberBlockProps extends LandingMemberDTO {}
+
+const MemberBlock = ({ username, imageUrl, role }: MemberBlockProps) => {
+ const myRole = useMemberStore((state) => state.myInfo.role);
+
+ return (
+
+
+

+
{username}
+
+
+
+ {myRole === "LEADER" && (
+
+ )}
+
+
+ );
+};
+
+export default MemberBlock;
diff --git a/frontend/src/components/setting/MemberSettingSection.tsx b/frontend/src/components/setting/MemberSettingSection.tsx
new file mode 100644
index 0000000..82bdfaa
--- /dev/null
+++ b/frontend/src/components/setting/MemberSettingSection.tsx
@@ -0,0 +1,26 @@
+import { LandingMemberDTO } from "../../types/DTO/landingDTO";
+import MemberBlock from "./MemberBlock";
+
+interface MemberSettingSectionProps {
+ memberList: LandingMemberDTO[];
+}
+
+const MemberSettingSection = ({ memberList }: MemberSettingSectionProps) => (
+
+
+
+
+
+ {...memberList.map((member) => )}
+
+
+
+);
+
+export default MemberSettingSection;
diff --git a/frontend/src/components/setting/ProjectDeleteModal.tsx b/frontend/src/components/setting/ProjectDeleteModal.tsx
new file mode 100644
index 0000000..1cb8ac0
--- /dev/null
+++ b/frontend/src/components/setting/ProjectDeleteModal.tsx
@@ -0,0 +1,97 @@
+import { ChangeEvent, MouseEventHandler, useState } from "react";
+import useSettingProjectSocket from "../../hooks/pages/setting/useSettingProjectSocket";
+import Closed from "../../assets/icons/closed.svg?react";
+import { Socket } from "socket.io-client";
+
+interface ProjectDeleteModalProps {
+ projectTitle: string;
+ socket: Socket;
+ close: () => void;
+}
+
+const ProjectDeleteModal = ({
+ projectTitle,
+ socket,
+ close,
+}: ProjectDeleteModalProps) => {
+ const [inputValue, setInputValue] = useState("");
+ const [confirmed, setConfirmed] = useState(false);
+
+ const { emitProjectDeleteEvent } = useSettingProjectSocket(socket);
+
+ const handleInputChange = (event: ChangeEvent) => {
+ const { value } = event.target;
+
+ setInputValue(value);
+
+ if (value === projectTitle) {
+ setConfirmed(true);
+ } else {
+ setConfirmed(false);
+ }
+ };
+
+ const handleCloseClick: MouseEventHandler<
+ HTMLButtonElement | HTMLDivElement
+ > = ({ target, currentTarget }: React.MouseEvent) => {
+ if (target !== currentTarget) {
+ return;
+ }
+ close();
+ };
+
+ const handleDeleteButtonClick = () => {
+ if (confirmed) {
+ emitProjectDeleteEvent();
+ }
+ };
+
+ return (
+
+
+
+
프로젝트 삭제 후 되돌릴 수 없습니다.
+
+
+ 삭제하시려면 "{projectTitle}"을(를) 입력해주세요.
+
+
+
+ {!confirmed && (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default ProjectDeleteModal;
diff --git a/frontend/src/components/setting/ProjectDeleteSection.tsx b/frontend/src/components/setting/ProjectDeleteSection.tsx
new file mode 100644
index 0000000..1bfcfaf
--- /dev/null
+++ b/frontend/src/components/setting/ProjectDeleteSection.tsx
@@ -0,0 +1,34 @@
+import { useOutletContext } from "react-router-dom";
+import { Socket } from "socket.io-client";
+import { useModal } from "../../hooks/common/modal/useModal";
+import ProjectDeleteModal from "./ProjectDeleteModal";
+
+interface ProjectDeleteSectionProps {
+ projectTitle: string;
+}
+
+const ProjectDeleteSection = ({ projectTitle }: ProjectDeleteSectionProps) => {
+ const { open, close } = useModal(true);
+ const { socket }: { socket: Socket } = useOutletContext();
+ const handleDeleteButtonClick = () => {
+ open();
+ };
+
+ return (
+
+
+
프로젝트 삭제
+
프로젝트를 삭제한 후 되돌릴 수 없습니다.
+
+
+
+ );
+};
+
+export default ProjectDeleteSection;
diff --git a/frontend/src/hooks/common/landing/useLandingProjectSocket.ts b/frontend/src/hooks/common/landing/useLandingProjectSocket.ts
index a86f4ab..098b9a4 100644
--- a/frontend/src/hooks/common/landing/useLandingProjectSocket.ts
+++ b/frontend/src/hooks/common/landing/useLandingProjectSocket.ts
@@ -6,6 +6,11 @@ import {
LandingSocketData,
LandingSocketDomain,
} from "../../../types/common/landing";
+import {
+ SettingSocketData,
+ SettingSocketDomain,
+} from "../../../types/common/setting";
+import { SettingProjectDTO } from "../../../types/DTO/settingDTO";
const useLandingProjectSocket = (socket: Socket) => {
const [project, setProject] = useState(
@@ -17,11 +22,22 @@ const useLandingProjectSocket = (socket: Socket) => {
setProject(project);
};
- const handleOnLanding = ({ domain, content }: LandingSocketData) => {
- if (domain !== LandingSocketDomain.INIT) {
+ const handleProjectInfoEvent = (content: SettingProjectDTO) => {
+ setProject({ ...project, title: content.title, subject: content.subject });
+ };
+
+ const handleOnLanding = ({
+ domain,
+ content,
+ }: LandingSocketData | SettingSocketData) => {
+ if (domain === SettingSocketDomain.PROJECT_INFO) {
+ handleProjectInfoEvent(content);
+ }
+
+ if (domain === LandingSocketDomain.INIT) {
+ handleInitEvent(content);
return;
}
- handleInitEvent(content);
};
useEffect(() => {
diff --git a/frontend/src/hooks/common/socket/useSocket.ts b/frontend/src/hooks/common/socket/useSocket.ts
index de0fada..f575984 100644
--- a/frontend/src/hooks/common/socket/useSocket.ts
+++ b/frontend/src/hooks/common/socket/useSocket.ts
@@ -1,7 +1,9 @@
-import { io } from "socket.io-client";
-import { BASE_URL } from "../../../constants/path";
import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { io } from "socket.io-client";
+import { BASE_URL, ROUTER_URL } from "../../../constants/path";
import { getAccessToken } from "../../../apis/utils/authAPI";
+import { SettingSocketData } from "../../../types/common/setting";
const useSocket = (projectId: string) => {
const WS_URL = `${BASE_URL}/project-${projectId}`;
@@ -15,6 +17,16 @@ const useSocket = (projectId: string) => {
})
);
const [connected, setConnected] = useState(false);
+ const navigate = useNavigate();
+
+ const handleProjectDeleted = ({ domain, action }: SettingSocketData) => {
+ if (domain === "projectInfo" && action === "delete") {
+ alert("프로젝트가 삭제되었습니다.");
+ setTimeout(() => {
+ navigate(ROUTER_URL.PROJECTS);
+ }, 1000);
+ }
+ };
useEffect(() => {
const handleOnConnect = () => {
@@ -27,11 +39,13 @@ const useSocket = (projectId: string) => {
socket.connect();
socket.on("connect", handleOnConnect);
socket.on("disconnect", handleOnDisconnect);
+ socket.on("main", handleProjectDeleted);
return () => {
socket.disconnect();
socket.off("connect", handleOnConnect);
socket.off("disconnect", handleOnDisconnect);
+ socket.off("main", handleProjectDeleted);
};
}, []);
diff --git a/frontend/src/hooks/pages/setting/useSettingProjectInfoSocket.ts b/frontend/src/hooks/pages/setting/useSettingProjectInfoSocket.ts
new file mode 100644
index 0000000..00ec667
--- /dev/null
+++ b/frontend/src/hooks/pages/setting/useSettingProjectInfoSocket.ts
@@ -0,0 +1,14 @@
+import { Socket } from "socket.io-client";
+
+const useSettingProjectInfoSocket = (socket: Socket) => {
+ const emitProjectInfoUpdateEvent = (content: {
+ title: string;
+ subject: string;
+ }) => {
+ socket.emit("projectInfo", { action: "update", content });
+ };
+
+ return { emitProjectInfoUpdateEvent };
+};
+
+export default useSettingProjectInfoSocket;
diff --git a/frontend/src/hooks/pages/setting/useSettingProjectSocket.ts b/frontend/src/hooks/pages/setting/useSettingProjectSocket.ts
new file mode 100644
index 0000000..2423f6a
--- /dev/null
+++ b/frontend/src/hooks/pages/setting/useSettingProjectSocket.ts
@@ -0,0 +1,11 @@
+import { Socket } from "socket.io-client";
+
+const useSettingProjectSocket = (socket: Socket) => {
+ const emitProjectDeleteEvent = () => {
+ socket.emit("projectInfo", { action: "delete", content: {} });
+ };
+
+ return { emitProjectDeleteEvent };
+};
+
+export default useSettingProjectSocket;
diff --git a/frontend/src/hooks/pages/setting/useSettingSocket.ts b/frontend/src/hooks/pages/setting/useSettingSocket.ts
new file mode 100644
index 0000000..7f1f549
--- /dev/null
+++ b/frontend/src/hooks/pages/setting/useSettingSocket.ts
@@ -0,0 +1,59 @@
+import { useEffect, useState } from "react";
+import { Socket } from "socket.io-client";
+import {
+ SettingSocketData,
+ SettingSocketDomain,
+ SettingSocketProjectInfoAction,
+} from "../../../types/common/setting";
+import { SettingDTO, SettingProjectDTO } from "../../../types/DTO/settingDTO";
+
+const useSettingSocket = (socket: Socket) => {
+ const [projectInfo, setProjectInfo] = useState({
+ title: "",
+ subject: "",
+ });
+
+ const handleInitEvent = (content: SettingDTO) => {
+ const { project } = content;
+ setProjectInfo(project);
+ };
+
+ const handleProjectInfoEvent = (
+ action: SettingSocketProjectInfoAction,
+ content: SettingProjectDTO
+ ) => {
+ switch (action) {
+ case SettingSocketProjectInfoAction.UPDATE:
+ setProjectInfo(content);
+ break;
+ }
+ };
+
+ const handleOnProjectInfoSetting = ({
+ domain,
+ action,
+ content,
+ }: SettingSocketData) => {
+ switch (domain) {
+ case SettingSocketDomain.INIT:
+ handleInitEvent(content);
+ break;
+ case SettingSocketDomain.PROJECT_INFO:
+ handleProjectInfoEvent(action, content);
+ break;
+ }
+ };
+
+ useEffect(() => {
+ socket.emit("joinSetting");
+ socket.on("setting", handleOnProjectInfoSetting);
+
+ return () => {
+ socket.off("setting", handleOnProjectInfoSetting);
+ };
+ }, []);
+
+ return { projectInfo };
+};
+
+export default useSettingSocket;
diff --git a/frontend/src/pages/setting/SettingPage.tsx b/frontend/src/pages/setting/SettingPage.tsx
new file mode 100644
index 0000000..990bce2
--- /dev/null
+++ b/frontend/src/pages/setting/SettingPage.tsx
@@ -0,0 +1,42 @@
+import { Socket } from "socket.io-client";
+import InformationSettingSection from "../../components/setting/InformationSettingSection";
+import MemberSettingSection from "../../components/setting/MemberSettingSection";
+import ProjectDeleteSection from "../../components/setting/ProjectDeleteSection";
+import { LandingMemberDTO } from "../../types/DTO/landingDTO";
+import { useOutletContext } from "react-router-dom";
+import useSettingSocket from "../../hooks/pages/setting/useSettingSocket";
+
+const memberList: LandingMemberDTO[] = [
+ { id: 1, username: "lesserTest", role: "LEADER", imageUrl: "", status: "on" },
+ {
+ id: 2,
+ username: "lesserTest2",
+ role: "MEMBER",
+ imageUrl: "",
+ status: "on",
+ },
+ {
+ id: 3,
+ username: "lesserTest3",
+ role: "MEMBER",
+ imageUrl: "",
+ status: "on",
+ },
+];
+
+const SettingPage = () => {
+ const { socket }: { socket: Socket } = useOutletContext();
+ const {
+ projectInfo: { title, subject },
+ } = useSettingSocket(socket);
+
+ return (
+
+ );
+};
+
+export default SettingPage;
diff --git a/frontend/src/stores/useMemberStore.ts b/frontend/src/stores/useMemberStore.ts
index ba76552..4ec6cef 100644
--- a/frontend/src/stores/useMemberStore.ts
+++ b/frontend/src/stores/useMemberStore.ts
@@ -15,7 +15,7 @@ interface MemberState extends InitialMemberState {
}
const initialState: InitialMemberState = {
- myInfo: { id: -1, username: "", imageUrl: "", status: "on" },
+ myInfo: { id: -1, username: "", imageUrl: "", status: "on", role: "MEMBER" },
memberList: [],
};
diff --git a/frontend/src/test/sortMemberByStatus.test.ts b/frontend/src/test/sortMemberByStatus.test.ts
index cbf6504..f205cc0 100644
--- a/frontend/src/test/sortMemberByStatus.test.ts
+++ b/frontend/src/test/sortMemberByStatus.test.ts
@@ -4,12 +4,12 @@ import sortMemberByStatus from "../utils/sortMemberByStatus";
describe("sortMemberByStatus test", () => {
it("on, away, off 순 정렬 테스트", () => {
const memberList: LandingMemberDTO[] = [
- { id: 1, username: "", imageUrl: "", status: "off" },
- { id: 2, username: "", imageUrl: "", status: "on" },
- { id: 3, username: "", imageUrl: "", status: "away" },
- { id: 4, username: "", imageUrl: "", status: "on" },
- { id: 5, username: "", imageUrl: "", status: "off" },
- { id: 6, username: "", imageUrl: "", status: "away" },
+ { id: 1, username: "", imageUrl: "", status: "off", role: "LEADER" },
+ { id: 2, username: "", imageUrl: "", status: "on", role: "MEMBER" },
+ { id: 3, username: "", imageUrl: "", status: "away", role: "MEMBER" },
+ { id: 4, username: "", imageUrl: "", status: "on", role: "MEMBER" },
+ { id: 5, username: "", imageUrl: "", status: "off", role: "MEMBER" },
+ { id: 6, username: "", imageUrl: "", status: "away", role: "MEMBER" },
];
const sortedMemberIdList = memberList
diff --git a/frontend/src/types/DTO/landingDTO.ts b/frontend/src/types/DTO/landingDTO.ts
index 4d152ae..7d3230c 100644
--- a/frontend/src/types/DTO/landingDTO.ts
+++ b/frontend/src/types/DTO/landingDTO.ts
@@ -2,6 +2,8 @@ import { MemoColorType } from "../common/landing";
export type MemberStatus = "on" | "off" | "away";
+export type MemberRole = "LEADER" | "MEMBER";
+
export interface LandingProjectDTO {
title: string;
subject: string;
@@ -13,6 +15,7 @@ export interface LandingMemberDTO {
username: string;
imageUrl: string;
status: MemberStatus;
+ role: MemberRole;
}
export interface LandingSprintDTO {
diff --git a/frontend/src/types/DTO/settingDTO.ts b/frontend/src/types/DTO/settingDTO.ts
new file mode 100644
index 0000000..2ffe4bc
--- /dev/null
+++ b/frontend/src/types/DTO/settingDTO.ts
@@ -0,0 +1,18 @@
+import { MemberRole } from "./landingDTO";
+
+export interface SettingProjectDTO {
+ title: string;
+ subject: string;
+}
+
+export interface SettingMemberDTO {
+ id: number;
+ username: string;
+ imageUrl: string;
+ role: MemberRole;
+}
+
+export interface SettingDTO {
+ project: SettingProjectDTO;
+ member: SettingMemberDTO[];
+}
diff --git a/frontend/src/types/common/setting.ts b/frontend/src/types/common/setting.ts
new file mode 100644
index 0000000..6780efb
--- /dev/null
+++ b/frontend/src/types/common/setting.ts
@@ -0,0 +1,27 @@
+import { SettingDTO, SettingProjectDTO } from "../DTO/settingDTO";
+
+export enum SettingSocketDomain {
+ INIT = "setting",
+ PROJECT_INFO = "projectInfo",
+}
+
+export enum SettingSocketProjectInfoAction {
+ UPDATE = "update",
+ DELETE = "delete",
+}
+
+interface SettingSocketInitData {
+ domain: SettingSocketDomain.INIT;
+ action: "init";
+ content: SettingDTO;
+}
+
+interface SettingSocketProjectInfoData {
+ domain: SettingSocketDomain.PROJECT_INFO;
+ action: SettingSocketProjectInfoAction;
+ content: SettingProjectDTO;
+}
+
+export type SettingSocketData =
+ | SettingSocketInitData
+ | SettingSocketProjectInfoData;