Skip to content

feat: 세팅 페이지, 프로젝트 제목, 주제 수정 기능 구현 #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import UnfinishedStoryPage from "./pages/backlog/UnfinishedStoryPage";
import BacklogPage from "./pages/backlog/BacklogPage";
import FinishedStoryPage from "./pages/backlog/FinishedStoryPage";
import EpicPage from "./pages/backlog/EpicPage";
import SettingPage from "./pages/setting/SettingPage";

type RouteType = "PRIVATE" | "PUBLIC";

Expand Down Expand Up @@ -96,7 +97,7 @@ const router = createBrowserRouter([
element: <BacklogPage />,
},
{ path: ROUTER_URL.SPRINT, element: <div>sprint Page</div> },
{ path: ROUTER_URL.SETTINGS, element: <div>setting Page</div> },
{ path: ROUTER_URL.SETTINGS, element: <SettingPage /> },
],
},
]),
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/backlog/BacklogHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const BacklogHeader = () => {
return (
<header className="flex items-baseline justify-between">
<div className="flex items-baseline gap-2">
<h1 className="text-m text-dark-green">{TAB_TITLE[lastPath]} 백로그</h1>
<h1 className="font-bold text-m text-middle-green">
{TAB_TITLE[lastPath]} 백로그
</h1>
<p className="">우선순위 내림차순</p>
</div>
<div className="flex gap-1">
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/components/landing/member/LandingMember.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,20 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => {
/>
<div className="flex justify-between text-white">
<p className="text-xs font-bold">| 함께하는 사람들</p>
<button
className="text-xxs hover:underline"
onClick={handleInviteButtonClick}
>
초대링크 복사
</button>
{myInfo.role === "LEADER" && (
<div className="flex gap-1">
<button
className="text-xxs hover:underline"
onClick={handleInviteButtonClick}
>
초대링크 복사
</button>
<span>|</span>
<button className="text-xxs hover:underline" onClick={() => {}}>
링크 변경
</button>
</div>
)}
</div>
{memberList.map((memberData: LandingMemberDTO) => (
<UserBlock {...memberData} key={memberData.id} />
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/setting/InformationInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ChangeEvent } from "react";

interface InformationInputProps {
label: string;
inputId: string;
inputValue: string;
errorMessage: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

const InformationInput = ({
label,
inputId,
inputValue,
errorMessage,
onChange,
}: InformationInputProps) => (
<div className="flex w-full gap-6 mb-5">
<label
className="min-w-[6.125rem] text-xs font-semibold text-middle-green"
htmlFor={inputId}
>
{label}
</label>
<div>
<input
className="w-[60.3rem] mb-1 h-10 border-[2px] border-text-gray rounded-lg focus:outline-middle-green px-1 hover:cursor-pointer"
type="text"
id={inputId}
autoComplete="off"
value={inputValue}
onChange={onChange}
/>
<p className="text-xxxs text-error-red">{errorMessage}</p>
</div>
</div>
);

export default InformationInput;
116 changes: 116 additions & 0 deletions frontend/src/components/setting/InformationSettingSection.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
const { value } = event.target;

setTitleValue(value);

if (!value.trim()) {
setTitleErrorMessage("프로젝트 이름을 입력해주세요.");
return;
}

if (value.length > 255) {
setTitleErrorMessage("프로젝트 이름이 너무 깁니다.");
return;
}

setTitleErrorMessage("");
};

const handleSubjectChange = (event: ChangeEvent<HTMLInputElement>) => {
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 (
<>
<div className="w-full mb-2">
<p className="font-bold text-m text-middle-green">프로젝트 설정</p>
</div>
<div className="flex flex-col w-full">
<InformationInput
label="프로젝트 이름"
inputId="title"
inputValue={titleValue}
errorMessage={titleErrorMessage}
onChange={handleTitleChange}
/>
<InformationInput
label="프로젝트 주제"
inputId="subject"
inputValue={subjectValue}
errorMessage={subjectErrorMessage}
onChange={handleSubjectChange}
/>
<div className="self-end relative w-[4.5rem] h-10">
{!submitActivated && (
<div className="absolute w-full h-full rounded-lg opacity-50 bg-slate-500"></div>
)}

<button
className="w-full h-full text-xs text-white rounded-lg bg-middle-green"
type="button"
disabled={!submitActivated}
onClick={handleSubmit}
>
저장
</button>
</div>
</div>
</>
);
};

export default InformationSettingSection;
32 changes: 32 additions & 0 deletions frontend/src/components/setting/MemberBlock.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex w-full gap-3">
<div className="w-[16.25rem] flex gap-3 items-center">
<img className="w-8 h-8 rounded-full" src={imageUrl} alt={username} />
<p className="">{username}</p>
</div>
<div className="w-[18.75rem]">
<p className="">{role}</p>
</div>
<div className="w-[30rem]">
{myRole === "LEADER" && (
<button
className="px-2 py-1 text-white rounded w-fit bg-error-red text-xxs"
type="button"
>
프로젝트에서 제거
</button>
)}
</div>
</div>
);
};

export default MemberBlock;
26 changes: 26 additions & 0 deletions frontend/src/components/setting/MemberSettingSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LandingMemberDTO } from "../../types/DTO/landingDTO";
import MemberBlock from "./MemberBlock";

interface MemberSettingSectionProps {
memberList: LandingMemberDTO[];
}

const MemberSettingSection = ({ memberList }: MemberSettingSectionProps) => (
<div className="mb-5">
<div className="mb-2">
<p className="font-bold text-m text-middle-green">멤버 관리</p>
</div>
<div className="flex flex-col gap-2 h-[18.52rem]">
<div className="flex w-full gap-3 border-b-[2px] text-[1rem] text-dark-gray">
<p className="w-[16.25rem]">닉네임</p>
<p className="w-[18.75rem]">역할</p>
<p className="w-[30rem]">작업</p>
</div>
<div className="flex flex-col gap-3 overflow-y-auto scrollbar-thin">
{...memberList.map((member) => <MemberBlock {...member} />)}
</div>
</div>
</div>
);

export default MemberSettingSection;
97 changes: 97 additions & 0 deletions frontend/src/components/setting/ProjectDeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
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 (
<div
className="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full bg-black bg-opacity-30"
onClick={handleCloseClick}
>
<div className="px-6 py-7 bg-white rounded-lg w-[23.75rem] h-fit">
<div className="flex justify-between w-full mb-2">
<p className="font-bold text-m">|프로젝트 삭제</p>
<button type="button" onClick={close}>
<Closed width={32} height={32} stroke="black" />
</button>
</div>
<p className="mb-10 text-lg">프로젝트 삭제 후 되돌릴 수 없습니다.</p>
<div className="flex flex-col gap-2">
<p className="text-sm font-bold select-none">
삭제하시려면 "{projectTitle}"을(를) 입력해주세요.
</p>
<input
className="w-full px-1 text-sm border rounded border-error-red outline-error-red"
type="text"
value={inputValue}
onChange={handleInputChange}
/>
<div
className={`${
!confirmed ? "hover:cursor-not-allowed" : ""
} h-7 relative`}
>
{!confirmed && (
<div className="absolute w-full h-full bg-black rounded opacity-40"></div>
)}
<button
className={`w-full h-full text-sm text-center text-white rounded ${
!confirmed ? "bg-gray-300" : "bg-error-red"
}`}
type="button"
disabled={!confirmed}
onClick={handleDeleteButtonClick}
>
프로젝트 삭제
</button>
</div>
</div>
</div>
</div>
);
};

export default ProjectDeleteModal;
Loading
Loading