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/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; 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/apis/question.ts b/FE/src/apis/question.ts index af74d3dd..e5ca7b1d 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); }; +/** + * 질문을 등록합니다. + * - isAnonymous : 질문을 등록할 때 체크박스를 체크했는지 여부. 체크가 되었다면 1, 아니라면 0 + */ export interface PostQuestionParams { programId: number; teamId: number; questionContent: string; parentsCommentId?: number; + commentType: "ANONYMOUS" | "NON_ANONYMOUS"; } export const postQuestion = async ({ programId, teamId, questionContent, parentsCommentId = -1, + commentType = "NON_ANONYMOUS", }: PostQuestionParams) => { return await https({ url: API.QUESTION.CREATE, method: "POST", - data: { programId, teamId, content: questionContent, parentsCommentId }, + data: { + programId, + teamId, + content: questionContent, + parentsCommentId, + commentType, + }, }); }; diff --git a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx index 4eb29799..3cce624d 100644 --- a/FE/src/app/(admin)/admin/detail/[programId]/page.tsx +++ b/FE/src/app/(admin)/admin/detail/[programId]/page.tsx @@ -1,5 +1,9 @@ -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"; +import ProgramattendModeManageSection from "@/components/feature/detail/program/ProgramAttendStatusManageSection"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; +import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { params: { @@ -12,7 +16,15 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return (
- +
+ + + + +
+ +
+
); diff --git a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx index 0701336d..822373a3 100644 --- a/FE/src/app/(guest)/guest/detail/[programId]/page.tsx +++ b/FE/src/app/(guest)/guest/detail/[programId]/page.tsx @@ -1,6 +1,9 @@ -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"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; +import BlurDashboard from "@/components/feature/detail/Dashboard/BlurDashboard"; interface ProgramDetailPageProps { params: { @@ -13,10 +16,18 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return (
- +
+ + + +
+ +
+
); }; + 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 08621ea5..8ead99d2 100644 --- a/FE/src/app/(private)/(program)/detail/[programId]/page.tsx +++ b/FE/src/app/(private)/(program)/detail/[programId]/page.tsx @@ -1,6 +1,9 @@ -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"; +import ProgramPresentationsSection from "@/components/feature/detail/presentation/ProgramPresentationsSection"; +import ProgramDashboardSection from "@/components/feature/detail/Dashboard/ProgramDashboardSection"; interface ProgramDetailPageProps { params: { @@ -13,10 +16,18 @@ const ProgramDetailPage = ({ params }: ProgramDetailPageProps) => { return (
- +
+ + + +
+ +
+
); }; + export default ProgramDetailPage; diff --git a/FE/src/components/common/CheckBox/CheckBox.tsx b/FE/src/components/common/CheckBox/CheckBox.tsx index 2da6b92a..253ab649 100644 --- a/FE/src/components/common/CheckBox/CheckBox.tsx +++ b/FE/src/components/common/CheckBox/CheckBox.tsx @@ -1,23 +1,32 @@ +"use client"; + import classNames from "classnames"; import Image from "next/image"; interface CheckBoxProps { checked: boolean; - onClick: () => void; + 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 = () => { - !disabled && onClick(); + !disabled && onClick && onClick(); }; return ( 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 ( + <> +
+

{writer}

+
+
+ {!isModifyMode && ( + + )} +
+ {isModifyMode && ( +