From 750343bd3dbd411e5cf3565a3cd2bccf3b1fe499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Fri, 1 Nov 2024 15:34:03 +0900 Subject: [PATCH 01/33] =?UTF-8?q?fix:=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=B6=9C=EC=84=9D=EC=B2=B4=ED=81=AC=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=B4=20=ED=99=9C=EC=84=B1=ED=99=94=20=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95(UserAttendModal.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #124 --- .../userAttendModal/AttendStatusView.tsx | 3 -- .../userAttendModal/UserAttendModal.tsx | 34 +++++++++++++++---- FE/src/hooks/query/useProgramQuery.ts | 1 + 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx b/FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx index e925d20e..3aff4cb8 100644 --- a/FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx +++ b/FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx @@ -17,10 +17,7 @@ const AttendStatusView = ({ userInfo, programId }: AttendStatusViewProps) => { programId, ]); - console.log(programType); - console.log(attendStatus); const { demand_text, text, color } = ATTEND_STATUS.USER[attendStatus]; - console.log(demand_text, text, color); const isDemandNonResponse = programType === "demand" && attendStatus === "nonResponse"; const displayText = isDemandNonResponse ? demand_text : text; diff --git a/FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx b/FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx index 52d79854..5104083f 100644 --- a/FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx +++ b/FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx @@ -10,7 +10,7 @@ import { } from "@/hooks/query/useUserQuery"; import { EditableStatus } from "@/types/attendStatusModal"; import { AttendStatus } from "@/types/member"; -import { ProgramStatus } from "@/types/program"; +import { ProgramAttendStatus, ProgramStatus } from "@/types/program"; interface UserAttendModalProps { programId: number; @@ -24,6 +24,10 @@ const UserAttendModal = ({ programId }: UserAttendModalProps) => { if (isLoading) return ; const { attendStatus } = userInfo; + const attendMode = queryClient.getQueryData([ + "attendMode", + programId, + ]); const programStatus = queryClient.getQueryData([ "programStatus", programId, @@ -31,16 +35,32 @@ const UserAttendModal = ({ programId }: UserAttendModalProps) => { const getEditableStatus = ( attendStatus: AttendStatus, + programAttendMode: ProgramAttendStatus, programStatus: ProgramStatus, ): EditableStatus => { - if (attendStatus === "nonRelated") return "NON_RELATED"; - if (programStatus === "end" || attendStatus === "absent") return "INACTIVE"; - if (attendStatus === "attend" || attendStatus === "late") - return "ALREADY_ATTENDED"; - return "EDITABLE"; + if (attendStatus === "nonRelated") { + return "NON_RELATED"; + } + if (programStatus === "active") { + // 당일날에 + if (programAttendMode === "attend" || programAttendMode === "late") { + // 출석 중이면 + if (attendStatus === "attend" || attendStatus === "late") + // 내 상태가 참석 또는 지각이면 + return "ALREADY_ATTENDED"; // 변경 불가 + + return "EDITABLE"; //내 상태가 없다면 변경 가능 + } + if (programAttendMode === "end") return "NON_RELATED"; // 당일날 종료되었으면 변경 불가 + } + return "INACTIVE"; // 당일날이 아니면 변경 불가 }; - const editableStatus = getEditableStatus(attendStatus, programStatus); + const editableStatus = getEditableStatus( + attendStatus, + attendMode, + programStatus, + ); const handleSelectorClick = () => { if (editableStatus === "EDITABLE") diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index dad34904..1261b12f 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -91,6 +91,7 @@ export const useGetProgramByProgramId = ( ["programStatus", programId], res.programStatus, ); + queryClient.setQueryData(["attendMode", programId], res.attendMode); queryClient.setQueryData( ["programType", programId], res.type, From 005bf7256fffa29b3a8c8098e8dc3fe49aedfe30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Sat, 2 Nov 2024 16:31:10 +0900 Subject: [PATCH 02/33] =?UTF-8?q?chore:=20github=20url=EC=9D=98=20validati?= =?UTF-8?q?on=EC=9D=98=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EC=A0=84=EB=B6=80?= =?UTF-8?q?=20=EB=B0=B1=EC=97=94=EB=93=9C=EA=B0=80=20=EA=B0=80=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/form/program/CreateForm.tsx | 12 ++++++------ FE/src/components/programEdit/EditForm.tsx | 13 +++++++------ FE/src/utils/convert.ts | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/FE/src/components/common/form/program/CreateForm.tsx b/FE/src/components/common/form/program/CreateForm.tsx index 59e0eae8..3cd3fa34 100644 --- a/FE/src/components/common/form/program/CreateForm.tsx +++ b/FE/src/components/common/form/program/CreateForm.tsx @@ -21,7 +21,7 @@ import { import { useMemberSet } from "@/hooks/useMemberForm"; import { ProgramCategory } from "@/types/program"; import { TeamInputInfo } from "@/types/team"; -import { checkIsValidateGithubUrl } from "@/utils/github"; +// import { checkIsValidateGithubUrl } from "@/utils/github"; export interface ProgramFormDataState { title: string; @@ -75,12 +75,12 @@ const CreateForm = () => { return; } - const isValidGithubUrl = checkIsValidateGithubUrl(programGithubUrl); + // const isValidGithubUrl = checkIsValidateGithubUrl(programGithubUrl); - if (!isValidGithubUrl) { - toast.error("올바른 Github URL을 입력해주세요."); - return; - } + // if (!isValidGithubUrl) { + // toast.error("올바른 Github URL을 입력해주세요."); + // return; + // } const toastId = toast.loading(MESSAGE.CREATE.PENDING); createProgramMutate( diff --git a/FE/src/components/programEdit/EditForm.tsx b/FE/src/components/programEdit/EditForm.tsx index 116f210b..51f3a03e 100644 --- a/FE/src/components/programEdit/EditForm.tsx +++ b/FE/src/components/programEdit/EditForm.tsx @@ -24,7 +24,7 @@ import { import { useMemberMap } from "@/hooks/useMemberForm"; import { ProgramCategory } from "@/types/program"; import { TeamInputInfo } from "@/types/team"; -import { checkIsValidateGithubUrl } from "@/utils/github"; +// import { checkIsValidateGithubUrl } from "@/utils/github"; const initialState: ProgramFormDataState = { title: "", @@ -89,12 +89,13 @@ const EditForm = ({ programId }: EditFormProps) => { return; } - const isValidGithubUrl = checkIsValidateGithubUrl(programGithubUrl); + // Github URL 유효성 검사는 전부 백엔드로 위임 (#127) + // const isValidGithubUrl = checkIsValidateGithubUrl(programGithubUrl); - if (!isValidGithubUrl) { - toast.error("올바른 Github URL을 입력해주세요."); - return; - } + // if (!isValidGithubUrl) { + // toast.error("올바른 Github URL을 입력해주세요."); + // return; + // } const toastId = toast.loading(MESSAGE.EDIT.PENDING); diff --git a/FE/src/utils/convert.ts b/FE/src/utils/convert.ts index 59056e32..8a9770a4 100644 --- a/FE/src/utils/convert.ts +++ b/FE/src/utils/convert.ts @@ -25,8 +25,8 @@ export const convertText = (text: string, str: string) => { //githubUrl을 owner, repo, branch, path로 분리 export const convertGitHubUrl = (githubUrl: string) => { - const isValidateGithubUrl = checkIsValidateGithubUrl(githubUrl); - if (!isValidateGithubUrl) throw new Error("올바르지 않은 깃허브 url입니다."); + // const isValidateGithubUrl = checkIsValidateGithubUrl(githubUrl); + // if (!isValidateGithubUrl) throw new Error("올바르지 않은 깃허브 url입니다."); const parsedUrl = new URL(githubUrl); const parts = parsedUrl.pathname.split("/").filter(Boolean); From 5580aeb7e8e7dc90d5bd4373d586000bfec7d12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Sat, 2 Nov 2024 16:56:42 +0900 Subject: [PATCH 03/33] =?UTF-8?q?test:=20=EC=8A=AC=EB=9E=99=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=9A=94=EC=B2=AD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20url=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/apis/__test__/program.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FE/src/apis/__test__/program.test.ts b/FE/src/apis/__test__/program.test.ts index d7b54aee..37d8bc23 100644 --- a/FE/src/apis/__test__/program.test.ts +++ b/FE/src/apis/__test__/program.test.ts @@ -301,7 +301,7 @@ describe("sendSlackMessage", () => { // 내부 구현사항 expect(https).toHaveBeenCalledWith({ - data: { programUrl: "https://econo.eeos.store/detail/1" }, + data: { programUrl: "https://eeos.co.kr/detail/1" }, method: "POST", url: "/programs/1/slack/notification", }); @@ -381,6 +381,7 @@ describe("patchProgram", () => { }, ], teams: [{ teamId: 1 }], + programGithubUrl: "", }; const programId = 1; From 49109922daa5217e51d8160b55b28521414e360a Mon Sep 17 00:00:00 2001 From: Klomachenko Date: Thu, 21 Nov 2024 15:36:04 +0900 Subject: [PATCH 04/33] =?UTF-8?q?feat:=20member=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20polling=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/query/useMemberQuery.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FE/src/hooks/query/useMemberQuery.ts b/FE/src/hooks/query/useMemberQuery.ts index 54b79c2a..3f9eeb1b 100644 --- a/FE/src/hooks/query/useMemberQuery.ts +++ b/FE/src/hooks/query/useMemberQuery.ts @@ -53,6 +53,13 @@ export const useGetProgramMembersByAttend = ({ queryKey: [API.MEMBER.ATTEND_STATUS(programId), status], queryFn: () => getProgramMembersByAttendStatus(programId, status), staleTime: 1000 * 60 * 5, + refetchInterval: () => { + if (status === "attend") { + return 2000; + } else { + return false; + } + }, }); }; From 7f65c080ee69e6ba7b2c55b3b2da51756da9313c Mon Sep 17 00:00:00 2001 From: Klomachenko Date: Sun, 24 Nov 2024 19:33:12 +0900 Subject: [PATCH 05/33] =?UTF-8?q?feat:=20attendStatus=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20refetchInterval=EC=9D=84=20=ED=86=B5=ED=95=9C=20pol?= =?UTF-8?q?ling=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/query/useMemberQuery.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/FE/src/hooks/query/useMemberQuery.ts b/FE/src/hooks/query/useMemberQuery.ts index 3f9eeb1b..ffad68fa 100644 --- a/FE/src/hooks/query/useMemberQuery.ts +++ b/FE/src/hooks/query/useMemberQuery.ts @@ -13,6 +13,7 @@ import { ActiveStatusWithAll, AttendStatus, } from "@/types/member"; +import { ProgramAttendStatus } from "@/types/program"; export const useGetMemberByActive = (activeStatus: ActiveStatusWithAll) => { return useQuery({ @@ -49,12 +50,18 @@ export const useGetProgramMembersByAttend = ({ programId, status, }: GetProgramMemebersByAttend) => { + const queryClient = useQueryClient(); + const adminAttendStatus = queryClient.getQueryData([ + "attendMode", + programId, + ]); + return useQuery({ queryKey: [API.MEMBER.ATTEND_STATUS(programId), status], queryFn: () => getProgramMembersByAttendStatus(programId, status), staleTime: 1000 * 60 * 5, refetchInterval: () => { - if (status === "attend") { + if (adminAttendStatus === "attend") { return 2000; } else { return false; From 47f779d4fb580c55946c8de5837de342d0b8aee6 Mon Sep 17 00:00:00 2001 From: Klomachenko Date: Sun, 24 Nov 2024 20:36:31 +0900 Subject: [PATCH 06/33] =?UTF-8?q?feat:=20program=20attendMode=20statleTime?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pollingm은 적용되었으나, admin에서 지각으로 변경해도 계속 polling이 받아와짐, 새로고침을 하면 더이상 polling이 되지 않으나 새로고침을 하지 않는 경우 주간발표가 끝날때까지 2초마다 요청 발생. 이를 해결하기 위해 useGetProgramId 의 staleTime을 5분에서 1분으로 변경 --- FE/src/hooks/query/useProgramQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index 1261b12f..ff4f93e6 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -102,7 +102,7 @@ export const useGetProgramByProgramId = ( ); return res; }), - staleTime: 1000 * 60 * 5, + staleTime: 1000 * 60, }); }; From 42ec3ff170ed750ba9439d87f1597de377852afc Mon Sep 17 00:00:00 2001 From: Klomachenko Date: Sun, 24 Nov 2024 22:13:12 +0900 Subject: [PATCH 07/33] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20polling=20=EB=8F=99=EC=8B=9C=EC=84=B1=20race=20cond?= =?UTF-8?q?ition=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 멤버 상태의 polling에 대한 동시성 이슈를 해결하였습니다. cancelQueries를 통해 실행중이던 useGetProgramMembersByAttend 쿼리를 최초에 취소를 하여 동시성을 해결합니다. --- FE/src/hooks/query/useProgramQuery.ts | 44 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index ff4f93e6..130dfcf9 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -85,23 +85,33 @@ export const useGetProgramByProgramId = ( return useQuery({ queryKey: [API.PROGRAM.Edit_DETAIL(programId)], queryFn: () => - getProgramById(programId, isAbleToEdit).then((res) => { - //TODO: setquery 지양하기 - queryClient.setQueryData( - ["programStatus", programId], - res.programStatus, - ); - queryClient.setQueryData(["attendMode", programId], res.attendMode); - queryClient.setQueryData( - ["programType", programId], - res.type, - ); - queryClient.setQueryData( - ["githubUrl", programId], - res.programGithubUrl, - ); - return res; - }), + getProgramById(programId, isAbleToEdit) + .then((res) => { + queryClient.cancelQueries({ + queryKey: [API.MEMBER.ATTEND_STATUS(programId)], + }); + return res; + }) + .then((res) => { + //TODO: setquery 지양하기 + queryClient.setQueryData( + ["programStatus", programId], + res.programStatus, + ); + queryClient.setQueryData(["attendMode", programId], res.attendMode); + queryClient.setQueryData( + ["programType", programId], + res.type, + ); + queryClient.setQueryData( + ["githubUrl", programId], + res.programGithubUrl, + ); + queryClient.invalidateQueries({ + queryKey: [API.MEMBER.ATTEND_STATUS(programId)], + }); + return res; + }), staleTime: 1000 * 60, }); }; From 75227a8e5c681cc88686f6b5a0860adefa52a920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Wed, 27 Nov 2024 23:50:36 +0900 Subject: [PATCH 08/33] =?UTF-8?q?refactor:=20=EC=BD=94=EB=94=A9=20?= =?UTF-8?q?=EC=BB=A8=EB=B0=B4=EC=85=98=EC=97=90=20=EB=A7=9E=EC=B6=94?= =?UTF-8?q?=EC=96=B4=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로그램의 질문 게시판 section을 feature로 이동 #141 --- .../Dashboard/ProgramDashboardSection.tsx | 68 ++++++++++++ .../detail/Dashboard/components/Chat.tsx | 37 +++++++ .../detail/Dashboard/components/ChatList.tsx | 74 +++++++++++++ .../Dashboard/components/DashboardContent.tsx | 39 +++++++ .../components/DashboardContentChatBox.tsx | 35 ++++++ .../Dashboard/components/DashboardInput.tsx | 103 ++++++++++++++++++ .../detail/Dashboard/components/ReplyChat.tsx | 27 +++++ .../feature/detail/ProgramDetail.tsx | 35 ++++++ .../feature/detail/ProgramPresentations.tsx | 51 +++++++++ .../detail/loader/ProgramDetail.skeleton.tsx | 10 ++ .../detail/loader/ProgramHeader.skeleton.tsx | 11 ++ .../detail/loader/ProgramInfo.loader.tsx | 12 ++ 12 files changed, 502 insertions(+) create mode 100644 FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/Chat.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/ChatList.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx create mode 100644 FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx create mode 100644 FE/src/components/feature/detail/ProgramDetail.tsx create mode 100644 FE/src/components/feature/detail/ProgramPresentations.tsx create mode 100644 FE/src/components/feature/detail/loader/ProgramDetail.skeleton.tsx create mode 100644 FE/src/components/feature/detail/loader/ProgramHeader.skeleton.tsx create mode 100644 FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx new file mode 100644 index 00000000..b92179ee --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -0,0 +1,68 @@ +"use client"; + +import Tab from "@/components/common/tabs/tab/TabCompound/TabCompound"; +import Title from "@/components/common/Title/Title"; +import { useTeamQuery } from "@/hooks/query/useTeamQuery"; +import DashboardContent from "./components/DashboardContent"; +import DashboardInput from "./components/DashboardInput"; + +interface ProgramDashboardSectionProps { + programId: number; +} +const ProgramDashboardSection = ({ + programId, +}: ProgramDashboardSectionProps) => { + const { data, isLoading } = useTeamQuery(programId); + + if (isLoading || !data) return null; + + const { teams } = data; + if (teams.length === 0) return null; + + const teamNameArray = teams.map(({ teamName }) => teamName); + + return ( +
+ + <Tab<string> + align="line" + defaultSelected={`${teams[0].teamName}`} + nonPickedColor="gray" + pickedColor="navy" + tabItemList={teamNameArray} + tabSize="md" + > + <Tab.List> + {teamNameArray.map((name, index) => ( + <Tab.Item key={`${name}-${index}`} text={name} /> + ))} + </Tab.List> + <Tab.Content<string>> + {({ selectedItem }) => ( + <div className="mt-8 flex flex-col gap-8"> + {/* Board */} + <DashboardContent + programId={programId} + selectedTeamId={ + teams.find(({ teamName }) => teamName === selectedItem) + ?.teamId + } + /> + + <DashboardInput + teams={teams} + programId={programId} + selectedTeamId={ + teams.find(({ teamName }) => teamName === selectedItem) + ?.teamId + } + /> + </div> + )} + </Tab.Content> + </Tab> + </section> + ); +}; + +export default ProgramDashboardSection; diff --git a/FE/src/components/feature/detail/Dashboard/components/Chat.tsx b/FE/src/components/feature/detail/Dashboard/components/Chat.tsx new file mode 100644 index 00000000..c6aa47b0 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/Chat.tsx @@ -0,0 +1,37 @@ +import ChatList from "./ChatList"; +import { Comment } from "@/apis/dtos/question.dto"; +import ReplyChat from "./ReplyChat"; + +const Chat = ({ + commentId, + content, + writer, + time, + answers, + accessRight, +}: Comment) => { + return ( + <div className="border p-4"> + <ChatList + showReplyButton={true} + markdownStyle="" + commentId={commentId} + writer={writer} + accessRight={accessRight} + time={time} + content={content} + /> + <div className="mt-8 px-14"> + {answers && ( + <> + {answers.map((props) => ( + <ReplyChat key={props.commentId} {...props} /> + ))} + </> + )} + </div> + </div> + ); +}; + +export default Chat; diff --git a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx new file mode 100644 index 00000000..a5359333 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx @@ -0,0 +1,74 @@ +import { + useDeleteQuestion, + useUpdateQuestion, +} from "@/hooks/query/useQuestionQuery"; +import ChatBox, { + ChatBoxInnerData, + UpdateComment, +} from "@/components/common/dashboard/ChatBox"; +import { useSetAtom } from "jotai"; +import dashboardAtoms from "@/store/dashboardAtoms"; + +interface ChatListProps { + commentId: number; + writer: string; + accessRight: "edit" | "read_only"; + time: string; + content: string; + markdownStyle?: string; + showReplyButton?: boolean; +} +const ChatList = ({ + writer, + accessRight, + commentId, + content, + time, + markdownStyle, + showReplyButton = true, +}: ChatListProps) => { + const setSelectedCommentId = useSetAtom(dashboardAtoms.selectedCommentId); + const setSelectedCommentContent = useSetAtom( + dashboardAtoms.selectedCommentContent, + ); + + const { mutate: updateComment, isSuccess: isUpdateSuccess } = + useUpdateQuestion(); + const { mutate: deleteComment, isSuccess: isDeleteSuccess } = + useDeleteQuestion(); + + const handleReply = () => { + setSelectedCommentId(commentId); + setSelectedCommentContent(content); + }; + + const handleUpdateComment = ({ + newContents, + setUserInputToModify, + }: UpdateComment) => { + updateComment({ commentId, contents: newContents }); + isUpdateSuccess && setUserInputToModify(newContents); + }; + + const handleDeleteComment = ({ setUserInputToModify }: ChatBoxInnerData) => { + deleteComment(commentId); + isDeleteSuccess && setUserInputToModify(""); + }; + + return ( + <ChatBox + writer={writer} + accessRight={accessRight} + commentId={commentId} + defaultContent={content} + deleteComment={handleDeleteComment} + handleReply={handleReply} + time={time} + updateComment={handleUpdateComment} // + markdownStyle={markdownStyle} + showReplyButton={showReplyButton} // + /> + ); +}; + +export default ChatList; diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx new file mode 100644 index 00000000..48853f32 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx @@ -0,0 +1,39 @@ +"use client"; + +import Chat from "@/components/feature/detail/Dashboard/components/Chat"; +import { useGetQuestions } from "@/hooks/query/useQuestionQuery"; + +interface DashboardContentProps { + programId: number; + selectedTeamId: number; +} +const DashboardContent = ({ + programId, + selectedTeamId, +}: DashboardContentProps) => { + console.log("programId", programId); + console.log("selectedTeamId", selectedTeamId); + + const { data, isLoading, error } = useGetQuestions(programId, selectedTeamId); + + // TODO: Loader 적용, 에러 처리 + if (isLoading) return <div>Loading...</div>; + if (error) return <div>Error...</div>; + + const { comments } = data; + + return ( + <div className="flex max-h-[36rem] w-full flex-col overflow-hidden overflow-y-auto rounded-sm border"> + {comments.length === 0 && ( + <div className="flex h-full items-center justify-center py-20 text-xl text-gray-30"> + 아직 질문이 없습니다. 🥲 + </div> + )} + {comments.map((props) => ( + <Chat key={props.commentId} {...props} /> + ))} + </div> + ); +}; + +export default DashboardContent; diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx new file mode 100644 index 00000000..b8ce3ef4 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx @@ -0,0 +1,35 @@ +import { Comment } from "@/apis/dtos/question.dto"; +import ChatList from "../../DashboardCompound/Board/common/ChatList"; +import ReplyChat from "../../DashboardCompound/Board/ReplyChat"; + +const DashboardContentChatBox = ({ + commentId, + content, + writer, + time, + answers, + accessRight, +}: Comment) => { + return ( + <div className="border p-4"> + <ChatList + commentId={commentId} + writer={writer} + accessRight={accessRight} + time={time} + content={content} + /> + <div className="mt-8 px-14"> + {answers && ( + <> + {answers.map((props) => ( + <ReplyChat key={props.commentId} {...props} /> + ))} + </> + )} + </div> + </div> + ); +}; + +export default DashboardContentChatBox; diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx new file mode 100644 index 00000000..f7689c4c --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { PostQuestionParams } from "@/apis/question"; +import StatusToggleItem from "@/components/common/StatusToggleItem"; +import { usePostQuestion } from "@/hooks/query/useQuestionQuery"; +import { useGetAccessType } from "@/hooks/useAccess"; +import dashboardAtoms from "@/store/dashboardAtoms"; +import { TeamInfo } from "@/types/team"; +import { useAtom, useAtomValue } from "jotai"; +import { useState } from "react"; + +interface DashboardInputProps { + programId: number; + selectedTeamId: number; + teams: TeamInfo[]; +} + +//TODO: UI 분리하기 +const DashboardInput = ({ + programId, + selectedTeamId, + teams, +}: DashboardInputProps) => { + const [questionInput, setQuestionInput] = useState<string>(""); + const [selectedCommentId, setSelectedCommentId] = useAtom( + dashboardAtoms.selectedCommentId, + ); + const [selectedCommentContent, setSelectedCommentContent] = useAtom( + dashboardAtoms.selectedCommentContent, + ); + + const { mutate } = usePostQuestion(); + const isReply = selectedCommentId !== -1; + const selectedTeamName = teams?.find((team) => team.teamId === selectedTeamId) + ?.teamName; + + const accessType = useGetAccessType(); + + const isAbleToPost = accessType === "private"; + + const handlePostQuestion = () => { + const questionContent = questionInput.trim(); + + if (!questionContent) return; + if (!isAbleToPost) return; + + const postQuestionParams: PostQuestionParams = { + programId, + teamId: selectedTeamId, + questionContent, + parentsCommentId: selectedCommentId, + }; + mutate(postQuestionParams); + setQuestionInput(""); + setSelectedCommentId(-1); + setSelectedCommentContent(""); + }; + + const resetSelectedComment = () => { + setSelectedCommentId(-1); + setSelectedCommentContent(""); + }; + + return ( + <div> + {/* <div className="absolute z-10 text-xl font-bold">{name}</div> */} + {isReply ? ( + <div className="truncate text-lg font-semibold"> + {/* <Image src={"/icons/x.svg"} alt="답글 종료" width={20} height={20} /> */} + {/* <button className="px-2 " onClick={() => setselectedCommentId(-1)}> */} + <button className="px-2 " onClick={resetSelectedComment}> + x + </button> + <p className="inline text-xl font-bold">답변하기 :</p> + <p className="ml-2 inline opacity-50">{selectedCommentContent}</p> + </div> + ) : ( + <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> + )} + + <div className="mb-2 " /> + <div className="relative"> + <textarea + className={`h-40 w-full resize-none rounded-sm border-2 p-4 px-8 pr-40 text-lg`} + placeholder="질문을 입력해주세요" + value={questionInput} + onChange={(e) => setQuestionInput(e.target.value)} + /> + <button + className="absolute right-4 top-1/2 -translate-y-1/2" + onClick={handlePostQuestion} + > + <StatusToggleItem + color={isAbleToPost ? "green" : "gray"} + text="전송" + /> + </button> + </div> + </div> + ); +}; + +export default DashboardInput; diff --git a/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx b/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx new file mode 100644 index 00000000..65d02cf4 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx @@ -0,0 +1,27 @@ +import ChatList from "./ChatList"; +import { Answer } from "@/apis/dtos/question.dto"; + +interface ReplyChatProps extends Answer {} +const ReplyChat = ({ + commentId, + content, + writer, + time, + accessRight, +}: ReplyChatProps) => { + return ( + <div className="border bg-gray-10 p-4"> + <ChatList + writer={writer} + content={content} + time={time} + commentId={commentId} + accessRight={accessRight} + markdownStyle="!bg-inherit" + showReplyButton={false} + /> + </div> + ); +}; + +export default ReplyChat; diff --git a/FE/src/components/feature/detail/ProgramDetail.tsx b/FE/src/components/feature/detail/ProgramDetail.tsx new file mode 100644 index 00000000..1750c587 --- /dev/null +++ b/FE/src/components/feature/detail/ProgramDetail.tsx @@ -0,0 +1,35 @@ +"use client"; +import { useQueryClient } from "@tanstack/react-query"; +import ProgramAttendStatusManageSection from "../../programDetail/program/ProgramAttendStatusManageSection"; +import ProgramPresentations from "./ProgramPresentations"; +import { ProgramInfoDto } from "@/apis/dtos/program.dto"; +import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; +import { AccessType } from "@/types/access"; +import ProgramDashboardSection from "./Dashboard/ProgramDashboardSection"; + +interface ProgramDetailProps { + data: ProgramInfoDto; + programId: number; + accessType: AccessType; +} + +const ProgramDetail = ({ data, programId, accessType }: ProgramDetailProps) => { + const isAdmin = accessType === "admin"; + + const { content } = data; + + const queryClient = useQueryClient(); + const githubUrl = queryClient.getQueryData(["githubUrl", programId]); + + return ( + <div> + <MarkdownViewer value={content} /> + {isAdmin && <ProgramAttendStatusManageSection programId={programId} />} + {githubUrl && <ProgramPresentations programId={programId} />} + <div className="mt-12"> + <ProgramDashboardSection programId={programId} /> + </div> + </div> + ); +}; +export default ProgramDetail; diff --git a/FE/src/components/feature/detail/ProgramPresentations.tsx b/FE/src/components/feature/detail/ProgramPresentations.tsx new file mode 100644 index 00000000..e38a302c --- /dev/null +++ b/FE/src/components/feature/detail/ProgramPresentations.tsx @@ -0,0 +1,51 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import Title from "@/components/common/Title/Title"; +import usePresentations from "@/hooks/query/usePresentations"; + +interface ProgramPresentationsProps { + programId: number; +} +const ProgramPresentations = ({ programId }: ProgramPresentationsProps) => { + const { + data: presentations, + isLoading, + isError, + } = usePresentations(programId); + + if (isError) return null; + + if (isLoading) return null; + if (!presentations) return null; + if (presentations.length === 0) return null; + return ( + <section> + <Title text="발표자료 " /> + {/* TODO: 로더 적용하기 */} + {isLoading && <div>로딩중...</div>} + <div className="mx-auto mt-8 grid w-fit gap-x-40 gap-y-8 px-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> + {presentations && + presentations.map(({ download_url, name }) => ( + <Link + key={name} + className="flex gap-4" + href={download_url} + target="_blank" + > + <Image + src="/icons/folder.svg" + width={20} + height={20} + alt="folder" + /> + {name} + </Link> + ))} + </div> + </section> + ); +}; + +export default ProgramPresentations; diff --git a/FE/src/components/feature/detail/loader/ProgramDetail.skeleton.tsx b/FE/src/components/feature/detail/loader/ProgramDetail.skeleton.tsx new file mode 100644 index 00000000..b4b83986 --- /dev/null +++ b/FE/src/components/feature/detail/loader/ProgramDetail.skeleton.tsx @@ -0,0 +1,10 @@ +const ProgramDetailSkeleton = () => { + return ( + <div className="animate-pulse space-y-4 [&>*]:h-5"> + {Array.from({ length: 12 }).map((_, i) => ( + <div key={i} className="w-full rounded-lg bg-slate-200"></div> + ))} + </div> + ); +}; +export default ProgramDetailSkeleton; diff --git a/FE/src/components/feature/detail/loader/ProgramHeader.skeleton.tsx b/FE/src/components/feature/detail/loader/ProgramHeader.skeleton.tsx new file mode 100644 index 00000000..4cabbf04 --- /dev/null +++ b/FE/src/components/feature/detail/loader/ProgramHeader.skeleton.tsx @@ -0,0 +1,11 @@ +const ProgramHeaderSkeleton = () => { + return ( + <div className="animate-pulse space-y-4 border-b-2 py-4"> + <div className="h-[1.6rem] w-12 rounded-lg bg-slate-200"></div> + <div className="h-10 rounded-lg bg-slate-200"></div> + <div className="h-7 rounded-lg bg-slate-200"></div> + </div> + ); +}; + +export default ProgramHeaderSkeleton; diff --git a/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx b/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx new file mode 100644 index 00000000..ccd99556 --- /dev/null +++ b/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx @@ -0,0 +1,12 @@ +import ProgramDetailSkeleton from "./ProgramDetail.skeleton"; +import ProgramHeaderSkeleton from "./ProgramHeader.skeleton"; + +const ProgramInfoLoader = () => { + return ( + <div className="space-y-8"> + <ProgramHeaderSkeleton /> + <ProgramDetailSkeleton /> + </div> + ); +}; +export default ProgramInfoLoader; From bc860897c5ccce692a019e4c5cfea6ce3643d071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Wed, 27 Nov 2024 23:51:15 +0900 Subject: [PATCH 09/33] =?UTF-8?q?refactor:=20=EB=8D=94=EC=9D=B4=EC=83=81?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- .../program/DashboardCompound/Board/Board.tsx | 33 ---- .../program/DashboardCompound/Board/Chat.tsx | 35 ----- .../DashboardCompound/Board/ReplyChat.tsx | 27 ---- .../Board/common/ChatList.tsx | 143 ------------------ .../DashboardCompound/DashboardWrapper.tsx | 142 ----------------- .../program/DashboardCompound/Form.tsx | 83 ---------- .../program/DashboardCompound/TeamTab.tsx | 46 ------ .../program/ProgramDashboard.tsx | 24 --- .../program/ProgramDetail.skeleton.tsx | 10 -- .../programDetail/program/ProgramDetail.tsx | 35 ----- .../program/ProgramHeader.skeleton.tsx | 11 -- .../program/ProgramInfo.loader.tsx | 12 -- .../program/ProgramPresentations.tsx | 51 ------- 13 files changed, 652 deletions(-) delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/Board/Board.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/Board/Chat.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/Board/ReplyChat.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/Board/common/ChatList.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/DashboardWrapper.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/Form.tsx delete mode 100644 FE/src/components/programDetail/program/DashboardCompound/TeamTab.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramDashboard.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramDetail.skeleton.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramDetail.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramHeader.skeleton.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramInfo.loader.tsx delete mode 100644 FE/src/components/programDetail/program/ProgramPresentations.tsx diff --git a/FE/src/components/programDetail/program/DashboardCompound/Board/Board.tsx b/FE/src/components/programDetail/program/DashboardCompound/Board/Board.tsx deleted file mode 100644 index 232b5d56..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/Board/Board.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useContext } from "react"; -import { DashboardContext } from "../DashboardWrapper"; -import Chat from "./Chat"; -import { useGetQuestion } from "@/hooks/query/useQuestionQuery"; - -const Board = () => { - const { - teamValues: { selectedTeamId }, - programValue: { programId }, - } = useContext(DashboardContext); - const { data, isLoading, error } = useGetQuestion(programId, selectedTeamId); - - // TODO: Loader 적용, 에러 처리 - if (isLoading) return <div>Loading...</div>; - if (error) return <div>Error...</div>; - - const { comments } = data; - - return ( - <div className="flex max-h-[36rem] w-full flex-col overflow-hidden overflow-y-auto rounded-sm border"> - {comments.length === 0 && ( - <div className="flex h-full items-center justify-center py-20 text-xl text-gray-30"> - 아직 질문이 없습니다. 🥲 - </div> - )} - {comments.map((props) => ( - <Chat key={props.commentId} {...props} /> - ))} - </div> - ); -}; - -export default Board; diff --git a/FE/src/components/programDetail/program/DashboardCompound/Board/Chat.tsx b/FE/src/components/programDetail/program/DashboardCompound/Board/Chat.tsx deleted file mode 100644 index c8c3930f..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/Board/Chat.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import ChatList from "./common/ChatList"; -import ReplyChat from "./ReplyChat"; -import { Comment } from "@/apis/dtos/question.dto"; - -const Chat = ({ - commentId, - content, - writer, - time, - answers, - accessRight, -}: Comment) => { - return ( - <div className="border p-4"> - <ChatList - commentId={commentId} - writer={writer} - accessRight={accessRight} - time={time} - content={content} - /> - <div className="mt-8 px-14"> - {answers && ( - <> - {answers.map((props) => ( - <ReplyChat key={props.commentId} {...props} /> - ))} - </> - )} - </div> - </div> - ); -}; - -export default Chat; diff --git a/FE/src/components/programDetail/program/DashboardCompound/Board/ReplyChat.tsx b/FE/src/components/programDetail/program/DashboardCompound/Board/ReplyChat.tsx deleted file mode 100644 index 127f5a2a..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/Board/ReplyChat.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import ChatList from "./common/ChatList"; -import { Answer } from "@/apis/dtos/question.dto"; - -interface ReplyChatProps extends Answer {} -const ReplyChat = ({ - commentId, - content, - writer, - time, - accessRight, -}: ReplyChatProps) => { - return ( - <div className="border bg-gray-10 p-4"> - <ChatList - writer={writer} - content={content} - time={time} - commentId={commentId} - accessRight={accessRight} - markdownStyle="!bg-inherit" - showReplyButton={false} - /> - </div> - ); -}; - -export default ReplyChat; diff --git a/FE/src/components/programDetail/program/DashboardCompound/Board/common/ChatList.tsx b/FE/src/components/programDetail/program/DashboardCompound/Board/common/ChatList.tsx deleted file mode 100644 index ab84f030..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/Board/common/ChatList.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useContext, useState } from "react"; -import { DashboardContext } from "../../DashboardWrapper"; -import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; - -//TODO: headless 하게 변경해야함 -interface ChatListProps { - commentId: number; - writer: string; - accessRight: "edit" | "read_only"; - time: string; - content: string; - markdownStyle?: string; - showReplyButton?: boolean; -} -const ChatList = ({ - writer, - accessRight, - commentId, - content, - time, - markdownStyle, - showReplyButton = true, -}: ChatListProps) => { - const { - accessType, - commentValues: { - create: { setParentsCommentId, changeSelectedCommentContent }, - update: { updateComment, isUpdateSuccess }, - delete: { deleteComment, isDeleteSuccess }, - }, - } = useContext(DashboardContext); - - const [userInputToModify, setUserInputToModify] = useState(content); - const [isModify, setIsModify] = useState(false); - - const isGuest = accessType === "public"; - const isHasUpdateRight = accessRight === "edit" && !isGuest; - - const handleReply = () => { - setParentsCommentId(commentId); - changeSelectedCommentContent(content); - }; - - const toggleIsModify = () => { - if (!isHasUpdateRight) return; - setIsModify((prev) => !prev); - setUserInputToModify(content); - }; - - const handleUpdateComment = () => { - if (!isModify) return; - - const newContents = userInputToModify; - if (!newContents) return; - - if (content === newContents) { - setIsModify((prev) => !prev); - return; - } - - updateComment({ commentId, contents: newContents }); - isUpdateSuccess && setUserInputToModify(newContents); - - setIsModify((prev) => !prev); - }; - - const handleDeleteComment = () => { - if (!isHasUpdateRight) return; - - const isOkToDelete = confirm("정말 삭제하시겠습니까?"); - if (!isOkToDelete) return; - deleteComment(commentId); - isDeleteSuccess && setUserInputToModify(""); - }; - return ( - <> - <div className="relative my-4 h-2 w-fit translate-y-4 bg-emerald-300"> - <p className="w-fit -translate-y-4 text-lg font-semibold">{writer}</p> - </div> - <div> - {!isModify && ( - <MarkdownViewer value={content} className={markdownStyle} /> - )} - </div> - {isModify && ( - <textarea - className="mt-4 h-40 w-full rounded-sm border-2 p-4 text-lg" - value={userInputToModify} - onChange={(e) => setUserInputToModify(e.target.value)} - /> - )} - <div className="mt-4 flex items-center gap-4"> - <span className="opacity-60">{time}</span> - {!isModify && showReplyButton && ( - <> - {!isGuest && ( - <button - className="opacity-60 transition-all hover:opacity-100" - onClick={handleReply} - > - 답변하기 - </button> - )} - {isHasUpdateRight && ( - <button - className="opacity-60 transition-all hover:opacity-100" - onClick={toggleIsModify} - > - 수정하기 - </button> - )} - {isHasUpdateRight && ( - <button - className="opacity-60 transition-all hover:opacity-100" - onClick={handleDeleteComment} - > - 삭제하기 - </button> - )} - </> - )} - {isModify && ( - <> - <button - className="opacity-60 transition-all hover:opacity-100" - onClick={handleUpdateComment} - > - 수정완료 - </button> - <button - className="opacity-60 transition-all hover:opacity-100" - onClick={toggleIsModify} - > - 취소 - </button> - </> - )} - </div> - </> - ); -}; - -export default ChatList; diff --git a/FE/src/components/programDetail/program/DashboardCompound/DashboardWrapper.tsx b/FE/src/components/programDetail/program/DashboardCompound/DashboardWrapper.tsx deleted file mode 100644 index d567e309..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/DashboardWrapper.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { createContext, useState } from "react"; -import Board from "./Board/Board"; -import Input from "./Form"; -import TeamTab from "./TeamTab"; -import { - useDeleteQuestion, - useUpdateQuestion, -} from "@/hooks/query/useQuestionQuery"; -import { useTeamQuery } from "@/hooks/query/useTeamQuery"; -import { AccessType } from "@/types/access"; -import { TeamInfo } from "@/types/team"; - -interface DashboardContextValue { - accessType: AccessType; - programValue: { - readonly programId: number; - }; - teamValues: { - readonly teams: TeamInfo[]; - readonly isLoading: boolean; - readonly selectedTeamId: number; - readonly changeSelectedTeamId: (teamId: number) => void; - }; - commentValues: { - create: { - readonly parentsCommentId: number; - readonly setParentsCommentId: (id: number) => void; - readonly selectedCommentContent: string; - readonly changeSelectedCommentContent: (content: string) => void; - }; - update: { - readonly updateComment: (question: unknown) => void; - readonly isUpdateSuccess: boolean; - }; - delete: { - readonly deleteComment: (question: unknown) => void; - readonly isDeleteSuccess: boolean; - }; - }; - programId: number; - children: React.ReactNode; -} - -export const DashboardContext = createContext<DashboardContextValue>(null); - -interface DashboardWrapperProps { - programId: number; - accessType: AccessType; //TODO: 현재 accessType에 의존하는 중임. 이는 알지 않아도 되는 정보이므로 추후 수정 필요 - children: React.ReactNode; -} -const DashboardWrapper = ({ - programId, - accessType, - children, -}: DashboardWrapperProps) => { - // teamValues - const { data, isLoading } = useTeamQuery(programId); - const { teams } = data || { teams: [] }; - const [selectedTeamId, setSelectedTeamId] = useState<number>(); - const changeSelectedTeamId = (teamId: number) => { - setSelectedTeamId(teamId); - }; - - // comment - const [parentsCommentId, setParentsCommentId] = useState<number>(-1); - const [selectedCommentContent, setSelectedCommentContent] = - useState<string>(""); - const { mutate: updateComment, isSuccess: isUpdateSuccess } = - useUpdateQuestion(); - const { mutate: deleteComment, isSuccess: isDeleteSuccess } = - useDeleteQuestion(); - - const changeSelectedCommentContent = (content: string) => { - setSelectedCommentContent(content); - }; - - if (isLoading) return null; - if (!data || data.teams.length === 0) return null; - - const programValue = { - programId, - }; - - const teamValues = { - teams, - isLoading, - selectedTeamId, - changeSelectedTeamId, - }; - - const commentValues = { - create: { - parentsCommentId, - setParentsCommentId, - selectedCommentContent, - changeSelectedCommentContent, - }, - update: { - updateComment, - isUpdateSuccess, - }, - delete: { - deleteComment, - isDeleteSuccess, - }, - }; - - //TODO: useMemo 사용해야함 _ 급하게 임의로 수정함. 이는 의존성 문제 존재하므로 이부분 보기 - // const DashBoardValue = useMemo(() => { - // return { - // accessType, - // programValue, - // teamValues, - // commentValues, - // programId, - // children, - // }; - // }, [programValue, teamValues, commentValues, programId, children]); - - const DashBoardValue = { - accessType, - programValue, - teamValues, - commentValues, - programId, - children, - }; - - return ( - <DashboardContext.Provider value={DashBoardValue}> - {children} - </DashboardContext.Provider> - ); -}; - -DashboardWrapper.TeamTab = TeamTab; -DashboardWrapper.Board = Board; -DashboardWrapper.Input = Input; - -export default DashboardWrapper; diff --git a/FE/src/components/programDetail/program/DashboardCompound/Form.tsx b/FE/src/components/programDetail/program/DashboardCompound/Form.tsx deleted file mode 100644 index bed90370..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/Form.tsx +++ /dev/null @@ -1,83 +0,0 @@ -//TODO: 답변 취소 아이콘 추가하기 - -import { useContext, useState } from "react"; -import { DashboardContext } from "./DashboardWrapper"; -import { PostQuestionParams } from "@/apis/question"; -import StatusToggleItem from "@/components/common/StatusToggleItem"; -import { usePostQuestion } from "@/hooks/query/useQuestionQuery"; - -const Input = () => { - const { - accessType, - programValue: { programId }, - teamValues: { teams, selectedTeamId }, - commentValues: { - create: { parentsCommentId, setParentsCommentId, selectedCommentContent }, - }, - } = useContext(DashboardContext); - - const [questionInput, setQuestionInput] = useState<string>(""); - - const { mutate } = usePostQuestion(); - const isReply = parentsCommentId !== -1; - const selectedTeamName = teams?.find((team) => team.teamId === selectedTeamId) - ?.teamName; - - const isAbleToPost = accessType === "private"; - - const handlePostQuestion = () => { - const questionContent = questionInput.trim(); - - if (!questionContent) return; - if (!isAbleToPost) return; - - const postQuestionParams: PostQuestionParams = { - programId, - teamId: selectedTeamId, - questionContent, - parentsCommentId, - }; - mutate(postQuestionParams); - setQuestionInput(""); - setParentsCommentId(-1); - }; - - return ( - <div> - {/* <div className="absolute z-10 text-xl font-bold">{name}</div> */} - {isReply ? ( - <div className="truncate text-lg font-semibold"> - {/* <Image src={"/icons/x.svg"} alt="답글 종료" width={20} height={20} /> */} - <button className="px-2 " onClick={() => setParentsCommentId(-1)}> - x - </button> - <p className="inline text-xl font-bold">답변하기 :</p> - <p className="ml-2 inline opacity-50">{selectedCommentContent}</p> - </div> - ) : ( - <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> - )} - - <div className="mb-2 " /> - <div className="relative"> - <textarea - className={`h-40 w-full resize-none rounded-sm border-2 p-4 px-8 pr-40 text-lg`} - placeholder="질문을 입력해주세요" - value={questionInput} - onChange={(e) => setQuestionInput(e.target.value)} - /> - <button - className="absolute right-4 top-1/2 -translate-y-1/2" - onClick={handlePostQuestion} - > - <StatusToggleItem - color={isAbleToPost ? "green" : "gray"} - text="전송" - /> - </button> - </div> - </div> - ); -}; - -export default Input; diff --git a/FE/src/components/programDetail/program/DashboardCompound/TeamTab.tsx b/FE/src/components/programDetail/program/DashboardCompound/TeamTab.tsx deleted file mode 100644 index f59dae2e..00000000 --- a/FE/src/components/programDetail/program/DashboardCompound/TeamTab.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useContext, useEffect } from "react"; -import { DashboardContext } from "./DashboardWrapper"; -import Tab from "@/components/common/tabs/tab/Tab"; -import { TabOption } from "@/types/tab"; -import { TeamInfo } from "@/types/team"; - -const TeamTab = () => { - const { - teamValues: { teams, isLoading, selectedTeamId, changeSelectedTeamId }, - } = useContext(DashboardContext); - - useEffect(() => { - if (teams && teams.length > 0) { - changeSelectedTeamId(teams[0].teamId); - } - }, [teams]); - - const teamTabOptions: TabOption<number>[] = - teams?.map(({ teamId, teamName }: TeamInfo) => ({ - text: teamName, - type: teamId, - })) || []; - - const handleTeamSelect = (selected: number) => { - changeSelectedTeamId(selected); - }; - - return ( - <div> - {isLoading && <div>로딩중</div>} - {teams && teams.length > 0 && ( - <Tab<number> - selected={selectedTeamId} - baseColor="gray" - size="md" - pointColor="navy" - align="line" - onItemClick={handleTeamSelect} - options={teamTabOptions} - /> - )} - </div> - ); -}; - -export default TeamTab; diff --git a/FE/src/components/programDetail/program/ProgramDashboard.tsx b/FE/src/components/programDetail/program/ProgramDashboard.tsx deleted file mode 100644 index 205d9ea3..00000000 --- a/FE/src/components/programDetail/program/ProgramDashboard.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import DashboardWrapper from "./DashboardCompound/DashboardWrapper"; -import Title from "@/components/common/Title/Title"; -import { AccessType } from "@/types/access"; - -interface ProgramDashboardProps { - programId: number; - accessType: AccessType; -} -const ProgramDashboard = ({ programId, accessType }: ProgramDashboardProps) => { - return ( - <DashboardWrapper programId={programId} accessType={accessType}> - <Title text="질문 게시판" /> - <div className="mt-8 flex flex-col gap-8"> - <DashboardWrapper.TeamTab /> - <DashboardWrapper.Board /> - <DashboardWrapper.Input /> - </div> - </DashboardWrapper> - ); -}; - -export default ProgramDashboard; diff --git a/FE/src/components/programDetail/program/ProgramDetail.skeleton.tsx b/FE/src/components/programDetail/program/ProgramDetail.skeleton.tsx deleted file mode 100644 index b4b83986..00000000 --- a/FE/src/components/programDetail/program/ProgramDetail.skeleton.tsx +++ /dev/null @@ -1,10 +0,0 @@ -const ProgramDetailSkeleton = () => { - return ( - <div className="animate-pulse space-y-4 [&>*]:h-5"> - {Array.from({ length: 12 }).map((_, i) => ( - <div key={i} className="w-full rounded-lg bg-slate-200"></div> - ))} - </div> - ); -}; -export default ProgramDetailSkeleton; diff --git a/FE/src/components/programDetail/program/ProgramDetail.tsx b/FE/src/components/programDetail/program/ProgramDetail.tsx deleted file mode 100644 index 7ec29a4b..00000000 --- a/FE/src/components/programDetail/program/ProgramDetail.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; -import { useQueryClient } from "@tanstack/react-query"; -import ProgramAttendStatusManageSection from "./ProgramAttendStatusManageSection"; -import ProgramDashboard from "./ProgramDashboard"; -import ProgramPresentations from "./ProgramPresentations"; -import { ProgramInfoDto } from "@/apis/dtos/program.dto"; -import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; -import { AccessType } from "@/types/access"; - -interface ProgramDetailProps { - data: ProgramInfoDto; - programId: number; - accessType: AccessType; -} - -const ProgramDetail = ({ data, programId, accessType }: ProgramDetailProps) => { - const isAdmin = accessType === "admin"; - - const { content } = data; - - const queryClient = useQueryClient(); - const githubUrl = queryClient.getQueryData(["githubUrl", programId]); - - return ( - <div> - <MarkdownViewer value={content} /> - {isAdmin && <ProgramAttendStatusManageSection programId={programId} />} - {githubUrl && <ProgramPresentations programId={programId} />} - <div className="mt-12"> - <ProgramDashboard programId={programId} accessType={accessType} /> - </div> - </div> - ); -}; -export default ProgramDetail; diff --git a/FE/src/components/programDetail/program/ProgramHeader.skeleton.tsx b/FE/src/components/programDetail/program/ProgramHeader.skeleton.tsx deleted file mode 100644 index 4cabbf04..00000000 --- a/FE/src/components/programDetail/program/ProgramHeader.skeleton.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const ProgramHeaderSkeleton = () => { - return ( - <div className="animate-pulse space-y-4 border-b-2 py-4"> - <div className="h-[1.6rem] w-12 rounded-lg bg-slate-200"></div> - <div className="h-10 rounded-lg bg-slate-200"></div> - <div className="h-7 rounded-lg bg-slate-200"></div> - </div> - ); -}; - -export default ProgramHeaderSkeleton; diff --git a/FE/src/components/programDetail/program/ProgramInfo.loader.tsx b/FE/src/components/programDetail/program/ProgramInfo.loader.tsx deleted file mode 100644 index ccd99556..00000000 --- a/FE/src/components/programDetail/program/ProgramInfo.loader.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import ProgramDetailSkeleton from "./ProgramDetail.skeleton"; -import ProgramHeaderSkeleton from "./ProgramHeader.skeleton"; - -const ProgramInfoLoader = () => { - return ( - <div className="space-y-8"> - <ProgramHeaderSkeleton /> - <ProgramDetailSkeleton /> - </div> - ); -}; -export default ProgramInfoLoader; diff --git a/FE/src/components/programDetail/program/ProgramPresentations.tsx b/FE/src/components/programDetail/program/ProgramPresentations.tsx deleted file mode 100644 index e38a302c..00000000 --- a/FE/src/components/programDetail/program/ProgramPresentations.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import Image from "next/image"; -import Link from "next/link"; -import Title from "@/components/common/Title/Title"; -import usePresentations from "@/hooks/query/usePresentations"; - -interface ProgramPresentationsProps { - programId: number; -} -const ProgramPresentations = ({ programId }: ProgramPresentationsProps) => { - const { - data: presentations, - isLoading, - isError, - } = usePresentations(programId); - - if (isError) return null; - - if (isLoading) return null; - if (!presentations) return null; - if (presentations.length === 0) return null; - return ( - <section> - <Title text="발표자료 " /> - {/* TODO: 로더 적용하기 */} - {isLoading && <div>로딩중...</div>} - <div className="mx-auto mt-8 grid w-fit gap-x-40 gap-y-8 px-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> - {presentations && - presentations.map(({ download_url, name }) => ( - <Link - key={name} - className="flex gap-4" - href={download_url} - target="_blank" - > - <Image - src="/icons/folder.svg" - width={20} - height={20} - alt="folder" - /> - {name} - </Link> - ))} - </div> - </section> - ); -}; - -export default ProgramPresentations; From 566d1b29c98d700009c8697ef6e7c76e9840aa5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Wed, 27 Nov 2024 23:53:55 +0900 Subject: [PATCH 10/33] =?UTF-8?q?feat:=20dashboard=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dashoaord에서 상태값이 서로 의존하는 관계가 생겨서 이를 facc만으로는 해결하지 못하는 경우가 생김 이에 불가피하게 전역상태를 사용하게됨 다만, 다른 전역상태를 사용할 때 도메인이 헷갈리지 않도록 객체로 구조화 하여 사용. 추가적으로 해당 atom이 다른 도메인에서 사용되지 않도록 하는 장치에 대한 고민 필요 --- FE/package.json | 3 +- .../components/common/dashboard/ChatBox.tsx | 190 ++++++++++++++++++ .../feature/detail/ProgramDetail.tsx | 7 +- .../programDetail/program/ProgramInfo.tsx | 10 +- FE/src/hooks/query/useQuestionQuery.ts | 2 +- FE/src/hooks/useAccess.tsx | 13 ++ FE/src/store/dashboardAtoms.ts | 17 ++ 7 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 FE/src/components/common/dashboard/ChatBox.tsx create mode 100644 FE/src/hooks/useAccess.tsx create mode 100644 FE/src/store/dashboardAtoms.ts diff --git a/FE/package.json b/FE/package.json index 3cf090e9..1eab693f 100644 --- a/FE/package.json +++ b/FE/package.json @@ -64,5 +64,6 @@ "tailwindcss": "latest", "ts-node": "^10.9.2", "typescript": "latest" - } + }, + "packageManager": "pnpm@8.15.1+sha1.8adba2d20330c02d3856e18c4eb3819d1d3ca6aa" } diff --git a/FE/src/components/common/dashboard/ChatBox.tsx b/FE/src/components/common/dashboard/ChatBox.tsx new file mode 100644 index 00000000..ac709fa9 --- /dev/null +++ b/FE/src/components/common/dashboard/ChatBox.tsx @@ -0,0 +1,190 @@ +"use client"; +import { useState } from "react"; +import MarkdownViewer from "../markdown/MarkdownViewer"; +import { useGetAccessType } from "@/hooks/useAccess"; + +export interface ChatBoxInnerData { + commentId: number; + defaultContent: string; + time: string; + markdownStyle: string; + showReplyButton: boolean; + writer: string; + userInputToModify: string; + setUserInputToModify: (content: string) => void; + isGuest: boolean; + hasUpdateRight: boolean; + toggleIsModify: () => void; +} +export interface UpdateComment extends ChatBoxInnerData { + newContents: string; +} + +interface ChatBoxProps { + writer: string; + defaultContent: string; + time: string; + markdownStyle?: string; + showReplyButton?: boolean; + accessRight: "edit" | "read_only"; + updateComment: (question: UpdateComment) => void; + deleteComment: (question: ChatBoxInnerData) => void; + commentId: number; + handleReply: () => void; +} + +const ChatBox = ({ + writer, + defaultContent, + time, + markdownStyle, + showReplyButton, + accessRight, + updateComment, + commentId, //TODO: 필요 없는지 확인 필요 + deleteComment, + handleReply, +}: ChatBoxProps) => { + const [userInputToModify, setUserInputToModify] = useState(defaultContent); + const [isModifyMode, setIsModify] = useState(false); + + const accessType = useGetAccessType(); + + const isGuest = accessType === "public"; + const hasUpdateRight = accessRight === "edit" && !isGuest; + + const toggleIsModify = () => { + if (!hasUpdateRight) return; + setIsModify((prev) => !prev); + setUserInputToModify(defaultContent); + }; + + // const handleReply = () => { + // setParentsCommentId(commentId); + // changeSelectedCommentContent(content); + // }; + + const handleUpdateComment = () => { + if (!isModifyMode) return; + + const newContents = userInputToModify; + if (!newContents) return; + + if (defaultContent === newContents) { + setIsModify((prev) => !prev); + return; + } + + // updateComment({ commentId, contents: newContents }); + // isUpdateSuccess && setUserInputToModify(newContents); + + updateComment({ + commentId, + defaultContent, + hasUpdateRight, + isGuest, + markdownStyle, + showReplyButton, + time, + writer, + toggleIsModify, + newContents, + userInputToModify, + setUserInputToModify, + }); + + setIsModify((prev) => !prev); + }; + + const handleDeleteComment = () => { + if (!hasUpdateRight) return; + + const isOkToDelete = confirm("정말 삭제하시겠습니까?"); + if (!isOkToDelete) return; + + deleteComment({ + commentId, + defaultContent, + time, + markdownStyle, + showReplyButton, + writer, + userInputToModify, + setUserInputToModify, + isGuest, + hasUpdateRight, + toggleIsModify, + }); + // deleteComment({ commentId }); + // isDeleteSuccess && setUserInputToModify(""); + }; + + return ( + <> + <div className="relative my-4 h-2 w-fit translate-y-4 bg-emerald-300"> + <p className="w-fit -translate-y-4 text-lg font-semibold">{writer}</p> + </div> + <div> + {!isModifyMode && ( + <MarkdownViewer value={defaultContent} className={markdownStyle} /> + )} + </div> + {isModifyMode && ( + <textarea + className="mt-4 h-40 w-full rounded-sm border-2 p-4 text-lg" + value={userInputToModify} + onChange={(e) => setUserInputToModify(e.target.value)} + /> + )} + <div className="mt-4 flex items-center gap-4"> + <span className="opacity-60">{time}</span> + {!isModifyMode && showReplyButton && ( + <> + {!isGuest && ( + <button + className="opacity-60 transition-all hover:opacity-100" + onClick={handleReply} + > + 답변하기 + </button> + )} + {hasUpdateRight && ( + <button + className="opacity-60 transition-all hover:opacity-100" + onClick={toggleIsModify} + > + 수정하기 + </button> + )} + {hasUpdateRight && ( + <button + className="opacity-60 transition-all hover:opacity-100" + onClick={handleDeleteComment} + > + 삭제하기 + </button> + )} + </> + )} + {isModifyMode && ( + <> + <button + className="opacity-60 transition-all hover:opacity-100" + onClick={handleUpdateComment} + > + 수정완료 + </button> + <button + className="opacity-60 transition-all hover:opacity-100" + onClick={toggleIsModify} + > + 취소 + </button> + </> + )} + </div> + </> + ); +}; + +export default ChatBox; diff --git a/FE/src/components/feature/detail/ProgramDetail.tsx b/FE/src/components/feature/detail/ProgramDetail.tsx index 1750c587..28b1794b 100644 --- a/FE/src/components/feature/detail/ProgramDetail.tsx +++ b/FE/src/components/feature/detail/ProgramDetail.tsx @@ -1,19 +1,20 @@ "use client"; + import { useQueryClient } from "@tanstack/react-query"; import ProgramAttendStatusManageSection from "../../programDetail/program/ProgramAttendStatusManageSection"; import ProgramPresentations from "./ProgramPresentations"; import { ProgramInfoDto } from "@/apis/dtos/program.dto"; import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; -import { AccessType } from "@/types/access"; import ProgramDashboardSection from "./Dashboard/ProgramDashboardSection"; +import { useGetAccessType } from "@/hooks/useAccess"; interface ProgramDetailProps { data: ProgramInfoDto; programId: number; - accessType: AccessType; } -const ProgramDetail = ({ data, programId, accessType }: ProgramDetailProps) => { +const ProgramDetail = ({ data, programId }: ProgramDetailProps) => { + const accessType = useGetAccessType(); const isAdmin = accessType === "admin"; const { content } = data; diff --git a/FE/src/components/programDetail/program/ProgramInfo.tsx b/FE/src/components/programDetail/program/ProgramInfo.tsx index 319b411f..3c5714bc 100644 --- a/FE/src/components/programDetail/program/ProgramInfo.tsx +++ b/FE/src/components/programDetail/program/ProgramInfo.tsx @@ -1,8 +1,8 @@ "use client"; -import ProgramDetail from "./ProgramDetail"; +import ProgramDetail from "../../feature/detail/ProgramDetail"; import ProgramHeader from "./ProgramHeader"; -import ProgramInfoLoader from "./ProgramInfo.loader"; +import ProgramInfoLoader from "../../feature/detail/loader/ProgramInfo.loader"; import { useGetProgramByProgramId } from "@/hooks/query/useProgramQuery"; import { AccessType } from "@/types/access"; @@ -25,11 +25,7 @@ const ProgramInfo = ({ programId, accessType }: ProgramInfoProps) => { return ( <section className="space-y-8"> <ProgramHeader data={programData} /> - <ProgramDetail - data={programData} - programId={programId} - accessType={accessType} - /> + <ProgramDetail data={programData} programId={programId} /> </section> ); }; diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index 918eb72f..d4dfd230 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -7,7 +7,7 @@ import { updateQuestion, } from "@/apis/question"; -export const useGetQuestion = (programId: number, teamId: number) => { +export const useGetQuestions = (programId: number, teamId: number) => { return useQuery({ queryKey: ["question", programId, teamId], queryFn: () => getQuestionsByTeam(programId, teamId), diff --git a/FE/src/hooks/useAccess.tsx b/FE/src/hooks/useAccess.tsx new file mode 100644 index 00000000..2992be4f --- /dev/null +++ b/FE/src/hooks/useAccess.tsx @@ -0,0 +1,13 @@ +import { AccessType } from "@/types/access"; +import { usePathname } from "next/navigation"; + +export const useGetAccessType = (): AccessType => { + const pathname = usePathname(); + if (pathname.includes("admin")) { + return "admin"; + } + if (pathname.includes("guest")) { + return "public"; + } + return "private"; +}; diff --git a/FE/src/store/dashboardAtoms.ts b/FE/src/store/dashboardAtoms.ts new file mode 100644 index 00000000..51938de8 --- /dev/null +++ b/FE/src/store/dashboardAtoms.ts @@ -0,0 +1,17 @@ +import { atom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; + +/** + * 선택된 질문의 id + * -1이면 선택된 질문이 없음 + */ +const selectedCommentId = atom(-1); +const selectedCommentContent = atom(""); +const questionInput = atomWithStorage("questionInput", ""); + +const dashboardAtoms = { + selectedCommentId, + selectedCommentContent, +}; + +export default dashboardAtoms; From c44fbb4f3e14b0dafab3a9a4a596a436bcca0e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 14:21:32 +0900 Subject: [PATCH 11/33] =?UTF-8?q?feat:=20=EC=9D=B5=EB=AA=85=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- FE/src/apis/question.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/FE/src/apis/question.ts b/FE/src/apis/question.ts index af74d3dd..743bfa1e 100644 --- a/FE/src/apis/question.ts +++ b/FE/src/apis/question.ts @@ -11,22 +11,34 @@ export const getQuestionsByTeam = async (programId: number, teamId: number) => { return new QuestionListDto(data?.data); }; +/** + * 질문을 등록합니다. + * - isCheck : 질문을 등록할 때 체크박스를 체크했는지 여부. 체크가 되었다면 1, 아니라면 0 + */ export interface PostQuestionParams { programId: number; teamId: number; questionContent: string; parentsCommentId?: number; + isChecked: 0 | 1; } export const postQuestion = async ({ programId, teamId, questionContent, parentsCommentId = -1, + isChecked, }: PostQuestionParams) => { return await https({ url: API.QUESTION.CREATE, method: "POST", - data: { programId, teamId, content: questionContent, parentsCommentId }, + data: { + programId, + teamId, + content: questionContent, + parentsCommentId, + isChecked, + }, }); }; From 93f78059cb6b20ab7e5cc8114624ff7cfc935235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 14:22:53 +0900 Subject: [PATCH 12/33] =?UTF-8?q?refactor:=20CheckBox=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=A1=B0=EA=B8=88=20=EB=8D=94=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=83=81=EC=9C=84=EC=97=90=EC=84=9C=20classname=EC=9D=84=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- FE/src/components/common/CheckBox/CheckBox.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/FE/src/components/common/CheckBox/CheckBox.tsx b/FE/src/components/common/CheckBox/CheckBox.tsx index 2da6b92a..31311bd5 100644 --- a/FE/src/components/common/CheckBox/CheckBox.tsx +++ b/FE/src/components/common/CheckBox/CheckBox.tsx @@ -5,15 +5,22 @@ interface CheckBoxProps { checked: boolean; onClick: () => void; disabled?: boolean; + className: string; } -const CheckBox = ({ checked, onClick, disabled = false }: CheckBoxProps) => { +const CheckBox = ({ + checked, + onClick, + disabled = false, + className, +}: CheckBoxProps) => { const checkboxClass = classNames( "flex h-6 w-6 items-center justify-center rounded border-2 transition duration-100", checked ? "border-blue-500 bg-blue-500" : "border-gray-20 bg-background", { "cursor-not-allowed opacity-0": disabled, }, + className, ); const handleCheckBoxClick = () => { From 0ad92e51d0d19f222363e1495ebafa7ce4ca4cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 14:23:44 +0900 Subject: [PATCH 13/33] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=9E=85=EB=A0=A5=ED=95=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EB=A1=9C=EC=BB=AC=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=9E=84=EC=8B=9C=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- .../Dashboard/ProgramDashboardSection.tsx | 1 - .../Dashboard/components/DashboardInput.tsx | 24 ++++++++++++++++--- FE/src/store/dashboardAtoms.ts | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx index b92179ee..198a882c 100644 --- a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -40,7 +40,6 @@ const ProgramDashboardSection = ({ <Tab.Content<string>> {({ selectedItem }) => ( <div className="mt-8 flex flex-col gap-8"> - {/* Board */} <DashboardContent programId={programId} selectedTeamId={ diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index f7689c4c..26927a9e 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -1,6 +1,7 @@ "use client"; import { PostQuestionParams } from "@/apis/question"; +import CheckBox from "@/components/common/CheckBox/CheckBox"; import StatusToggleItem from "@/components/common/StatusToggleItem"; import { usePostQuestion } from "@/hooks/query/useQuestionQuery"; import { useGetAccessType } from "@/hooks/useAccess"; @@ -21,7 +22,10 @@ const DashboardInput = ({ selectedTeamId, teams, }: DashboardInputProps) => { - const [questionInput, setQuestionInput] = useState<string>(""); + const [isChecked, setIsChecked] = useState<0 | 1>(0); + const [questionInput, setQuestionInput] = useAtom( + dashboardAtoms.questionInput, + ); const [selectedCommentId, setSelectedCommentId] = useAtom( dashboardAtoms.selectedCommentId, ); @@ -49,7 +53,9 @@ const DashboardInput = ({ teamId: selectedTeamId, questionContent, parentsCommentId: selectedCommentId, + isChecked, }; + mutate(postQuestionParams); setQuestionInput(""); setSelectedCommentId(-1); @@ -75,9 +81,21 @@ const DashboardInput = ({ <p className="ml-2 inline opacity-50">{selectedCommentContent}</p> </div> ) : ( - <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> + <div className="flex items-center justify-between gap-4"> + <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> + <label + className="flex select-none items-center justify-end gap-2 text-lg" + onClick={() => setIsChecked((prev) => (prev === 0 ? 1 : 0))} + > + <CheckBox + checked={isChecked === 1} + onClick={() => {}} + className="h-5 w-5" + /> + 익명으로 질문하기 + </label> + </div> )} - <div className="mb-2 " /> <div className="relative"> <textarea diff --git a/FE/src/store/dashboardAtoms.ts b/FE/src/store/dashboardAtoms.ts index 51938de8..7e99c652 100644 --- a/FE/src/store/dashboardAtoms.ts +++ b/FE/src/store/dashboardAtoms.ts @@ -12,6 +12,7 @@ const questionInput = atomWithStorage("questionInput", ""); const dashboardAtoms = { selectedCommentId, selectedCommentContent, + questionInput, }; export default dashboardAtoms; From a79c958bfbb3c73ddbcd3dd66b080870dd57dcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 15:09:33 +0900 Subject: [PATCH 14/33] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=BF=BC=EB=A6=AC=EC=97=90=20=EB=82=99=EA=B4=80?= =?UTF-8?q?=EC=A0=81=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/apis/question.ts | 6 +-- .../Dashboard/ProgramDashboardSection.tsx | 1 + .../Dashboard/components/DashboardInput.tsx | 14 +++--- FE/src/hooks/query/useQuestionQuery.ts | 50 +++++++++++++++++-- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/FE/src/apis/question.ts b/FE/src/apis/question.ts index 743bfa1e..4f803e5b 100644 --- a/FE/src/apis/question.ts +++ b/FE/src/apis/question.ts @@ -20,14 +20,14 @@ export interface PostQuestionParams { teamId: number; questionContent: string; parentsCommentId?: number; - isChecked: 0 | 1; + isAnonymous: 0 | 1; } export const postQuestion = async ({ programId, teamId, questionContent, parentsCommentId = -1, - isChecked, + isAnonymous = 0, }: PostQuestionParams) => { return await https({ url: API.QUESTION.CREATE, @@ -37,7 +37,7 @@ export const postQuestion = async ({ teamId, content: questionContent, parentsCommentId, - isChecked, + isAnonymous, }, }); }; diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx index 198a882c..9bf61822 100644 --- a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -24,6 +24,7 @@ const ProgramDashboardSection = ({ return ( <section> <Title text="질문 게시판" /> + <div className="mt-4" /> <Tab<string> align="line" defaultSelected={`${teams[0].teamName}`} diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index 26927a9e..dab53a75 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -7,7 +7,7 @@ import { usePostQuestion } from "@/hooks/query/useQuestionQuery"; import { useGetAccessType } from "@/hooks/useAccess"; import dashboardAtoms from "@/store/dashboardAtoms"; import { TeamInfo } from "@/types/team"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom } from "jotai"; import { useState } from "react"; interface DashboardInputProps { @@ -22,7 +22,7 @@ const DashboardInput = ({ selectedTeamId, teams, }: DashboardInputProps) => { - const [isChecked, setIsChecked] = useState<0 | 1>(0); + const [isAnonymous, setIsAnonymous] = useState<0 | 1>(0); const [questionInput, setQuestionInput] = useAtom( dashboardAtoms.questionInput, ); @@ -33,7 +33,7 @@ const DashboardInput = ({ dashboardAtoms.selectedCommentContent, ); - const { mutate } = usePostQuestion(); + const { mutate: postQuestion } = usePostQuestion(); const isReply = selectedCommentId !== -1; const selectedTeamName = teams?.find((team) => team.teamId === selectedTeamId) ?.teamName; @@ -53,10 +53,10 @@ const DashboardInput = ({ teamId: selectedTeamId, questionContent, parentsCommentId: selectedCommentId, - isChecked, + isAnonymous, }; - mutate(postQuestionParams); + postQuestion(postQuestionParams); setQuestionInput(""); setSelectedCommentId(-1); setSelectedCommentContent(""); @@ -85,10 +85,10 @@ const DashboardInput = ({ <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> <label className="flex select-none items-center justify-end gap-2 text-lg" - onClick={() => setIsChecked((prev) => (prev === 0 ? 1 : 0))} + onClick={() => setIsAnonymous((prev) => (prev === 0 ? 1 : 0))} > <CheckBox - checked={isChecked === 1} + checked={isAnonymous === 1} onClick={() => {}} className="h-5 w-5" /> diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index d4dfd230..3382f2ae 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -6,6 +6,9 @@ import { PostQuestionParams, updateQuestion, } from "@/apis/question"; +import { Comment, QuestionListDto } from "@/apis/dtos/question.dto"; +import API from "@/constants/API"; +import { UserAttendStatusInfoDto } from "@/apis/dtos/user.dto"; export const useGetQuestions = (programId: number, teamId: number) => { return useQuery({ @@ -20,14 +23,51 @@ export const useGetQuestions = (programId: number, teamId: number) => { export const usePostQuestion = () => { const queryClient = useQueryClient(); return useMutation({ - mutationKey: ["question", "post"], mutationFn: async (postQuestionParams: PostQuestionParams) => { - const res = await postQuestion(postQuestionParams); + return await postQuestion(postQuestionParams); + }, + onMutate: ({ + isAnonymous, + programId, + teamId, + questionContent, + }: PostQuestionParams) => { + queryClient.cancelQueries(["question", programId, teamId]); - const { programId, teamId } = postQuestionParams; - queryClient.invalidateQueries(["question", programId, teamId]); + const oldData = queryClient.getQueryData<QuestionListDto>([ + "question", + programId, + teamId, + ]); + if (!oldData) return; - return res; + const { name: userName } = + queryClient.getQueryData<UserAttendStatusInfoDto>([ + API.USER.ATTEND_STATUS(programId), + ]); + alert(userName); + + const newComment: Comment = { + commentId: oldData.comments.length + 1, + teamId, + writer: isAnonymous ? "익명" : userName, + accessRight: "edit", + time: "방금전", + content: questionContent, + answers: [], + }; + + queryClient.setQueryData<QuestionListDto>( + ["question", programId, teamId], + { + comments: [...oldData.comments, newComment], + }, + ); + + return oldData; + }, + onError: (_, { programId, teamId }, oldData) => { + queryClient.setQueriesData(["question", programId, teamId], oldData); }, }); }; From f7570276abcc45abfc3d0d56b9c567f8c6ffc10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 15:17:10 +0900 Subject: [PATCH 15/33] =?UTF-8?q?fix:=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=ED=83=80=EC=9E=85=20=EC=A7=80=EC=A0=95=EC=9D=B4=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8D=98=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95(CheckBox.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상위에서 디자인을 수정할 수 있도록 className을 props에 추가 다만, 이는 이전에 다른 곳에서 사용하던 부분이 존재하며, 하위호환성을 위해서 옵셔널로 지정 #141 --- FE/src/components/common/CheckBox/CheckBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/components/common/CheckBox/CheckBox.tsx b/FE/src/components/common/CheckBox/CheckBox.tsx index 31311bd5..415c9d28 100644 --- a/FE/src/components/common/CheckBox/CheckBox.tsx +++ b/FE/src/components/common/CheckBox/CheckBox.tsx @@ -5,7 +5,7 @@ interface CheckBoxProps { checked: boolean; onClick: () => void; disabled?: boolean; - className: string; + className?: string; } const CheckBox = ({ From 2e06eac9ee4cd8237a266207cb6dd87b2937069b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 15:18:59 +0900 Subject: [PATCH 16/33] =?UTF-8?q?fix:=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=A3=BC=EC=84=9D=20=EC=84=A4=EB=AA=85=EC=9D=B4=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8D=98=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95(question.ts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api가 변경되기 전의 설명이 들어있던 부분 수정 #141 --- FE/src/apis/question.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/apis/question.ts b/FE/src/apis/question.ts index 4f803e5b..6617ba6f 100644 --- a/FE/src/apis/question.ts +++ b/FE/src/apis/question.ts @@ -13,7 +13,7 @@ export const getQuestionsByTeam = async (programId: number, teamId: number) => { /** * 질문을 등록합니다. - * - isCheck : 질문을 등록할 때 체크박스를 체크했는지 여부. 체크가 되었다면 1, 아니라면 0 + * - isAnonymous : 질문을 등록할 때 체크박스를 체크했는지 여부. 체크가 되었다면 1, 아니라면 0 */ export interface PostQuestionParams { programId: number; From 46d9f5313cca31d5637a5cc6a9b75c53f6686447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 15:21:46 +0900 Subject: [PATCH 17/33] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0(DachboardContentChatBox.tsx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- .../components/DashboardContentChatBox.tsx | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx deleted file mode 100644 index b8ce3ef4..00000000 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardContentChatBox.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Comment } from "@/apis/dtos/question.dto"; -import ChatList from "../../DashboardCompound/Board/common/ChatList"; -import ReplyChat from "../../DashboardCompound/Board/ReplyChat"; - -const DashboardContentChatBox = ({ - commentId, - content, - writer, - time, - answers, - accessRight, -}: Comment) => { - return ( - <div className="border p-4"> - <ChatList - commentId={commentId} - writer={writer} - accessRight={accessRight} - time={time} - content={content} - /> - <div className="mt-8 px-14"> - {answers && ( - <> - {answers.map((props) => ( - <ReplyChat key={props.commentId} {...props} /> - ))} - </> - )} - </div> - </div> - ); -}; - -export default DashboardContentChatBox; From b8951206797cc7f5b53e23adbae9ed804e9459ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 15:56:35 +0900 Subject: [PATCH 18/33] =?UTF-8?q?fix:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=ED=94=8C=EB=A1=9C=EC=9A=B0=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=ED=95=98=EA=B8=B0=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B5=EB=AA=85=EC=9D=B4=20=ED=92=80=EB=A6=AC?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- .../feature/detail/Dashboard/components/ChatList.tsx | 2 ++ .../detail/Dashboard/components/DashboardContent.tsx | 3 --- .../detail/Dashboard/components/DashboardInput.tsx | 2 +- FE/src/hooks/query/useQuestionQuery.ts | 8 ++++---- FE/src/store/dashboardAtoms.ts | 3 +++ 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx index a5359333..23dbe373 100644 --- a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx @@ -31,6 +31,7 @@ const ChatList = ({ const setSelectedCommentContent = useSetAtom( dashboardAtoms.selectedCommentContent, ); + const setIsAnonymous = useSetAtom(dashboardAtoms.isAnonymous); const { mutate: updateComment, isSuccess: isUpdateSuccess } = useUpdateQuestion(); @@ -40,6 +41,7 @@ const ChatList = ({ const handleReply = () => { setSelectedCommentId(commentId); setSelectedCommentContent(content); + setIsAnonymous(0); }; const handleUpdateComment = ({ diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx index 48853f32..f855942e 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardContent.tsx @@ -11,9 +11,6 @@ const DashboardContent = ({ programId, selectedTeamId, }: DashboardContentProps) => { - console.log("programId", programId); - console.log("selectedTeamId", selectedTeamId); - const { data, isLoading, error } = useGetQuestions(programId, selectedTeamId); // TODO: Loader 적용, 에러 처리 diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index dab53a75..e1c201bd 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -22,7 +22,7 @@ const DashboardInput = ({ selectedTeamId, teams, }: DashboardInputProps) => { - const [isAnonymous, setIsAnonymous] = useState<0 | 1>(0); + const [isAnonymous, setIsAnonymous] = useAtom(dashboardAtoms.isAnonymous); const [questionInput, setQuestionInput] = useAtom( dashboardAtoms.questionInput, ); diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index 3382f2ae..e694c770 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -23,9 +23,8 @@ export const useGetQuestions = (programId: number, teamId: number) => { export const usePostQuestion = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (postQuestionParams: PostQuestionParams) => { - return await postQuestion(postQuestionParams); - }, + mutationFn: async (postQuestionParams: PostQuestionParams) => + await postQuestion(postQuestionParams), onMutate: ({ isAnonymous, programId, @@ -45,7 +44,6 @@ export const usePostQuestion = () => { queryClient.getQueryData<UserAttendStatusInfoDto>([ API.USER.ATTEND_STATUS(programId), ]); - alert(userName); const newComment: Comment = { commentId: oldData.comments.length + 1, @@ -57,6 +55,8 @@ export const usePostQuestion = () => { answers: [], }; + const newComments = [...oldData.comments, newComment]; + queryClient.setQueryData<QuestionListDto>( ["question", programId, teamId], { diff --git a/FE/src/store/dashboardAtoms.ts b/FE/src/store/dashboardAtoms.ts index 7e99c652..013a66cf 100644 --- a/FE/src/store/dashboardAtoms.ts +++ b/FE/src/store/dashboardAtoms.ts @@ -2,17 +2,20 @@ import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; /** + * selectedCommentId * 선택된 질문의 id * -1이면 선택된 질문이 없음 */ const selectedCommentId = atom(-1); const selectedCommentContent = atom(""); const questionInput = atomWithStorage("questionInput", ""); +const isAnonymous = atom<0 | 1>(0); const dashboardAtoms = { selectedCommentId, selectedCommentContent, questionInput, + isAnonymous, }; export default dashboardAtoms; From cea45bc605408f0687e6b665fd1e9587cf8949a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 16:18:44 +0900 Subject: [PATCH 19/33] =?UTF-8?q?feat:=20=EB=8B=B5=EB=B3=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=82=99=EA=B4=80=EC=A0=81=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 답변의 경우 부모 id와 동일한 id를 가지는 객체의 answer에 넣어주도록 수정 넣어주는 로직은 util함수로서 만듦 #141 --- FE/src/hooks/query/useQuestionQuery.ts | 12 ++++++---- FE/src/utils/question.ts | 33 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 FE/src/utils/question.ts diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index e694c770..288982d9 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -9,6 +9,7 @@ import { import { Comment, QuestionListDto } from "@/apis/dtos/question.dto"; import API from "@/constants/API"; import { UserAttendStatusInfoDto } from "@/apis/dtos/user.dto"; +import { makeNewQuestionData } from "@/utils/question"; export const useGetQuestions = (programId: number, teamId: number) => { return useQuery({ @@ -30,6 +31,7 @@ export const usePostQuestion = () => { programId, teamId, questionContent, + parentsCommentId, }: PostQuestionParams) => { queryClient.cancelQueries(["question", programId, teamId]); @@ -55,13 +57,15 @@ export const usePostQuestion = () => { answers: [], }; - const newComments = [...oldData.comments, newComment]; + const newComments = makeNewQuestionData( + oldData, + newComment, + parentsCommentId, + ); queryClient.setQueryData<QuestionListDto>( ["question", programId, teamId], - { - comments: [...oldData.comments, newComment], - }, + newComments, ); return oldData; diff --git a/FE/src/utils/question.ts b/FE/src/utils/question.ts new file mode 100644 index 00000000..672561b5 --- /dev/null +++ b/FE/src/utils/question.ts @@ -0,0 +1,33 @@ +import type { Comment, QuestionListDto } from "@/apis/dtos/question.dto"; + +export const makeNewQuestionData = ( + prevData: QuestionListDto, + newComment: Comment, + newCommentParentId: number, +): QuestionListDto => { + let result: QuestionListDto; + + // 일반 질문인 경우 + if (newCommentParentId === -1) { + const newComments = [...prevData.comments, newComment]; + result = { + comments: newComments, + }; + } + + const newComments = prevData.comments.map((comment) => { + if (comment.commentId === newCommentParentId) { + return { + ...comment, + answers: [...comment.answers, newComment], + }; + } + return comment; + }); + + result = { + comments: newComments, + }; + + return result; +}; From 1ca1f6c38da6e106375713a9cef055da69e448a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Thu, 28 Nov 2024 17:27:20 +0900 Subject: [PATCH 20/33] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #141 --- FE/src/apis/__test__/question.test.ts | 30 +++++++++++++++++++++++++- FE/src/hooks/query/useQuestionQuery.ts | 15 ++++++++++--- FE/src/utils/question.ts | 2 ++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/FE/src/apis/__test__/question.test.ts b/FE/src/apis/__test__/question.test.ts index b03be2c5..248fc55a 100644 --- a/FE/src/apis/__test__/question.test.ts +++ b/FE/src/apis/__test__/question.test.ts @@ -56,9 +56,10 @@ describe("postQuestion", () => { const programId = 1; const teamId = 1; const questionContent = "질문 내용"; + const isAnonymous = 0; // act - await postQuestion({ programId, teamId, questionContent }); + await postQuestion({ programId, teamId, questionContent, isAnonymous }); // assert expect(mockHttps).toHaveBeenCalledWith({ @@ -67,6 +68,30 @@ describe("postQuestion", () => { data: { programId, teamId, + isAnonymous: 0, + content: questionContent, + parentsCommentId: -1, + }, + }); + }); + it("익명 질문을 등록한다", async () => { + // arrange + const programId = 1; + const teamId = 1; + const questionContent = "질문 내용"; + const isAnonymous = 1; + + // act + await postQuestion({ programId, teamId, questionContent, isAnonymous }); + + // assert + expect(mockHttps).toHaveBeenCalledWith({ + url: "comments", + method: "POST", + data: { + programId, + teamId, + isAnonymous: 1, content: questionContent, parentsCommentId: -1, }, @@ -78,6 +103,7 @@ describe("postQuestion", () => { const teamId = 1; const questionContent = "답변 내용"; const parentsCommentId = 1; + const isAnonymous = 0; // act await postQuestion({ @@ -85,6 +111,7 @@ describe("postQuestion", () => { teamId, questionContent, parentsCommentId, + isAnonymous, }); // assert @@ -94,6 +121,7 @@ describe("postQuestion", () => { data: { programId, teamId, + isAnonymous: 0, content: questionContent, parentsCommentId, }, diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index 288982d9..43e6719d 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -39,8 +39,7 @@ export const usePostQuestion = () => { "question", programId, teamId, - ]); - if (!oldData) return; + ]) || { comments: [] }; const { name: userName } = queryClient.getQueryData<UserAttendStatusInfoDto>([ @@ -48,7 +47,7 @@ export const usePostQuestion = () => { ]); const newComment: Comment = { - commentId: oldData.comments.length + 1, + commentId: new Date().getTime(), teamId, writer: isAnonymous ? "익명" : userName, accessRight: "edit", @@ -68,6 +67,16 @@ export const usePostQuestion = () => { newComments, ); + // console.log(newComments); + + // console.log( + // queryClient.getQueryData<QuestionListDto>([ + // "question", + // programId, + // teamId, + // ]) || { comments: [] }, + // ); + return oldData; }, onError: (_, { programId, teamId }, oldData) => { diff --git a/FE/src/utils/question.ts b/FE/src/utils/question.ts index 672561b5..5035f271 100644 --- a/FE/src/utils/question.ts +++ b/FE/src/utils/question.ts @@ -13,8 +13,10 @@ export const makeNewQuestionData = ( result = { comments: newComments, }; + return result; } + // 답변인 경우 const newComments = prevData.comments.map((comment) => { if (comment.commentId === newCommentParentId) { return { From e2367939c5eada301a6d76a7e97dbdbc53cf27af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Fri, 29 Nov 2024 22:31:20 +0900 Subject: [PATCH 21/33] =?UTF-8?q?refactor(detail-refactor):=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=8E=8A=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EC=A7=80=EB=8A=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명세에 맞추어 page는 여러 독립적인 section으로 이루어지도록 구성하기 위해서 불필요하게 section 두개를 묶은 부분(ProgramHeader, ProgramDetail)을 따로 독립적인 section으로 수정 - 분리함에 따라서 기존에 두 컴포넌트를 묶어서 로더를 적용하는 부분 분리 --- .../(admin)/admin/detail/[programId]/page.tsx | 10 ++-- .../(guest)/guest/detail/[programId]/page.tsx | 12 +++-- .../(program)/detail/[programId]/page.tsx | 12 +++-- .../feature/detail/ProgramDetail.tsx | 36 ------------- .../attendee/AttendeeInfo.container.tsx | 0 .../detail}/attendee/AttendeeInfo.loader.tsx | 0 .../detail}/attendee/AttendeeInfo.tsx | 0 .../detail}/attendee/AttendeeStatus.tsx | 0 .../detail}/attendee/BluredAttedee.tsx | 0 .../detail/loader/ProgramInfo.loader.tsx | 12 ----- .../detail}/program/EditAndDeleteButton.tsx | 0 .../ProgramAttendStatusManageSection.tsx | 0 .../detail/program/ProgramDetailSection.tsx | 51 +++++++++++++++++++ .../detail/program/ProgramHeaderSection.tsx} | 30 ++++++++--- .../AttendStatusModal.loader.tsx | 0 .../userAttendModal/AttendStatusView.tsx | 0 .../userAttendModal/AttendToggleLabel.tsx | 0 .../detail}/userAttendModal/LoginModal.tsx | 0 .../UserAttendModal.container.tsx | 0 .../userAttendModal/UserAttendModal.tsx | 0 .../programDetail/program/ProgramInfo.tsx | 32 ------------ FE/src/hooks/{useAccess.tsx => useAccess.ts} | 0 FE/src/hooks/usePrograms.ts | 6 +++ 23 files changed, 102 insertions(+), 99 deletions(-) delete mode 100644 FE/src/components/feature/detail/ProgramDetail.tsx rename FE/src/components/{programDetail => feature/detail}/attendee/AttendeeInfo.container.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/attendee/AttendeeInfo.loader.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/attendee/AttendeeInfo.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/attendee/AttendeeStatus.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/attendee/BluredAttedee.tsx (100%) delete mode 100644 FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx rename FE/src/components/{programDetail => feature/detail}/program/EditAndDeleteButton.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/program/ProgramAttendStatusManageSection.tsx (100%) create mode 100644 FE/src/components/feature/detail/program/ProgramDetailSection.tsx rename FE/src/components/{programDetail/program/ProgramHeader.tsx => feature/detail/program/ProgramHeaderSection.tsx} (52%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/AttendStatusModal.loader.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/AttendStatusView.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/AttendToggleLabel.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/LoginModal.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/UserAttendModal.container.tsx (100%) rename FE/src/components/{programDetail => feature/detail}/userAttendModal/UserAttendModal.tsx (100%) delete mode 100644 FE/src/components/programDetail/program/ProgramInfo.tsx rename FE/src/hooks/{useAccess.tsx => useAccess.ts} (100%) create mode 100644 FE/src/hooks/usePrograms.ts diff --git a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx index 4eb29799..f49bdee2 100644 --- a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx +++ b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx @@ -1,5 +1,6 @@ -import AttendeeInfoContainer from "@/components/programDetail/attendee/AttendeeInfo.container"; -import ProgramInfo from "@/components/programDetail/program/ProgramInfo"; +import AttendeeInfoContainer from "@/components/feature/detail/attendee/AttendeeInfo.container"; +import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; +import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; interface ProgramDetailPageProps { params: { @@ -12,7 +13,10 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return ( <div className="mb-16 space-y-16"> - <ProgramInfo programId={+programId} accessType="admin" /> + <section className="space-y-8"> + <ProgramHeaderSection /> + <ProgramDetailSection /> + </section> <AttendeeInfoContainer programId={+programId} isLoggedIn /> </div> ); diff --git a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx index 0701336d..3ff1a964 100644 --- a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx +++ b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx @@ -1,6 +1,7 @@ -import AttendeeInfoContainer from "@/components/programDetail/attendee/AttendeeInfo.container"; -import ProgramInfo from "@/components/programDetail/program/ProgramInfo"; -import UserAttendModalContainer from "@/components/programDetail/userAttendModal/UserAttendModal.container"; +import AttendeeInfoContainer from "@/components/feature/detail/attendee/AttendeeInfo.container"; +import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; +import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; +import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; interface ProgramDetailPageProps { params: { @@ -13,7 +14,10 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return ( <div className="mb-16 space-y-16"> - <ProgramInfo programId={+programId} accessType="public" /> + <section className="space-y-8"> + <ProgramHeaderSection /> + <ProgramDetailSection /> + </section> <AttendeeInfoContainer programId={+programId} isLoggedIn={false} /> <UserAttendModalContainer programId={+programId} isLoggedIn={false} /> </div> diff --git a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx index 08621ea5..9a1c5287 100644 --- a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx +++ b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx @@ -1,6 +1,7 @@ -import AttendeeInfoContainer from "@/components/programDetail/attendee/AttendeeInfo.container"; -import ProgramInfo from "@/components/programDetail/program/ProgramInfo"; -import UserAttendModalContainer from "@/components/programDetail/userAttendModal/UserAttendModal.container"; +import AttendeeInfoContainer from "@/components/feature/detail/attendee/AttendeeInfo.container"; +import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; +import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; +import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; interface ProgramDetailPageProps { params: { @@ -13,7 +14,10 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return ( <div className="mb-16 space-y-16"> - <ProgramInfo programId={+programId} accessType="private" /> + <section className="space-y-8"> + <ProgramHeaderSection /> + <ProgramDetailSection /> + </section> <AttendeeInfoContainer programId={+programId} isLoggedIn /> <UserAttendModalContainer programId={+programId} isLoggedIn /> </div> diff --git a/FE/src/components/feature/detail/ProgramDetail.tsx b/FE/src/components/feature/detail/ProgramDetail.tsx deleted file mode 100644 index 28b1794b..00000000 --- a/FE/src/components/feature/detail/ProgramDetail.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { useQueryClient } from "@tanstack/react-query"; -import ProgramAttendStatusManageSection from "../../programDetail/program/ProgramAttendStatusManageSection"; -import ProgramPresentations from "./ProgramPresentations"; -import { ProgramInfoDto } from "@/apis/dtos/program.dto"; -import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; -import ProgramDashboardSection from "./Dashboard/ProgramDashboardSection"; -import { useGetAccessType } from "@/hooks/useAccess"; - -interface ProgramDetailProps { - data: ProgramInfoDto; - programId: number; -} - -const ProgramDetail = ({ data, programId }: ProgramDetailProps) => { - const accessType = useGetAccessType(); - const isAdmin = accessType === "admin"; - - const { content } = data; - - const queryClient = useQueryClient(); - const githubUrl = queryClient.getQueryData(["githubUrl", programId]); - - return ( - <div> - <MarkdownViewer value={content} /> - {isAdmin && <ProgramAttendStatusManageSection programId={programId} />} - {githubUrl && <ProgramPresentations programId={programId} />} - <div className="mt-12"> - <ProgramDashboardSection programId={programId} /> - </div> - </div> - ); -}; -export default ProgramDetail; diff --git a/FE/src/components/programDetail/attendee/AttendeeInfo.container.tsx b/FE/src/components/feature/detail/attendee/AttendeeInfo.container.tsx similarity index 100% rename from FE/src/components/programDetail/attendee/AttendeeInfo.container.tsx rename to FE/src/components/feature/detail/attendee/AttendeeInfo.container.tsx diff --git a/FE/src/components/programDetail/attendee/AttendeeInfo.loader.tsx b/FE/src/components/feature/detail/attendee/AttendeeInfo.loader.tsx similarity index 100% rename from FE/src/components/programDetail/attendee/AttendeeInfo.loader.tsx rename to FE/src/components/feature/detail/attendee/AttendeeInfo.loader.tsx diff --git a/FE/src/components/programDetail/attendee/AttendeeInfo.tsx b/FE/src/components/feature/detail/attendee/AttendeeInfo.tsx similarity index 100% rename from FE/src/components/programDetail/attendee/AttendeeInfo.tsx rename to FE/src/components/feature/detail/attendee/AttendeeInfo.tsx diff --git a/FE/src/components/programDetail/attendee/AttendeeStatus.tsx b/FE/src/components/feature/detail/attendee/AttendeeStatus.tsx similarity index 100% rename from FE/src/components/programDetail/attendee/AttendeeStatus.tsx rename to FE/src/components/feature/detail/attendee/AttendeeStatus.tsx diff --git a/FE/src/components/programDetail/attendee/BluredAttedee.tsx b/FE/src/components/feature/detail/attendee/BluredAttedee.tsx similarity index 100% rename from FE/src/components/programDetail/attendee/BluredAttedee.tsx rename to FE/src/components/feature/detail/attendee/BluredAttedee.tsx diff --git a/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx b/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx deleted file mode 100644 index ccd99556..00000000 --- a/FE/src/components/feature/detail/loader/ProgramInfo.loader.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import ProgramDetailSkeleton from "./ProgramDetail.skeleton"; -import ProgramHeaderSkeleton from "./ProgramHeader.skeleton"; - -const ProgramInfoLoader = () => { - return ( - <div className="space-y-8"> - <ProgramHeaderSkeleton /> - <ProgramDetailSkeleton /> - </div> - ); -}; -export default ProgramInfoLoader; diff --git a/FE/src/components/programDetail/program/EditAndDeleteButton.tsx b/FE/src/components/feature/detail/program/EditAndDeleteButton.tsx similarity index 100% rename from FE/src/components/programDetail/program/EditAndDeleteButton.tsx rename to FE/src/components/feature/detail/program/EditAndDeleteButton.tsx diff --git a/FE/src/components/programDetail/program/ProgramAttendStatusManageSection.tsx b/FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx similarity index 100% rename from FE/src/components/programDetail/program/ProgramAttendStatusManageSection.tsx rename to FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx diff --git a/FE/src/components/feature/detail/program/ProgramDetailSection.tsx b/FE/src/components/feature/detail/program/ProgramDetailSection.tsx new file mode 100644 index 00000000..43b9792b --- /dev/null +++ b/FE/src/components/feature/detail/program/ProgramDetailSection.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import ProgramAttendStatusManageSection from "./ProgramAttendStatusManageSection"; +import ProgramPresentations from "../ProgramPresentations"; +import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; +import ProgramDashboardSection from "../Dashboard/ProgramDashboardSection"; +import { useGetAccessType } from "@/hooks/useAccess"; +import { useGetProgramId } from "@/hooks/usePrograms"; +import { useGetProgramByProgramId } from "@/hooks/query/useProgramQuery"; +import ProgramDetailSkeleton from "../loader/ProgramDetail.skeleton"; + +const ProgramDetailSection = () => { + const queryClient = useQueryClient(); + + const accessType = useGetAccessType(); + const isAdmin = accessType === "admin"; + const isGuest = accessType === "public"; + + const isAbleToEdit = useGetAccessType() === "admin"; + const programId = useGetProgramId(); + + const { + data: programData, + isLoading, + isError, + } = useGetProgramByProgramId(programId, isAbleToEdit); + + if (isLoading) return <ProgramDetailSkeleton />; + if (isError) return <div>에러 발생</div>; + + const { content } = programData; + + const githubUrl = queryClient.getQueryData(["githubUrl", programId]); + + return ( + <div> + <MarkdownViewer value={content} /> + {isAdmin && <ProgramAttendStatusManageSection programId={programId} />} + {githubUrl && <ProgramPresentations programId={programId} />} + <div className="mt-12"> + {isGuest ? ( + <div>로그인 후 이용해주세요.</div> + ) : ( + <ProgramDashboardSection programId={programId} /> + )} + </div> + </div> + ); +}; +export default ProgramDetailSection; diff --git a/FE/src/components/programDetail/program/ProgramHeader.tsx b/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx similarity index 52% rename from FE/src/components/programDetail/program/ProgramHeader.tsx rename to FE/src/components/feature/detail/program/ProgramHeaderSection.tsx index cd4b1ac3..f6092837 100644 --- a/FE/src/components/programDetail/program/ProgramHeader.tsx +++ b/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx @@ -1,18 +1,32 @@ +"use client"; + import EditAndDeleteButton from "./EditAndDeleteButton"; -import { ProgramInfoDto } from "@/apis/dtos/program.dto"; import TabItem from "@/components/common/tabs/tab/TabItem"; import Title from "@/components/common/Title/Title"; import PROGRAM from "@/constants/PROGRAM"; +import { useGetProgramByProgramId } from "@/hooks/query/useProgramQuery"; +import { useGetAccessType } from "@/hooks/useAccess"; import { formatTimestamp } from "@/utils/convert"; - -interface ProgramHeaderProps { - data: ProgramInfoDto; -} +import { useGetProgramId } from "@/hooks/usePrograms"; +import ProgramHeaderSkeleton from "../loader/ProgramHeader.skeleton"; const DEADLINE_TEXT = "행사일정 : "; -const ProgramHeader = ({ data }: ProgramHeaderProps) => { - const { category, title, deadLine, programId, accessRight } = data; +const ProgramHeaderSection = () => { + const isAbleToEdit = useGetAccessType() === "admin"; + const programId = useGetProgramId(); + + const { + data: programData, + isLoading, + isError, + } = useGetProgramByProgramId(programId, isAbleToEdit); + + // TODO: Loader 적용, 에러 처리 + if (isLoading) return <ProgramHeaderSkeleton />; + if (isError) return <div>에러 발생</div>; + + const { accessRight, category, deadLine, title } = programData; const categoryText = PROGRAM.CATEGORY_TAB[category]?.text ?? "기타"; @@ -31,4 +45,4 @@ const ProgramHeader = ({ data }: ProgramHeaderProps) => { </section> ); }; -export default ProgramHeader; +export default ProgramHeaderSection; diff --git a/FE/src/components/programDetail/userAttendModal/AttendStatusModal.loader.tsx b/FE/src/components/feature/detail/userAttendModal/AttendStatusModal.loader.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/AttendStatusModal.loader.tsx rename to FE/src/components/feature/detail/userAttendModal/AttendStatusModal.loader.tsx diff --git a/FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx b/FE/src/components/feature/detail/userAttendModal/AttendStatusView.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/AttendStatusView.tsx rename to FE/src/components/feature/detail/userAttendModal/AttendStatusView.tsx diff --git a/FE/src/components/programDetail/userAttendModal/AttendToggleLabel.tsx b/FE/src/components/feature/detail/userAttendModal/AttendToggleLabel.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/AttendToggleLabel.tsx rename to FE/src/components/feature/detail/userAttendModal/AttendToggleLabel.tsx diff --git a/FE/src/components/programDetail/userAttendModal/LoginModal.tsx b/FE/src/components/feature/detail/userAttendModal/LoginModal.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/LoginModal.tsx rename to FE/src/components/feature/detail/userAttendModal/LoginModal.tsx diff --git a/FE/src/components/programDetail/userAttendModal/UserAttendModal.container.tsx b/FE/src/components/feature/detail/userAttendModal/UserAttendModal.container.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/UserAttendModal.container.tsx rename to FE/src/components/feature/detail/userAttendModal/UserAttendModal.container.tsx diff --git a/FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx b/FE/src/components/feature/detail/userAttendModal/UserAttendModal.tsx similarity index 100% rename from FE/src/components/programDetail/userAttendModal/UserAttendModal.tsx rename to FE/src/components/feature/detail/userAttendModal/UserAttendModal.tsx diff --git a/FE/src/components/programDetail/program/ProgramInfo.tsx b/FE/src/components/programDetail/program/ProgramInfo.tsx deleted file mode 100644 index 3c5714bc..00000000 --- a/FE/src/components/programDetail/program/ProgramInfo.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import ProgramDetail from "../../feature/detail/ProgramDetail"; -import ProgramHeader from "./ProgramHeader"; -import ProgramInfoLoader from "../../feature/detail/loader/ProgramInfo.loader"; -import { useGetProgramByProgramId } from "@/hooks/query/useProgramQuery"; -import { AccessType } from "@/types/access"; - -interface ProgramInfoProps { - programId: number; - accessType?: AccessType; -} - -const ProgramInfo = ({ programId, accessType }: ProgramInfoProps) => { - const isAbleToEdit = accessType === "admin"; - const { - data: programData, - isLoading, - isError, - } = useGetProgramByProgramId(programId, isAbleToEdit); - - if (isLoading) return <ProgramInfoLoader />; - if (isError) return <div>에러 발생</div>; - - return ( - <section className="space-y-8"> - <ProgramHeader data={programData} /> - <ProgramDetail data={programData} programId={programId} /> - </section> - ); -}; -export default ProgramInfo; diff --git a/FE/src/hooks/useAccess.tsx b/FE/src/hooks/useAccess.ts similarity index 100% rename from FE/src/hooks/useAccess.tsx rename to FE/src/hooks/useAccess.ts diff --git a/FE/src/hooks/usePrograms.ts b/FE/src/hooks/usePrograms.ts new file mode 100644 index 00000000..476560c2 --- /dev/null +++ b/FE/src/hooks/usePrograms.ts @@ -0,0 +1,6 @@ +import { useParams } from "next/navigation"; + +export const useGetProgramId = () => { + const { programId } = useParams(); + return +programId; +}; From 4bc2238e0f5bb4c9a9e0c284954c1ab3882ffc47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Fri, 29 Nov 2024 22:53:42 +0900 Subject: [PATCH 22/33] =?UTF-8?q?refactor(detail-refactor):=20programDetai?= =?UTF-8?q?lSection=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20section=EB=93=A4=EC=9D=B4=20=EB=AC=B6?= =?UTF-8?q?=EC=97=AC=EC=9E=88=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(admin)/admin/detail/[programId]/page.tsx | 8 ++++++ .../(guest)/guest/detail/[programId]/page.tsx | 7 +++++ .../(program)/detail/[programId]/page.tsx | 7 +++++ .../Dashboard/ProgramDashboardSection.tsx | 9 +++---- ...ns.tsx => ProgramPresentationsSection.tsx} | 16 +++++++---- .../ProgramAttendStatusManageSection.tsx | 14 +++++----- .../detail/program/ProgramDetailSection.tsx | 27 +------------------ 7 files changed, 43 insertions(+), 45 deletions(-) rename FE/src/components/feature/detail/{ProgramPresentations.tsx => ProgramPresentationsSection.tsx} (75%) diff --git a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx index f49bdee2..9fc04f35 100644 --- a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx +++ b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx @@ -1,6 +1,9 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/AttendeeInfo.container"; import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; +import ProgramattendModeManageSection from "@/components/feature/detail/program/ProgramAttendStatusManageSection"; +import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; +import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { params: { @@ -16,6 +19,11 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { <section className="space-y-8"> <ProgramHeaderSection /> <ProgramDetailSection /> + <ProgramattendModeManageSection /> + <ProgramPresentationsSection /> + <div className="mt-12"> + <ProgramDashboardSection /> + </div> </section> <AttendeeInfoContainer programId={+programId} isLoggedIn /> </div> diff --git a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx index 3ff1a964..c33edb27 100644 --- a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx +++ b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx @@ -2,6 +2,8 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/Attendee import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; +import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; +import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { params: { @@ -17,10 +19,15 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { <section className="space-y-8"> <ProgramHeaderSection /> <ProgramDetailSection /> + <ProgramPresentationsSection /> + <div className="mt-12"> + <ProgramDashboardSection /> + </div> </section> <AttendeeInfoContainer programId={+programId} isLoggedIn={false} /> <UserAttendModalContainer programId={+programId} isLoggedIn={false} /> </div> ); }; + export default ProgramDetailPage; diff --git a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx index 9a1c5287..de95fc73 100644 --- a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx +++ b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx @@ -2,6 +2,8 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/Attendee import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; +import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; +import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { params: { @@ -17,10 +19,15 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { <section className="space-y-8"> <ProgramHeaderSection /> <ProgramDetailSection /> + <ProgramPresentationsSection /> + <div className="mt-12"> + <ProgramDashboardSection /> + </div> </section> <AttendeeInfoContainer programId={+programId} isLoggedIn /> <UserAttendModalContainer programId={+programId} isLoggedIn /> </div> ); }; + export default ProgramDetailPage; diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx index 9bf61822..59057dbc 100644 --- a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -5,13 +5,10 @@ import Title from "@/components/common/Title/Title"; import { useTeamQuery } from "@/hooks/query/useTeamQuery"; import DashboardContent from "./components/DashboardContent"; import DashboardInput from "./components/DashboardInput"; +import { useGetProgramId } from "@/hooks/usePrograms"; -interface ProgramDashboardSectionProps { - programId: number; -} -const ProgramDashboardSection = ({ - programId, -}: ProgramDashboardSectionProps) => { +const ProgramDashboardSection = () => { + const programId = useGetProgramId(); const { data, isLoading } = useTeamQuery(programId); if (isLoading || !data) return null; diff --git a/FE/src/components/feature/detail/ProgramPresentations.tsx b/FE/src/components/feature/detail/ProgramPresentationsSection.tsx similarity index 75% rename from FE/src/components/feature/detail/ProgramPresentations.tsx rename to FE/src/components/feature/detail/ProgramPresentationsSection.tsx index e38a302c..d4e8fe6e 100644 --- a/FE/src/components/feature/detail/ProgramPresentations.tsx +++ b/FE/src/components/feature/detail/ProgramPresentationsSection.tsx @@ -4,17 +4,23 @@ import Image from "next/image"; import Link from "next/link"; import Title from "@/components/common/Title/Title"; import usePresentations from "@/hooks/query/usePresentations"; +import { useQueryClient } from "@tanstack/react-query"; +import { useGetProgramId } from "@/hooks/usePrograms"; + +const ProgramPresentationsSection = () => { + const programId = useGetProgramId(); + const queryClient = useQueryClient(); + + const githubUrl = queryClient.getQueryData(["githubUrl", programId]); -interface ProgramPresentationsProps { - programId: number; -} -const ProgramPresentations = ({ programId }: ProgramPresentationsProps) => { const { data: presentations, isLoading, isError, } = usePresentations(programId); + if (githubUrl) return null; + if (isError) return null; if (isLoading) return null; @@ -48,4 +54,4 @@ const ProgramPresentations = ({ programId }: ProgramPresentationsProps) => { ); }; -export default ProgramPresentations; +export default ProgramPresentationsSection; diff --git a/FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx b/FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx index 3c38b23e..6380a21c 100644 --- a/FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx +++ b/FE/src/components/feature/detail/program/ProgramAttendStatusManageSection.tsx @@ -1,3 +1,5 @@ +"use client"; + // TODO: 리팩토링 필요 : 중복되는 코드 줄이기 // 출석 상태값만 받아오는 API 필요 @@ -6,17 +8,13 @@ import StatusToggleItem from "@/components/common/StatusToggleItem"; import Title from "@/components/common/Title/Title"; import { useGetProgramByProgramId, - // useGetProgramAttendModeAndStatus, - // useGetProgramByProgramId, useUpdateProgramAttendMode, } from "@/hooks/query/useProgramQuery"; +import { useGetProgramId } from "@/hooks/usePrograms"; + +const ProgramattendModeManageSection = () => { + const programId = useGetProgramId(); -interface ProgramattendModeManageSectionProps { - programId: number; -} -const ProgramattendModeManageSection = ({ - programId, -}: ProgramattendModeManageSectionProps) => { const { mutate: updateProgramAttendMode, isLoading } = useUpdateProgramAttendMode(programId); diff --git a/FE/src/components/feature/detail/program/ProgramDetailSection.tsx b/FE/src/components/feature/detail/program/ProgramDetailSection.tsx index 43b9792b..47ee12e1 100644 --- a/FE/src/components/feature/detail/program/ProgramDetailSection.tsx +++ b/FE/src/components/feature/detail/program/ProgramDetailSection.tsx @@ -1,22 +1,12 @@ "use client"; -import { useQueryClient } from "@tanstack/react-query"; -import ProgramAttendStatusManageSection from "./ProgramAttendStatusManageSection"; -import ProgramPresentations from "../ProgramPresentations"; import MarkdownViewer from "@/components/common/markdown/MarkdownViewer"; -import ProgramDashboardSection from "../Dashboard/ProgramDashboardSection"; import { useGetAccessType } from "@/hooks/useAccess"; import { useGetProgramId } from "@/hooks/usePrograms"; import { useGetProgramByProgramId } from "@/hooks/query/useProgramQuery"; import ProgramDetailSkeleton from "../loader/ProgramDetail.skeleton"; const ProgramDetailSection = () => { - const queryClient = useQueryClient(); - - const accessType = useGetAccessType(); - const isAdmin = accessType === "admin"; - const isGuest = accessType === "public"; - const isAbleToEdit = useGetAccessType() === "admin"; const programId = useGetProgramId(); @@ -31,21 +21,6 @@ const ProgramDetailSection = () => { const { content } = programData; - const githubUrl = queryClient.getQueryData(["githubUrl", programId]); - - return ( - <div> - <MarkdownViewer value={content} /> - {isAdmin && <ProgramAttendStatusManageSection programId={programId} />} - {githubUrl && <ProgramPresentations programId={programId} />} - <div className="mt-12"> - {isGuest ? ( - <div>로그인 후 이용해주세요.</div> - ) : ( - <ProgramDashboardSection programId={programId} /> - )} - </div> - </div> - ); + return <MarkdownViewer value={content} />; }; export default ProgramDetailSection; From 7f0c3f4ec901244a12320e481ceb25c9b3d518b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Fri, 29 Nov 2024 23:28:01 +0900 Subject: [PATCH 23/33] =?UTF-8?q?refactor:=20=EC=A7=88=EB=AC=B8=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=ED=8C=90=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=ED=8C=80=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=ED=83=AD=20=EB=B6=80=EB=B6=84=EC=9D=84=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dashboard에서 많은 책임을 가지고 있던 부분에서 탭 부분은 분리 --- FE/src/components/common/tabs/TeamsTab.tsx | 53 ++++++++++++ FE/src/components/common/tabs/tab/Tab.tsx | 2 + .../common/tabs/tab/TabAsChild/TabAsChild.tsx | 85 ------------------- .../Dashboard/ProgramDashboardSection.tsx | 59 +++---------- .../Dashboard/components/DashboardInput.tsx | 6 +- 5 files changed, 70 insertions(+), 135 deletions(-) create mode 100644 FE/src/components/common/tabs/TeamsTab.tsx delete mode 100644 FE/src/components/common/tabs/tab/TabAsChild/TabAsChild.tsx diff --git a/FE/src/components/common/tabs/TeamsTab.tsx b/FE/src/components/common/tabs/TeamsTab.tsx new file mode 100644 index 00000000..3977517c --- /dev/null +++ b/FE/src/components/common/tabs/TeamsTab.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useTeamQuery } from "@/hooks/query/useTeamQuery"; +import Tab from "./tab/TabCompound/TabCompound"; + +interface SelectedItemProps { + teamName: string; + teamId: number; +} + +interface TeamsTabProps { + programId: number; + children: (selectedItem: SelectedItemProps) => JSX.Element; +} +const TeamsTab = ({ programId, children }: TeamsTabProps) => { + const { data: teamsQueryData, isLoading, isError } = useTeamQuery(programId); + + if (isLoading) return null; + if (isError) return null; + + const { teams } = teamsQueryData; + if (teams.length === 0) return null; + + const teamNameArray = teams.map(({ teamName }) => teamName); + + return ( + <Tab<string> + align="line" + defaultSelected={`${teams[0].teamName}`} + nonPickedColor="gray" + pickedColor="navy" + tabItemList={teamNameArray} + tabSize="md" + > + <Tab.List> + {teamNameArray.map((name, index) => ( + <Tab.Item key={`${name}-${index}`} text={name} /> + ))} + </Tab.List> + <Tab.Content<string>> + {({ selectedItem }) => + children({ + teamName: selectedItem, + teamId: teams.find(({ teamName }) => teamName === selectedItem) + ?.teamId, + }) + } + </Tab.Content> + </Tab> + ); +}; + +export default TeamsTab; diff --git a/FE/src/components/common/tabs/tab/Tab.tsx b/FE/src/components/common/tabs/tab/Tab.tsx index 0b55eecc..3420c325 100644 --- a/FE/src/components/common/tabs/tab/Tab.tsx +++ b/FE/src/components/common/tabs/tab/Tab.tsx @@ -1,3 +1,5 @@ +// TODO 해당 컴포넌트 대신 /tabCompound/Tab 컴포넌트를 하용하기 바랍니다. + import classNames from "classnames"; import TabItem, { tabColors, tabSizes } from "./TabItem"; import { TabOption } from "@/types/tab"; diff --git a/FE/src/components/common/tabs/tab/TabAsChild/TabAsChild.tsx b/FE/src/components/common/tabs/tab/TabAsChild/TabAsChild.tsx deleted file mode 100644 index 29ac9f34..00000000 --- a/FE/src/components/common/tabs/tab/TabAsChild/TabAsChild.tsx +++ /dev/null @@ -1,85 +0,0 @@ -//TODO: 스크립트 작성 후 삭제할 파일 -"use client"; -import classNames from "classnames"; -import { useState, ReactNode } from "react"; -import TabItem from "../TabItem"; - -const tabAlign = { - line: "flex gap-4", - square: "grid grid-cols-2 gap-4", -} as const; - -export const tabColors = { - gray: "bg-gray-10 text-gray-30 border-gray-20", - yellow: "bg-warning-10 text-warning-30 border-warning-30", - teal: "bg-secondary-20 text-tertiary-20 border-tertiary-20", - white: "bg-background text-gray-30 border-background", - navy: "bg-paragraph text-background border-paragraph", -}; - -export const tabSizes = { - sm: "min-w-[4.25rem] px-2 py-[0.3rem] text-xs", - md: "min-w-[5rem] px-3 py-2 text-sm", - lg: "min-w-[6rem] px-4 py-2 text-base", -}; - -//TODO: Tab 컴포넌트까지 새롭게 만들어야 함. -interface TabAsChildProps<ListType> { - defaultSelected: ListType; - tabItemList?: ListType[]; - align: keyof typeof tabAlign; - children: ({ selectedItem }: { selectedItem: ListType }) => ReactNode; - rounded?: boolean; - tabSize: keyof typeof tabSizes; - nonPickedColor: keyof typeof tabColors; - pickedColor: keyof typeof tabColors; -} - -/** - * [변경] 탭의 사이즈, 색상, 정렬등의 모든 책임을 해당 컴포넌트에서 가집니다. - * - * @example - children에 ({selectedItem}) => <div>{selectedItem === "Home" && <HomeContent />}</div> 와 같이 사용합니다. - * - */ - -const TabAsChild = <ListType extends string>({ - defaultSelected, - tabItemList, - nonPickedColor, - pickedColor, - rounded, - align, - tabSize, - children, -}: TabAsChildProps<ListType>) => { - const [selectedItem, setSelectedItem] = useState<ListType>(defaultSelected); - - if (!tabItemList) { - throw new Error("optionItemList이 필요합니다."); - } - - const tabStyle = classNames( - tabAlign[align], - "w-full overflow-x-scroll scrollbar-hide", - ); - - return ( - <> - <div className={tabStyle}> - {tabItemList.map((item) => ( - <TabItem - key={item} - color={item === selectedItem ? pickedColor : nonPickedColor} - size={tabSize} - text={item} - onClick={() => setSelectedItem(item)} - rounded={rounded} - /> - ))} - </div> - {children({ selectedItem })} - </> - ); -}; - -export default TabAsChild; diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx index 59057dbc..2d474838 100644 --- a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -1,63 +1,30 @@ "use client"; -import Tab from "@/components/common/tabs/tab/TabCompound/TabCompound"; import Title from "@/components/common/Title/Title"; -import { useTeamQuery } from "@/hooks/query/useTeamQuery"; import DashboardContent from "./components/DashboardContent"; import DashboardInput from "./components/DashboardInput"; import { useGetProgramId } from "@/hooks/usePrograms"; +import TeamsTab from "@/components/common/tabs/TeamsTab"; const ProgramDashboardSection = () => { const programId = useGetProgramId(); - const { data, isLoading } = useTeamQuery(programId); - - if (isLoading || !data) return null; - - const { teams } = data; - if (teams.length === 0) return null; - - const teamNameArray = teams.map(({ teamName }) => teamName); return ( <section> <Title text="질문 게시판" /> <div className="mt-4" /> - <Tab<string> - align="line" - defaultSelected={`${teams[0].teamName}`} - nonPickedColor="gray" - pickedColor="navy" - tabItemList={teamNameArray} - tabSize="md" - > - <Tab.List> - {teamNameArray.map((name, index) => ( - <Tab.Item key={`${name}-${index}`} text={name} /> - ))} - </Tab.List> - <Tab.Content<string>> - {({ selectedItem }) => ( - <div className="mt-8 flex flex-col gap-8"> - <DashboardContent - programId={programId} - selectedTeamId={ - teams.find(({ teamName }) => teamName === selectedItem) - ?.teamId - } - /> - - <DashboardInput - teams={teams} - programId={programId} - selectedTeamId={ - teams.find(({ teamName }) => teamName === selectedItem) - ?.teamId - } - /> - </div> - )} - </Tab.Content> - </Tab> + <TeamsTab programId={programId}> + {({ teamId, teamName }) => ( + <div className="mt-8 flex flex-col gap-8"> + <DashboardContent programId={programId} selectedTeamId={teamId} /> + <DashboardInput + programId={programId} + selectedTeamId={teamId} + selectedTeamName={teamName} + /> + </div> + )} + </TeamsTab> </section> ); }; diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index e1c201bd..4f847706 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -13,14 +13,14 @@ import { useState } from "react"; interface DashboardInputProps { programId: number; selectedTeamId: number; - teams: TeamInfo[]; + selectedTeamName: string; } //TODO: UI 분리하기 const DashboardInput = ({ programId, selectedTeamId, - teams, + selectedTeamName, }: DashboardInputProps) => { const [isAnonymous, setIsAnonymous] = useAtom(dashboardAtoms.isAnonymous); const [questionInput, setQuestionInput] = useAtom( @@ -35,8 +35,6 @@ const DashboardInput = ({ const { mutate: postQuestion } = usePostQuestion(); const isReply = selectedCommentId !== -1; - const selectedTeamName = teams?.find((team) => team.teamId === selectedTeamId) - ?.teamName; const accessType = useGetAccessType(); From 9249e114d32772f7a3bab0bc4efd107e5fbdd4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Sat, 30 Nov 2024 16:26:45 +0900 Subject: [PATCH 24/33] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=94=94=ED=85=8C=EC=9D=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=91=EA=B7=BC=EC=8B=9C=20=EB=B8=94=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=90=9C=20=EC=A7=88=EB=AC=B8=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=ED=8C=90=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(admin)/admin/detail/[programId]/page.tsx | 2 +- .../(guest)/guest/detail/[programId]/page.tsx | 6 +- .../(program)/detail/[programId]/page.tsx | 2 +- .../components/common/CheckBox/CheckBox.tsx | 4 +- .../tabs/tab/TabCompound/TabCompound.tsx | 2 +- .../detail/Dashboard/BlurDashboard.tsx | 117 ++++++++++++++++++ .../Dashboard/ProgramDashboardSection.tsx | 2 +- .../detail/Dashboard}/TeamsTab.tsx | 2 +- .../detail/Dashboard/components/Chat.tsx | 2 + .../detail/Dashboard/components/ChatList.tsx | 2 + .../ProgramPresentationsSection.tsx | 0 .../detail/program/ProgramHeaderSection.tsx | 1 - 12 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 FE/src/components/feature/detail/Dashboard/BlurDashboard.tsx rename FE/src/components/{common/tabs => feature/detail/Dashboard}/TeamsTab.tsx (94%) rename FE/src/components/feature/detail/{ => presentation}/ProgramPresentationsSection.tsx (100%) diff --git a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx index 9fc04f35..3cce624d 100644 --- a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx +++ b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx @@ -2,7 +2,7 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/Attendee import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; import ProgramattendModeManageSection from "@/components/feature/detail/program/ProgramAttendStatusManageSection"; -import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { diff --git a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx index c33edb27..822373a3 100644 --- a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx +++ b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx @@ -2,8 +2,8 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/Attendee import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; -import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; -import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; +import BlurDashboard from "@/components/feature/detail/Dashboard/BlurDashboard"; interface ProgramDetailPageProps { params: { @@ -21,7 +21,7 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { <ProgramDetailSection /> <ProgramPresentationsSection /> <div className="mt-12"> - <ProgramDashboardSection /> + <BlurDashboard /> </div> </section> <AttendeeInfoContainer programId={+programId} isLoggedIn={false} /> diff --git a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx index de95fc73..8ead99d2 100644 --- a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx +++ b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx @@ -2,7 +2,7 @@ import AttendeeInfoContainer from "@/components/feature/detail/attendee/Attendee import ProgramHeaderSection from "@/components/feature/detail/program/ProgramHeaderSection"; import ProgramDetailSection from "@/components/feature/detail/program/ProgramDetailSection"; import UserAttendModalContainer from "@/components/feature/detail/userAttendModal/UserAttendModal.container"; -import ProgramPresentationsSection from "@/components/feature/detail/ProgramPresentationsSection"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { diff --git a/FE/src/components/common/CheckBox/CheckBox.tsx b/FE/src/components/common/CheckBox/CheckBox.tsx index 415c9d28..e4143a15 100644 --- a/FE/src/components/common/CheckBox/CheckBox.tsx +++ b/FE/src/components/common/CheckBox/CheckBox.tsx @@ -1,9 +1,11 @@ +"use client"; + import classNames from "classnames"; import Image from "next/image"; interface CheckBoxProps { checked: boolean; - onClick: () => void; + onClick?: () => void; disabled?: boolean; className?: string; } diff --git a/FE/src/components/common/tabs/tab/TabCompound/TabCompound.tsx b/FE/src/components/common/tabs/tab/TabCompound/TabCompound.tsx index bc626c16..285c6fe6 100644 --- a/FE/src/components/common/tabs/tab/TabCompound/TabCompound.tsx +++ b/FE/src/components/common/tabs/tab/TabCompound/TabCompound.tsx @@ -1,7 +1,7 @@ "use client"; import classNames from "classnames"; -import React, { +import { createContext, useContext, useState, diff --git a/FE/src/components/feature/detail/Dashboard/BlurDashboard.tsx b/FE/src/components/feature/detail/Dashboard/BlurDashboard.tsx new file mode 100644 index 00000000..b5d4a6c4 --- /dev/null +++ b/FE/src/components/feature/detail/Dashboard/BlurDashboard.tsx @@ -0,0 +1,117 @@ +import { TeamInfo } from "@/types/team"; +import { Comment } from "@/apis/dtos/question.dto"; +import Chat from "./components/Chat"; +import CheckBox from "@/components/common/CheckBox/CheckBox"; +import StatusToggleItem from "@/components/common/StatusToggleItem"; +import Title from "@/components/common/Title/Title"; +import classNames from "classnames"; + +const fakeTeams: TeamInfo[] = [ + { + teamId: 1, + teamName: "Blackcompany", + }, + { + teamId: 2, + teamName: "whitecompany", + }, + { + teamId: 3, + teamName: "Team3", + }, + { + teamId: 4, + teamName: "이름이 어떻게", + }, + { + teamId: 5, + teamName: "econo", + }, +]; + +const comments: Comment[] = [ + { + commentId: 1, + teamId: 1, + content: + "교육의 자주성·전문성·정치적 중립성 및 대학의 자율성은 법률이 정하는 바에 의하여 보장된다?? 공무원은 국민전체에 대한 봉사자이며, 국민에 대하여 책임을 진다.\n\n중앙선거관리위원회는 법령의 범위안에서 선거관리·국민투표관리 또는 정당사무에 관한 규칙을 제정할 수 있으며, 법률에 저촉되지 아니하는 범위안에서 내부규율에 관한 규칙을 제정할 수 있다?", + accessRight: "read_only", + writer: "이르음", + time: "2021-10-01", + answers: [ + { + writer: "홍길동", + content: + "통신·방송의 시설기준과 신문의 기능을 보장하기 위하여 필요한 사항은 법률로 정한다. 평화통일정책의 수립에 관한 대통령의 자문에 응하기 위하여 민주평화통일자문회의를 둘 수 있다.\n\n국무위원은 국무총리의 제청으로 대통령이 임명한다. 대통령은 조약을 체결·비준하고, 외교사절을 신임·접수 또는 파견하며, 선전포고와 강화를 한다.", + accessRight: "read_only", + commentId: 1, + time: "2021-10-01", + }, + { + writer: "김똥개", + content: "답변1", + accessRight: "read_only", + commentId: 1, + time: "2021-10-01", + }, + ], + }, +]; + +const BlurDashboard = () => { + return ( + <> + <Title text="질문 게시판" /> + <div className="mt-4" /> + <div className="pointer-events-none select-none blur-sm"> + {/* tab */} + <div className="flex gap-4"> + {fakeTeams.map(({ teamName }, i) => ( + <button + className={classNames( + "flex h-fit w-fit min-w-[5rem] items-center justify-center rounded-md border border-gray-300 px-3 py-2 text-sm font-semibold", + { + "bg-paragraph text-background": i === 0, + }, + )} + > + <p>{teamName}</p> + </button> + ))} + </div> + + {/* content */} + <div className="mt-8 flex flex-col gap-8"> + <div className="flex max-h-[36rem] w-full flex-col overflow-hidden overflow-y-auto rounded-sm border"> + {comments.map((props) => ( + <Chat key={props.commentId} {...props} /> + ))} + </div> + + {/* input */} + <div> + <div className="flex items-center justify-between gap-4"> + <p className="text-xl font-bold">@Blackcompany 에게 질문하기</p> + <label className="flex select-none items-center justify-end gap-2 text-lg"> + <CheckBox checked={false} className="h-5 w-5" /> + 익명으로 질문하기 + </label> + </div> + <div className="mb-2 " /> + <div className="relative"> + <textarea + className={`h-40 w-full resize-none rounded-sm border-2 p-4 px-8 pr-40 text-lg`} + placeholder="질문을 입력해주세요" + /> + <button className="absolute right-4 top-1/2 -translate-y-1/2"> + <StatusToggleItem color="green" text="전송" /> + </button> + </div> + </div> + </div> + </div> + </> + ); +}; + +export default BlurDashboard; diff --git a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx index 2d474838..dc9eddb7 100644 --- a/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx +++ b/FE/src/components/feature/detail/Dashboard/ProgramDashboardSection.tsx @@ -4,7 +4,7 @@ import Title from "@/components/common/Title/Title"; import DashboardContent from "./components/DashboardContent"; import DashboardInput from "./components/DashboardInput"; import { useGetProgramId } from "@/hooks/usePrograms"; -import TeamsTab from "@/components/common/tabs/TeamsTab"; +import TeamsTab from "@/components/feature/detail/Dashboard/TeamsTab"; const ProgramDashboardSection = () => { const programId = useGetProgramId(); diff --git a/FE/src/components/common/tabs/TeamsTab.tsx b/FE/src/components/feature/detail/Dashboard/TeamsTab.tsx similarity index 94% rename from FE/src/components/common/tabs/TeamsTab.tsx rename to FE/src/components/feature/detail/Dashboard/TeamsTab.tsx index 3977517c..9308020b 100644 --- a/FE/src/components/common/tabs/TeamsTab.tsx +++ b/FE/src/components/feature/detail/Dashboard/TeamsTab.tsx @@ -1,7 +1,7 @@ "use client"; +import Tab from "@/components/common/tabs/tab/TabCompound/TabCompound"; import { useTeamQuery } from "@/hooks/query/useTeamQuery"; -import Tab from "./tab/TabCompound/TabCompound"; interface SelectedItemProps { teamName: string; diff --git a/FE/src/components/feature/detail/Dashboard/components/Chat.tsx b/FE/src/components/feature/detail/Dashboard/components/Chat.tsx index c6aa47b0..c6d5f29f 100644 --- a/FE/src/components/feature/detail/Dashboard/components/Chat.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/Chat.tsx @@ -1,3 +1,5 @@ +"use client"; + import ChatList from "./ChatList"; import { Comment } from "@/apis/dtos/question.dto"; import ReplyChat from "./ReplyChat"; diff --git a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx index 23dbe373..f41ea253 100644 --- a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useDeleteQuestion, useUpdateQuestion, diff --git a/FE/src/components/feature/detail/ProgramPresentationsSection.tsx b/FE/src/components/feature/detail/presentation/ProgramPresentationsSection.tsx similarity index 100% rename from FE/src/components/feature/detail/ProgramPresentationsSection.tsx rename to FE/src/components/feature/detail/presentation/ProgramPresentationsSection.tsx diff --git a/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx b/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx index f6092837..3e4e062b 100644 --- a/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx +++ b/FE/src/components/feature/detail/program/ProgramHeaderSection.tsx @@ -22,7 +22,6 @@ const ProgramHeaderSection = () => { isError, } = useGetProgramByProgramId(programId, isAbleToEdit); - // TODO: Loader 적용, 에러 처리 if (isLoading) return <ProgramHeaderSkeleton />; if (isError) return <div>에러 발생</div>; From b22f75563e8734a4ab6d49359d340b4855753891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Sat, 30 Nov 2024 16:45:15 +0900 Subject: [PATCH 25/33] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20api?= =?UTF-8?q?=20=EB=AA=85=EC=84=B8=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20?= =?UTF-8?q?body=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #123 --- FE/src/apis/question.ts | 6 +++--- .../Dashboard/components/DashboardInput.tsx | 16 ++++++++-------- FE/src/store/dashboardAtoms.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/FE/src/apis/question.ts b/FE/src/apis/question.ts index 6617ba6f..e5ca7b1d 100644 --- a/FE/src/apis/question.ts +++ b/FE/src/apis/question.ts @@ -20,14 +20,14 @@ export interface PostQuestionParams { teamId: number; questionContent: string; parentsCommentId?: number; - isAnonymous: 0 | 1; + commentType: "ANONYMOUS" | "NON_ANONYMOUS"; } export const postQuestion = async ({ programId, teamId, questionContent, parentsCommentId = -1, - isAnonymous = 0, + commentType = "NON_ANONYMOUS", }: PostQuestionParams) => { return await https({ url: API.QUESTION.CREATE, @@ -37,7 +37,7 @@ export const postQuestion = async ({ teamId, content: questionContent, parentsCommentId, - isAnonymous, + commentType, }, }); }; diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index 4f847706..ee30a109 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -22,7 +22,7 @@ const DashboardInput = ({ selectedTeamId, selectedTeamName, }: DashboardInputProps) => { - const [isAnonymous, setIsAnonymous] = useAtom(dashboardAtoms.isAnonymous); + const [commentType, setCommentType] = useAtom(dashboardAtoms.commentType); const [questionInput, setQuestionInput] = useAtom( dashboardAtoms.questionInput, ); @@ -51,7 +51,7 @@ const DashboardInput = ({ teamId: selectedTeamId, questionContent, parentsCommentId: selectedCommentId, - isAnonymous, + commentType, }; postQuestion(postQuestionParams); @@ -67,11 +67,8 @@ const DashboardInput = ({ return ( <div> - {/* <div className="absolute z-10 text-xl font-bold">{name}</div> */} {isReply ? ( <div className="truncate text-lg font-semibold"> - {/* <Image src={"/icons/x.svg"} alt="답글 종료" width={20} height={20} /> */} - {/* <button className="px-2 " onClick={() => setselectedCommentId(-1)}> */} <button className="px-2 " onClick={resetSelectedComment}> x </button> @@ -83,11 +80,14 @@ const DashboardInput = ({ <p className="text-xl font-bold">@{selectedTeamName} 에게 질문하기</p> <label className="flex select-none items-center justify-end gap-2 text-lg" - onClick={() => setIsAnonymous((prev) => (prev === 0 ? 1 : 0))} + onClick={() => + setCommentType((prev) => + prev === "ANONYMOUS" ? "NON_ANONYMOUS" : "ANONYMOUS", + ) + } > <CheckBox - checked={isAnonymous === 1} - onClick={() => {}} + checked={commentType === "ANONYMOUS"} className="h-5 w-5" /> 익명으로 질문하기 diff --git a/FE/src/store/dashboardAtoms.ts b/FE/src/store/dashboardAtoms.ts index 013a66cf..a237ff56 100644 --- a/FE/src/store/dashboardAtoms.ts +++ b/FE/src/store/dashboardAtoms.ts @@ -9,13 +9,13 @@ import { atomWithStorage } from "jotai/utils"; const selectedCommentId = atom(-1); const selectedCommentContent = atom(""); const questionInput = atomWithStorage("questionInput", ""); -const isAnonymous = atom<0 | 1>(0); +const commentType = atom<"ANONYMOUS" | "NON_ANONYMOUS">("NON_ANONYMOUS"); const dashboardAtoms = { selectedCommentId, selectedCommentContent, questionInput, - isAnonymous, + commentType, }; export default dashboardAtoms; From ed11dd44e9e43cd16d2f20b387425ae77beb2959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Sat, 30 Nov 2024 16:48:49 +0900 Subject: [PATCH 26/33] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/detail/Dashboard/components/ChatList.tsx | 4 ++-- .../feature/detail/Dashboard/components/DashboardInput.tsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx index f41ea253..25f29337 100644 --- a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx @@ -33,7 +33,7 @@ const ChatList = ({ const setSelectedCommentContent = useSetAtom( dashboardAtoms.selectedCommentContent, ); - const setIsAnonymous = useSetAtom(dashboardAtoms.isAnonymous); + const setIsAnonymous = useSetAtom(dashboardAtoms.commentType); const { mutate: updateComment, isSuccess: isUpdateSuccess } = useUpdateQuestion(); @@ -43,7 +43,7 @@ const ChatList = ({ const handleReply = () => { setSelectedCommentId(commentId); setSelectedCommentContent(content); - setIsAnonymous(0); + setIsAnonymous("NON_ANONYMOUS"); }; const handleUpdateComment = ({ diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index ee30a109..8931ef8d 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -6,9 +6,7 @@ import StatusToggleItem from "@/components/common/StatusToggleItem"; import { usePostQuestion } from "@/hooks/query/useQuestionQuery"; import { useGetAccessType } from "@/hooks/useAccess"; import dashboardAtoms from "@/store/dashboardAtoms"; -import { TeamInfo } from "@/types/team"; import { useAtom } from "jotai"; -import { useState } from "react"; interface DashboardInputProps { programId: number; From 9858c726a3f10c9a5dcbba01b2da78d8361a84fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Sat, 30 Nov 2024 16:57:02 +0900 Subject: [PATCH 27/33] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=8C=80=ED=95=98=EC=97=AC=20=EC=95=84?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=95=98=EB=8D=98=20=EC=BD=94=EB=93=9C=EB=93=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #123 --- FE/src/hooks/query/useQuestionQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index 43e6719d..b21a4e72 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -27,7 +27,7 @@ export const usePostQuestion = () => { mutationFn: async (postQuestionParams: PostQuestionParams) => await postQuestion(postQuestionParams), onMutate: ({ - isAnonymous, + commentType, programId, teamId, questionContent, @@ -49,7 +49,7 @@ export const usePostQuestion = () => { const newComment: Comment = { commentId: new Date().getTime(), teamId, - writer: isAnonymous ? "익명" : userName, + writer: commentType === "ANONYMOUS" ? "익명" : userName, accessRight: "edit", time: "방금전", content: questionContent, From 4c666b716384b31947dbf3bb6d04e2e58b83d3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Mon, 2 Dec 2024 00:53:31 +0900 Subject: [PATCH 28/33] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=B0=BD=20resize=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #144 --- FE/src/components/common/CheckBox/CheckBox.tsx | 2 +- .../feature/detail/Dashboard/components/DashboardInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FE/src/components/common/CheckBox/CheckBox.tsx b/FE/src/components/common/CheckBox/CheckBox.tsx index e4143a15..253ab649 100644 --- a/FE/src/components/common/CheckBox/CheckBox.tsx +++ b/FE/src/components/common/CheckBox/CheckBox.tsx @@ -26,7 +26,7 @@ const CheckBox = ({ ); const handleCheckBoxClick = () => { - !disabled && onClick(); + !disabled && onClick && onClick(); }; return ( diff --git a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx index 8931ef8d..04df49b6 100644 --- a/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/DashboardInput.tsx @@ -95,7 +95,7 @@ const DashboardInput = ({ <div className="mb-2 " /> <div className="relative"> <textarea - className={`h-40 w-full resize-none rounded-sm border-2 p-4 px-8 pr-40 text-lg`} + className={`min-h-40 w-full rounded-sm border-2 p-4 px-8 pr-40 text-lg`} placeholder="질문을 입력해주세요" value={questionInput} onChange={(e) => setQuestionInput(e.target.value)} From 7bed8e180022207037af1e1cb9c8af20eea43299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Mon, 2 Dec 2024 13:45:39 +0900 Subject: [PATCH 29/33] =?UTF-8?q?feat:=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EC=9D=98=20=EB=8C=80=EB=8C=93=EA=B8=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #146 --- .../feature/detail/Dashboard/components/ChatList.tsx | 4 ++-- .../feature/detail/Dashboard/components/ReplyChat.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx index 25f29337..7e9442ad 100644 --- a/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/ChatList.tsx @@ -68,9 +68,9 @@ const ChatList = ({ deleteComment={handleDeleteComment} handleReply={handleReply} time={time} - updateComment={handleUpdateComment} // + updateComment={handleUpdateComment} markdownStyle={markdownStyle} - showReplyButton={showReplyButton} // + showReplyButton={showReplyButton} /> ); }; diff --git a/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx b/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx index 65d02cf4..0aad78a7 100644 --- a/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx +++ b/FE/src/components/feature/detail/Dashboard/components/ReplyChat.tsx @@ -18,7 +18,7 @@ const ReplyChat = ({ commentId={commentId} accessRight={accessRight} markdownStyle="!bg-inherit" - showReplyButton={false} + showReplyButton={true} /> </div> ); From 5d9a28aa4b68c8110657c30f60004137b47a6ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= <geongyu09@gmail.com> Date: Mon, 2 Dec 2024 14:11:55 +0900 Subject: [PATCH 30/33] =?UTF-8?q?fix:=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EC=9D=98=20=EB=8C=80=EB=8C=93=EA=B8=80=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=B4=20=EC=83=9D=EA=B9=80=EC=97=90=20=EB=94=B0=EB=9D=BC?= =?UTF-8?q?=EC=84=9C=20=EB=8C=80=EB=8C=93=EA=B8=80=EC=9D=98=20=EB=8C=80?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=B6=94=EA=B0=80=EC=8B=9C=20=EC=98=AC?= =?UTF-8?q?=EB=B0=94=EB=A5=B4=EA=B2=8C=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=EA=B0=80=20=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/query/useQuestionQuery.ts | 10 ---------- FE/src/utils/question.ts | 24 +++++++++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/FE/src/hooks/query/useQuestionQuery.ts b/FE/src/hooks/query/useQuestionQuery.ts index b21a4e72..80076556 100644 --- a/FE/src/hooks/query/useQuestionQuery.ts +++ b/FE/src/hooks/query/useQuestionQuery.ts @@ -67,16 +67,6 @@ export const usePostQuestion = () => { newComments, ); - // console.log(newComments); - - // console.log( - // queryClient.getQueryData<QuestionListDto>([ - // "question", - // programId, - // teamId, - // ]) || { comments: [] }, - // ); - return oldData; }, onError: (_, { programId, teamId }, oldData) => { diff --git a/FE/src/utils/question.ts b/FE/src/utils/question.ts index 5035f271..d66790d8 100644 --- a/FE/src/utils/question.ts +++ b/FE/src/utils/question.ts @@ -5,31 +5,41 @@ export const makeNewQuestionData = ( newComment: Comment, newCommentParentId: number, ): QuestionListDto => { - let result: QuestionListDto; - // 일반 질문인 경우 if (newCommentParentId === -1) { const newComments = [...prevData.comments, newComment]; - result = { + return { comments: newComments, }; - return result; } // 답변인 경우 const newComments = prevData.comments.map((comment) => { + // 부모 댓글의 답변으로 추가 if (comment.commentId === newCommentParentId) { return { ...comment, answers: [...comment.answers, newComment], }; } + + // 해당 comment가 부모가 아닌 경우, 해당 comment의 답변을 추가로 살펴보고 만약 대댓글이 부모인 경우 + const isCommentOfComment = comment.answers.some((answer) => { + return answer.commentId === newCommentParentId; + }); + + // 대댓글의 대댓글인 경우 해당 대댓글에 추가 + if (isCommentOfComment) { + return { + ...comment, + answers: [...comment.answers, newComment], + }; + } + return comment; }); - result = { + return { comments: newComments, }; - - return result; }; From ba9a9257c96592015a73cf8e45e604dc1c987a69 Mon Sep 17 00:00:00 2001 From: Klomachenko <leekyumin0901@naver.com> Date: Tue, 3 Dec 2024 22:01:34 +0900 Subject: [PATCH 31/33] =?UTF-8?q?fix:=20polling=20issue=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refetchInterval로 해결하였습니다. 자세한 내용은 pr에 작성합니다. --- FE/src/hooks/query/useProgramQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index 130dfcf9..ddd7886b 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -112,7 +112,7 @@ export const useGetProgramByProgramId = ( }); return res; }), - staleTime: 1000 * 60, + refetchInterval: 1000 * 60, }); }; From c462d1f5335bb4f7166ab668d11c476d705178f4 Mon Sep 17 00:00:00 2001 From: Klomachenko <leekyumin0901@naver.com> Date: Tue, 3 Dec 2024 22:30:28 +0900 Subject: [PATCH 32/33] =?UTF-8?q?feat:=20onSuccess=20invalidateQueries=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/query/useProgramQuery.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index ddd7886b..7631bb80 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -147,24 +147,28 @@ export const useUpdateProgramAttendMode = (programId: number) => { const queryClient = useQueryClient(); return useMutation({ mutationKey: [API.PROGRAM.UPDATE_ATTEND_MODE(programId)], - mutationFn: (attendMode: ProgramAttendStatus) => { - queryClient.invalidateQueries([API.PROGRAM.Edit_DETAIL(programId)]); - return updateProgramAttendMode(programId, attendMode); - }, - onSuccess: (_, targetAttendMode) => { + mutationFn: (attendMode: ProgramAttendStatus) => + updateProgramAttendMode(programId, attendMode), + onMutate: (targetAttendMode) => { + queryClient.cancelQueries([API.PROGRAM.Edit_DETAIL(programId)]); const prevProgram = queryClient.getQueryData<ProgramInfoDto>([ API.PROGRAM.Edit_DETAIL(programId), ]); - const newProgram: ProgramInfoDto = { ...prevProgram, attendMode: targetAttendMode, }; - queryClient.setQueryData<ProgramInfoDto>( [API.PROGRAM.Edit_DETAIL(programId)], newProgram, ); + return prevProgram; + }, + onError: (_, __, context) => { + queryClient.setQueryData( + [API.PROGRAM.Edit_DETAIL(programId)], + context as ProgramInfoDto, + ); }, }); }; From d77c2784b4b9c8fa2ee86aab5dfa88a650801922 Mon Sep 17 00:00:00 2001 From: Klomachenko <leekyumin0901@naver.com> Date: Tue, 3 Dec 2024 22:43:02 +0900 Subject: [PATCH 33/33] =?UTF-8?q?fix:=20attendMode=20invalidateQueries=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/query/useProgramQuery.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/FE/src/hooks/query/useProgramQuery.ts b/FE/src/hooks/query/useProgramQuery.ts index ddd7886b..2147bf6d 100644 --- a/FE/src/hooks/query/useProgramQuery.ts +++ b/FE/src/hooks/query/useProgramQuery.ts @@ -147,24 +147,37 @@ export const useUpdateProgramAttendMode = (programId: number) => { const queryClient = useQueryClient(); return useMutation({ mutationKey: [API.PROGRAM.UPDATE_ATTEND_MODE(programId)], - mutationFn: (attendMode: ProgramAttendStatus) => { - queryClient.invalidateQueries([API.PROGRAM.Edit_DETAIL(programId)]); - return updateProgramAttendMode(programId, attendMode); - }, - onSuccess: (_, targetAttendMode) => { + mutationFn: (attendMode: ProgramAttendStatus) => + updateProgramAttendMode(programId, attendMode), + onMutate: (targetAttendMode) => { + queryClient.cancelQueries([API.PROGRAM.Edit_DETAIL(programId)]); const prevProgram = queryClient.getQueryData<ProgramInfoDto>([ API.PROGRAM.Edit_DETAIL(programId), ]); - const newProgram: ProgramInfoDto = { ...prevProgram, attendMode: targetAttendMode, }; - queryClient.setQueryData<ProgramInfoDto>( [API.PROGRAM.Edit_DETAIL(programId)], newProgram, ); + queryClient.setQueryData<ProgramAttendStatus>( + ["attendMode", programId], + targetAttendMode, + ); + return prevProgram; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [API.MEMBER.ATTEND_STATUS(programId)], + }); + }, + onError: (_, __, context) => { + queryClient.setQueryData( + [API.PROGRAM.Edit_DETAIL(programId)], + context as ProgramInfoDto, + ); }, }); };