Skip to content

Commit c9aa172

Browse files
authored
Merge pull request #335 from boostcampwm2023/feature/setting-page
feat: 세팅 페이지, 프로젝트 제목, 주제 수정 기능 구현
2 parents 4c7783b + b96ef81 commit c9aa172

20 files changed

+579
-20
lines changed

frontend/src/AppRouter.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import UnfinishedStoryPage from "./pages/backlog/UnfinishedStoryPage";
2222
import BacklogPage from "./pages/backlog/BacklogPage";
2323
import FinishedStoryPage from "./pages/backlog/FinishedStoryPage";
2424
import EpicPage from "./pages/backlog/EpicPage";
25+
import SettingPage from "./pages/setting/SettingPage";
2526

2627
type RouteType = "PRIVATE" | "PUBLIC";
2728

@@ -96,7 +97,7 @@ const router = createBrowserRouter([
9697
element: <BacklogPage />,
9798
},
9899
{ path: ROUTER_URL.SPRINT, element: <div>sprint Page</div> },
99-
{ path: ROUTER_URL.SETTINGS, element: <div>setting Page</div> },
100+
{ path: ROUTER_URL.SETTINGS, element: <SettingPage /> },
100101
],
101102
},
102103
]),

frontend/src/components/backlog/BacklogHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const BacklogHeader = () => {
1414
return (
1515
<header className="flex items-baseline justify-between">
1616
<div className="flex items-baseline gap-2">
17-
<h1 className="text-m text-dark-green">{TAB_TITLE[lastPath]} 백로그</h1>
17+
<h1 className="font-bold text-m text-middle-green">
18+
{TAB_TITLE[lastPath]} 백로그
19+
</h1>
1820
<p className="">우선순위 내림차순</p>
1921
</div>
2022
<div className="flex gap-1">

frontend/src/components/landing/member/LandingMember.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,20 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => {
9494
/>
9595
<div className="flex justify-between text-white">
9696
<p className="text-xs font-bold">| 함께하는 사람들</p>
97-
<button
98-
className="text-xxs hover:underline"
99-
onClick={handleInviteButtonClick}
100-
>
101-
초대링크 복사
102-
</button>
97+
{myInfo.role === "LEADER" && (
98+
<div className="flex gap-1">
99+
<button
100+
className="text-xxs hover:underline"
101+
onClick={handleInviteButtonClick}
102+
>
103+
초대링크 복사
104+
</button>
105+
<span>|</span>
106+
<button className="text-xxs hover:underline" onClick={() => {}}>
107+
링크 변경
108+
</button>
109+
</div>
110+
)}
103111
</div>
104112
{memberList.map((memberData: LandingMemberDTO) => (
105113
<UserBlock {...memberData} key={memberData.id} />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ChangeEvent } from "react";
2+
3+
interface InformationInputProps {
4+
label: string;
5+
inputId: string;
6+
inputValue: string;
7+
errorMessage: string;
8+
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
9+
}
10+
11+
const InformationInput = ({
12+
label,
13+
inputId,
14+
inputValue,
15+
errorMessage,
16+
onChange,
17+
}: InformationInputProps) => (
18+
<div className="flex w-full gap-6 mb-5">
19+
<label
20+
className="min-w-[6.125rem] text-xs font-semibold text-middle-green"
21+
htmlFor={inputId}
22+
>
23+
{label}
24+
</label>
25+
<div>
26+
<input
27+
className="w-[60.3rem] mb-1 h-10 border-[2px] border-text-gray rounded-lg focus:outline-middle-green px-1 hover:cursor-pointer"
28+
type="text"
29+
id={inputId}
30+
autoComplete="off"
31+
value={inputValue}
32+
onChange={onChange}
33+
/>
34+
<p className="text-xxxs text-error-red">{errorMessage}</p>
35+
</div>
36+
</div>
37+
);
38+
39+
export default InformationInput;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { ChangeEvent, useEffect, useMemo, useState } from "react";
2+
import InformationInput from "./InformationInput";
3+
import { useOutletContext } from "react-router-dom";
4+
import { Socket } from "socket.io-client";
5+
import useSettingProjectInfoSocket from "../../hooks/pages/setting/useSettingProjectInfoSocket";
6+
7+
interface InformationSettingSectionProps {
8+
title: string;
9+
subject: string;
10+
}
11+
12+
const InformationSettingSection = ({
13+
title,
14+
subject,
15+
}: InformationSettingSectionProps) => {
16+
const { socket }: { socket: Socket } = useOutletContext();
17+
const { emitProjectInfoUpdateEvent } = useSettingProjectInfoSocket(socket);
18+
const [titleValue, setTitleValue] = useState(title);
19+
const [titleErrorMessage, setTitleErrorMessage] = useState("");
20+
const [subjectValue, setSubjectValue] = useState(subject);
21+
const [subjectErrorMessage, setSubjectErrorMessage] = useState("");
22+
const submitActivated = useMemo(
23+
() =>
24+
!(
25+
!titleValue ||
26+
!subjectValue ||
27+
(titleValue === title && subjectValue === subject)
28+
),
29+
[title, subject, titleValue, subjectValue]
30+
);
31+
32+
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
33+
const { value } = event.target;
34+
35+
setTitleValue(value);
36+
37+
if (!value.trim()) {
38+
setTitleErrorMessage("프로젝트 이름을 입력해주세요.");
39+
return;
40+
}
41+
42+
if (value.length > 255) {
43+
setTitleErrorMessage("프로젝트 이름이 너무 깁니다.");
44+
return;
45+
}
46+
47+
setTitleErrorMessage("");
48+
};
49+
50+
const handleSubjectChange = (event: ChangeEvent<HTMLInputElement>) => {
51+
const { value } = event.target;
52+
53+
setSubjectValue(value);
54+
55+
if (!value.trim()) {
56+
setSubjectErrorMessage("프로젝트 주제를 입력해주세요.");
57+
return;
58+
}
59+
60+
if (value.length > 255) {
61+
setSubjectErrorMessage("프로젝트 주제가 너무 깁니다.");
62+
return;
63+
}
64+
65+
setSubjectErrorMessage("");
66+
};
67+
68+
const handleSubmit = () => {
69+
emitProjectInfoUpdateEvent({ title: titleValue, subject: subjectValue });
70+
};
71+
72+
useEffect(() => {
73+
setTitleValue(title);
74+
setSubjectValue(subject);
75+
}, [title, subject]);
76+
77+
return (
78+
<>
79+
<div className="w-full mb-2">
80+
<p className="font-bold text-m text-middle-green">프로젝트 설정</p>
81+
</div>
82+
<div className="flex flex-col w-full">
83+
<InformationInput
84+
label="프로젝트 이름"
85+
inputId="title"
86+
inputValue={titleValue}
87+
errorMessage={titleErrorMessage}
88+
onChange={handleTitleChange}
89+
/>
90+
<InformationInput
91+
label="프로젝트 주제"
92+
inputId="subject"
93+
inputValue={subjectValue}
94+
errorMessage={subjectErrorMessage}
95+
onChange={handleSubjectChange}
96+
/>
97+
<div className="self-end relative w-[4.5rem] h-10">
98+
{!submitActivated && (
99+
<div className="absolute w-full h-full rounded-lg opacity-50 bg-slate-500"></div>
100+
)}
101+
102+
<button
103+
className="w-full h-full text-xs text-white rounded-lg bg-middle-green"
104+
type="button"
105+
disabled={!submitActivated}
106+
onClick={handleSubmit}
107+
>
108+
저장
109+
</button>
110+
</div>
111+
</div>
112+
</>
113+
);
114+
};
115+
116+
export default InformationSettingSection;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import useMemberStore from "../../stores/useMemberStore";
2+
import { LandingMemberDTO } from "../../types/DTO/landingDTO";
3+
4+
interface MemberBlockProps extends LandingMemberDTO {}
5+
6+
const MemberBlock = ({ username, imageUrl, role }: MemberBlockProps) => {
7+
const myRole = useMemberStore((state) => state.myInfo.role);
8+
9+
return (
10+
<div className="flex w-full gap-3">
11+
<div className="w-[16.25rem] flex gap-3 items-center">
12+
<img className="w-8 h-8 rounded-full" src={imageUrl} alt={username} />
13+
<p className="">{username}</p>
14+
</div>
15+
<div className="w-[18.75rem]">
16+
<p className="">{role}</p>
17+
</div>
18+
<div className="w-[30rem]">
19+
{myRole === "LEADER" && (
20+
<button
21+
className="px-2 py-1 text-white rounded w-fit bg-error-red text-xxs"
22+
type="button"
23+
>
24+
프로젝트에서 제거
25+
</button>
26+
)}
27+
</div>
28+
</div>
29+
);
30+
};
31+
32+
export default MemberBlock;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LandingMemberDTO } from "../../types/DTO/landingDTO";
2+
import MemberBlock from "./MemberBlock";
3+
4+
interface MemberSettingSectionProps {
5+
memberList: LandingMemberDTO[];
6+
}
7+
8+
const MemberSettingSection = ({ memberList }: MemberSettingSectionProps) => (
9+
<div className="mb-5">
10+
<div className="mb-2">
11+
<p className="font-bold text-m text-middle-green">멤버 관리</p>
12+
</div>
13+
<div className="flex flex-col gap-2 h-[18.52rem]">
14+
<div className="flex w-full gap-3 border-b-[2px] text-[1rem] text-dark-gray">
15+
<p className="w-[16.25rem]">닉네임</p>
16+
<p className="w-[18.75rem]">역할</p>
17+
<p className="w-[30rem]">작업</p>
18+
</div>
19+
<div className="flex flex-col gap-3 overflow-y-auto scrollbar-thin">
20+
{...memberList.map((member) => <MemberBlock {...member} />)}
21+
</div>
22+
</div>
23+
</div>
24+
);
25+
26+
export default MemberSettingSection;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { ChangeEvent, MouseEventHandler, useState } from "react";
2+
import useSettingProjectSocket from "../../hooks/pages/setting/useSettingProjectSocket";
3+
import Closed from "../../assets/icons/closed.svg?react";
4+
import { Socket } from "socket.io-client";
5+
6+
interface ProjectDeleteModalProps {
7+
projectTitle: string;
8+
socket: Socket;
9+
close: () => void;
10+
}
11+
12+
const ProjectDeleteModal = ({
13+
projectTitle,
14+
socket,
15+
close,
16+
}: ProjectDeleteModalProps) => {
17+
const [inputValue, setInputValue] = useState("");
18+
const [confirmed, setConfirmed] = useState(false);
19+
20+
const { emitProjectDeleteEvent } = useSettingProjectSocket(socket);
21+
22+
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
23+
const { value } = event.target;
24+
25+
setInputValue(value);
26+
27+
if (value === projectTitle) {
28+
setConfirmed(true);
29+
} else {
30+
setConfirmed(false);
31+
}
32+
};
33+
34+
const handleCloseClick: MouseEventHandler<
35+
HTMLButtonElement | HTMLDivElement
36+
> = ({ target, currentTarget }: React.MouseEvent) => {
37+
if (target !== currentTarget) {
38+
return;
39+
}
40+
close();
41+
};
42+
43+
const handleDeleteButtonClick = () => {
44+
if (confirmed) {
45+
emitProjectDeleteEvent();
46+
}
47+
};
48+
49+
return (
50+
<div
51+
className="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full bg-black bg-opacity-30"
52+
onClick={handleCloseClick}
53+
>
54+
<div className="px-6 py-7 bg-white rounded-lg w-[23.75rem] h-fit">
55+
<div className="flex justify-between w-full mb-2">
56+
<p className="font-bold text-m">|프로젝트 삭제</p>
57+
<button type="button" onClick={close}>
58+
<Closed width={32} height={32} stroke="black" />
59+
</button>
60+
</div>
61+
<p className="mb-10 text-lg">프로젝트 삭제 후 되돌릴 수 없습니다.</p>
62+
<div className="flex flex-col gap-2">
63+
<p className="text-sm font-bold select-none">
64+
삭제하시려면 "{projectTitle}"을(를) 입력해주세요.
65+
</p>
66+
<input
67+
className="w-full px-1 text-sm border rounded border-error-red outline-error-red"
68+
type="text"
69+
value={inputValue}
70+
onChange={handleInputChange}
71+
/>
72+
<div
73+
className={`${
74+
!confirmed ? "hover:cursor-not-allowed" : ""
75+
} h-7 relative`}
76+
>
77+
{!confirmed && (
78+
<div className="absolute w-full h-full bg-black rounded opacity-40"></div>
79+
)}
80+
<button
81+
className={`w-full h-full text-sm text-center text-white rounded ${
82+
!confirmed ? "bg-gray-300" : "bg-error-red"
83+
}`}
84+
type="button"
85+
disabled={!confirmed}
86+
onClick={handleDeleteButtonClick}
87+
>
88+
프로젝트 삭제
89+
</button>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
);
95+
};
96+
97+
export default ProjectDeleteModal;

0 commit comments

Comments
 (0)