Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion apps/web/src/app/desktop/component/home/RetrospectCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { formatDateAndTime } from "@/utils/date";
import { ProceedingTextBox } from "@/component/space/view/ProceedingTextBox";
import { useNavigate } from "react-router-dom";
import { PATHS } from "@layer/shared";
import { useFunnelModal } from "@/hooks/useFunnelModal";
import { Prepare } from "../../retrospectWrite/prepare";

interface RetrospectCardProps {
retrospect: Retrospect;
Expand All @@ -15,12 +17,23 @@ interface RetrospectCardProps {

export default function RetrospectCard({ retrospect, spaceId }: RetrospectCardProps) {
const navigate = useNavigate();
const { openFunnelModal } = useFunnelModal();

const { retrospectId, title, introduction, deadline, totalCount, writeCount, writeStatus, analysisStatus, retrospectStatus } = retrospect;
const { retrospectId, title, introduction, deadline, totalCount, writeCount, writeStatus, retrospectStatus, analysisStatus } = retrospect;

const handleCardClick = () => {
// TODO: spaceId가 없는 경우 처리(예: 홈 화면 최상단의 카드 클릭 시)

// 진행중인 회고 클릭 시
if (spaceId && retrospectStatus === "PROCEEDING") {
openFunnelModal({
title: "",
step: "retrospectWrite",
contents: <Prepare spaceId={Number(spaceId)} retrospectId={retrospect.retrospectId} title={title} introduction={introduction} />,
});
}

// 마감된 회고 클릭 시
if (spaceId && retrospectStatus === "DONE") {
navigate(PATHS.retrospectAnalysis(spaceId, retrospectId, title));
}
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/app/desktop/component/retrospectCreate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { retrospectInitialState } from "@/store/retrospect/retrospectInitial";
import { MainInfo } from "./steps/MainInfo";
import { DueDate } from "./steps/DueDate";
import { ConfirmDefaultTemplate } from "./steps/ConfirmDefaultTemplate";
import { PATHS } from "@layer/shared";
import { useNavigate } from "react-router-dom";
import { useFunnelModal } from "@/hooks/useFunnelModal";
import { useToast } from "@/hooks/useToast";

const PAGE_STEPS = ["confirmTemplate", "mainInfo", "dueDate"] as const;
const CUSTOM_TEMPLATE_STEPS = ["confirmDefaultTemplate", "editQuestions", "confirmEditTemplate"] as const;
Expand All @@ -26,6 +30,9 @@ type RetrospectCreateContextState = UseMultiStepFormContextState<(typeof PAGE_ST
export const RetrospectCreateContext = createContext<RetrospectCreateContextState>({} as RetrospectCreateContextState);

export function RetrospectCreate() {
const navigate = useNavigate();
const { closeFunnelModal } = useFunnelModal();
const { toast } = useToast();
const { spaceId, templateId } = useAtomValue(retrospectInitialState);
const spaceIdNumber = Number(spaceId);
const templateIdNumber = Number(templateId);
Expand All @@ -40,10 +47,19 @@ export function RetrospectCreate() {
const handleSubmit = useCallback(() => {
if (!pageState.isLastStep) return;
const questionsWithRequired = REQUIRED_QUESTIONS.concat(retroCreateData.questions);
postRetrospectCreate({
spaceId: spaceIdNumber,
body: { ...retroCreateData, questions: questionsWithRequired, curFormId: templateIdNumber },
});
postRetrospectCreate(
{
spaceId: spaceIdNumber,
body: { ...retroCreateData, questions: questionsWithRequired, curFormId: templateIdNumber },
},
{
onSuccess: () => {
navigate(PATHS.DesktopcompleteRetrospectCreate(String(spaceIdNumber)));
closeFunnelModal();
toast.success("회고가 생성되었어요!");
},
},
);
}, [retroCreateData.deadline]);

return (
Expand Down
217 changes: 217 additions & 0 deletions apps/web/src/app/desktop/component/retrospectWrite/complete/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { css, keyframes } from "@emotion/react";
import Lottie from "lottie-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { CompleteCheck, CompleteIcon } from "@/assets/imgs/write/template";
import confetti from "@/assets/lottie/template/confetti.json";
import { Button, ButtonProvider } from "@/component/common/button";
import { HeaderProvider } from "@/component/common/header";
import { LoadingModal } from "@/component/common/Modal/LoadingModal.tsx";
import { useApiGetSpacePrivate } from "@/hooks/api/space/useGetSpace.ts";
import { ANIMATION } from "@/style/common/animation.ts";
import { PATHS } from "@layer/shared";
import useDesktopBasicModal from "@/hooks/useDesktopBasicModal";

type UserInfoType = {
isLogin: boolean;
name: string;
email: string;
memberRole: string;
};

export function RetrospectWriteComplete() {
const navigate = useNavigate();
const { close } = useDesktopBasicModal();
const [searchParams] = useSearchParams();
const spaceId = searchParams.get("spaceId");

const { data, isLoading } = useApiGetSpacePrivate(Number(spaceId));
const isTeam = data?.category === "TEAM";
const completeWord = isTeam ? "모든 인원의 회고가 제출되면 AI 분석을 시작해요" : "회고가 제출되면 AI 분석을 시작해요";

const [isAnimation, setAnimation] = useState(false);
const [userInfo, _] = useState(() => {
const storedUserInfo = localStorage.getItem("auth");
return storedUserInfo ? (JSON.parse(storedUserInfo) as UserInfoType) : null;
});
const CARD_ANIMATION = {
FIRST_CARD: keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(-8deg);
}
`,
SECOND_CARD: keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(-16deg);
}
`,
};

useEffect(() => {
const timer = setTimeout(() => {
setAnimation(true);
}, 700);

return () => clearTimeout(timer);
}, []);

return (
<>
{isLoading && <LoadingModal />}
{!userInfo && <LoadingModal />}
<div
css={css`
height: 100%;
padding: 0 1.8rem;
`}
>
<HeaderProvider>
{/* FIXME: 추후 API 연동 후 닉네임 값이 들어와야해요 */}
<HeaderProvider.Subject contents={`${userInfo ? userInfo.name : null}님의\n회고가 제출되었어요!`} />
<HeaderProvider.Description contents={completeWord} />
</HeaderProvider>
<div
css={css`
padding-top: 2.2rem;
width: 100%;
height: 100%;
`}
>
<div
css={css`
width: 20rem;
height: auto;
aspect-ratio: 2 / 2.6;

display: flex;
align-items: center;
justify-content: center;

position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: ${ANIMATION.FADE_IN} 0.8s ease-in-out;

div {
width: 100%;
height: 100%;
border-radius: 0.8rem;
aspect-ratio: auto;
position: absolute;
transition: 0.4s all;
}

div:nth-of-type(1) {
background-color: #243753;
z-index: 3;
position: relative;
}

div:nth-of-type(2) {
background-color: #c4d7fd;
z-index: 2;
transform: rotate(-8deg);
animation: ${CARD_ANIMATION.FIRST_CARD} 0.6s ease-in-out;
}

div:nth-of-type(3) {
background-color: #a7c4fc;
z-index: 1;
transform: rotate(-16deg);
animation: ${CARD_ANIMATION.SECOND_CARD} 0.8s ease-in-out;
}
`}
>
<div
id="card-1"
css={css`
position: relative;
`}
>
<CompleteCheck
css={css`
position: absolute;
top: -10%;
right: -5%;
width: 20%;
height: auto;
z-index: 4;
`}
/>
<div
css={css`
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;

display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
row-gap: 2.5rem;
`}
>
<CompleteIcon
css={css`
width: 60%;
height: auto;
margin-top: 2rem;
`}
/>
<span
css={css`
color: white;
font-size: 1.9rem;
font-weight: 500;
display: flex;
justify-content: center;
`}
>
{userInfo ? userInfo.name : null}님의 회고
</span>
</div>
</div>
<div id="card-2"></div>
<div id="card-3"></div>
</div>

{isAnimation && (
<Lottie
animationData={confetti}
loop={false}
id="confetti"
css={css`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
height: 80%;
z-index: -1;
`}
/>
)}
</div>
<ButtonProvider>
<Button
onClick={() => {
close();
navigate(PATHS.DesktopcompleteRetrospectCreate(String(spaceId)));
}}
>
완료
</Button>
</ButtonProvider>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useContext } from "react";

import { Answer } from "@/component/write/phase/Write";
import { CAchievementTemplate, CDescriptiveTemplate, CSatisfactionTemplate } from "@/component/write/template/complete";
import { PhaseContext } from "..";
import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";
import { css } from "@emotion/react";
import { Typography } from "@/component/common/typography";

interface ResultLayoutProps {
children: React.ReactNode;
key?: number;
}

function ResultLayout({ children, key }: ResultLayoutProps) {
return (
<div
key={key}
css={css`
width: 100%;
min-height: 15.3rem;
padding: 1.6rem;
border-radius: 1.2rem;
background-color: ${DESIGN_TOKEN_COLOR.gray100};
`}
>
{children}
</div>
);
}

type CompleteProps = {
answers: Answer[];
};

export function Confirm({ answers }: CompleteProps) {
const { data } = useContext(PhaseContext);

// 사전 질문 (만족도, 달성률)
const satisfactionQuestion = data.questions[0];
const achievementQuestion = data.questions[1];

// 일반 질문들 (plain_text)
const mainQuestions = data.questions.slice(2);

return (
<>
<div
css={css`
display: flex;
align-items: stretch;
gap: 1.6rem;
margin-top: 2.4rem;
`}
>
{/* --------- 진행상황 만족도 --------- */}
<ResultLayout>
<Typography variant="body15Normal">{satisfactionQuestion?.question}</Typography>
{satisfactionQuestion && <CSatisfactionTemplate question={satisfactionQuestion.question} index={parseInt(answers[0]?.answerContent)} />}
</ResultLayout>

{/* --------- 목표 달성률 --------- */}
<ResultLayout>
<Typography variant="body15Normal">{achievementQuestion?.question}</Typography>
{achievementQuestion && <CAchievementTemplate question={achievementQuestion.question} index={parseInt(answers[1]?.answerContent)} />}
</ResultLayout>
</div>

{/* ---------- 메인 질문들 ---------- */}
<div
css={css`
display: flex;
flex-direction: column;
gap: 1.6rem;
margin-top: 1.6rem;
padding-bottom: 2.4rem;
`}
>
{mainQuestions.map((mainQuestion, index) => (
<ResultLayout key={index}>
<Typography variant="body15Normal">{`${index + 1}. ${mainQuestion.question}`}</Typography>
{achievementQuestion && <CDescriptiveTemplate answer={answers[mainQuestion.order]?.answerContent} />}
</ResultLayout>
))}
</div>
</>
);
}
Loading