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
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { ButtonProvider } from "@/component/common/button";
import { TextArea } from "@/component/common/input";
import { TabButton } from "@/component/common/tabs/TabButton";
import { Tabs } from "@/component/common/tabs/Tabs";
import { css } from "@emotion/react";
import { useTabs } from "@/hooks/useTabs";
import { useInput } from "@/hooks/useInput";
import { QUESTION_TYPES, RECOMMENDED_QUESTIONS } from "@/component/retrospectCreate/customTemplate/questions.const";
import { TagButton } from "@/component/common/tabs/TagButton";
import { CheckBoxGroup } from "@/component/common/checkBox";
import { useCheckBox } from "@/hooks/useCheckBox";
import { QuestionItemCheckbox } from "@/component/retrospectCreate";
import { useToast } from "@/hooks/useToast";
import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";

type AddQuestionViewProps = {
onAddQuestion: (content: string) => void;
onAddMultipleQuestions: (contents: string[]) => void;
maxCount: number;
};

export default function AddQuestionView({ onAddQuestion, onAddMultipleQuestions, maxCount }: AddQuestionViewProps) {
const { toast } = useToast();
const { tabs, curTab, selectTab } = useTabs(["직접작성", "추천질문"] as const);
const { value: customQuestion, handleInputChange: handleCustomChange, resetInput } = useInput();
const { tabs: categoryTabs, curTab: curCategoryTab, selectTab: selectCategoryTab } = useTabs(QUESTION_TYPES);
const { selectedValues, isChecked, toggle } = useCheckBox();

const handleDirectAdd = () => {
if (customQuestion.trim()) {
onAddQuestion(customQuestion);
resetInput();
}
};

const handleRecommendedAdd = () => {
if (selectedValues.length > 0) {
onAddMultipleQuestions(selectedValues);
}
};

return (
<div
css={css`
display: flex;
flex-direction: column;
height: 100%;
`}
>
{/* 탭 영역 */}
<Tabs tabs={tabs} curTab={curTab} selectTab={selectTab} TabComp={TabButton} />

{curTab === "직접작성" && (
<div
css={css`
flex-grow: 1;
display: flex;
flex-direction: column;
margin-top: 2.3rem;
`}
>
<TextArea placeholder="질문을 작성해주세요." value={customQuestion} onChange={handleCustomChange} maxLength={20} count />
</div>
)}

{curTab === "추천질문" && (
<div
css={css`
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
`}
>
<Tabs
tabs={categoryTabs}
curTab={curCategoryTab}
selectTab={selectCategoryTab}
TabComp={TagButton}
containerStyles={css`
flex-shrink: 0;
display: flex;
overflow-x: auto;
gap: 0.8rem;
margin: 2.3rem 0;
`}
/>

<div
css={css`
width: 100%;
padding: 1.6rem 0;
overflow: auto;
`}
>
<CheckBoxGroup
isChecked={isChecked}
onChange={(value) => {
if (!isChecked(value) && selectedValues.length >= maxCount) {
toast.error("추가 가능한 질문 개수를 초과했어요");
return;
}
toggle(value);
}}
gap={4}
>
{RECOMMENDED_QUESTIONS[curCategoryTab].map((question, index) => {
return (
<QuestionItemCheckbox value={question} key={index}>
{question}
</QuestionItemCheckbox>
);
})}
</CheckBoxGroup>
</div>
</div>
)}

<ButtonProvider
onlyContainerStyle={css`
padding: 0;
`}
>
<ButtonProvider.Primary onClick={curTab === "직접작성" ? handleDirectAdd : handleRecommendedAdd}>
{selectedValues.length > 0 ? (
<span>
추가하기{" "}
<span
css={css`
color: ${DESIGN_TOKEN_COLOR.blue600};
`}
>
{selectedValues.length}
</span>
</span>
) : (
"추가하기"
)}
</ButtonProvider.Primary>
</ButtonProvider>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Icon } from "@/component/common/Icon";
import { Spacing } from "@/component/common/Spacing";
import { Typography } from "@/component/common/typography";
import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";
import { css } from "@emotion/react";

export default function AdvanceQuestions() {
return (
<section>
<Typography variant="title16Bold">사전 질문</Typography>
<Spacing size={1.2} />
<section
css={css`
display: flex;
flex-direction: column;
gap: 1.2rem;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1.5rem 1.2rem;
background-color: ${DESIGN_TOKEN_COLOR.gray100};
border-radius: 0.8rem;
`}
>
<Icon icon="ic_star_with_cirecle" />
<Typography variant="body14SemiBold" color="gray700">
진행상황에 대해 얼마나 만족하나요?
</Typography>
</div>
<div
css={css`
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1.5rem 1.2rem;
background-color: ${DESIGN_TOKEN_COLOR.gray100};
border-radius: 0.8rem;
`}
>
<Icon icon="ic_star_with_cirecle" />
<Typography variant="body14SemiBold" color="gray700">
목표했던 부분에 얼마나 달성했나요?
</Typography>
</div>
</section>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { Icon } from "@/component/common/Icon";
import { useToast } from "@/hooks/useToast";
import { retrospectCreateAtom } from "@/store/retrospect/retrospectCreate";
import { DESIGN_TOKEN_COLOR } from "@/style/designTokens";
import { Questions } from "@/types/retrospectCreate";
import { css } from "@emotion/react";
import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd";
import { useSetAtom } from "jotai";
import { useEffect, useRef } from "react";

type MainQuestionsContentsProps = {
questions: Questions;
isDeleteMode: boolean;
handleDelete: (index: number) => void;
handleDragEnd: (result: any) => void;
};

export default function MainQuestionsContents({ questions, isDeleteMode, handleDelete, handleDragEnd }: MainQuestionsContentsProps) {
const { toast } = useToast();

const originalContentRef = useRef<{ [key: number]: string }>({});
const setRetroCreateData = useSetAtom(retrospectCreateAtom);

/**
* 질문 내용 변경 핸들러
*
* @param index - 질문 인덱스
* @param newContent - 새로운 질문 내용
*/
const handleContentChange = (index: number, newContent: string) => {
const updatedQuestions = questions.map((item, i) => (i === index ? { ...item, questionContent: newContent } : item));
setRetroCreateData((prev) => ({ ...prev, questions: updatedQuestions }));
};

/**
* 입력 필드 포커스 시 원본 내용 저장
*
* @param index - 질문 인덱스
*/
const handleContentFocus = (index: number) => {
originalContentRef.current[index] = questions[index]?.questionContent || "";
};

/**
* 입력 필드 blur 시 변경 확인 및 토스트 표시
*
* * 내용이 실제로 변경되고, 빈 문자열이 아닌 경우에만 토스트 표시
* * 현재 내용을 새로운 원본으로 업데이트
*
* @param index - 질문 인덱스
*/
const handleContentBlur = (index: number) => {
const currentContent = questions[index]?.questionContent || "";
const originalContent = originalContentRef.current[index] || "";

if (originalContent !== currentContent && currentContent.trim() !== "") {
toast.success("질문이 수정되었어요!");
}

originalContentRef.current[index] = currentContent;
};

// * 컴포넌트 마운트 시 원본 내용 저장
useEffect(() => {
questions.forEach((question, index) => {
originalContentRef.current[index] = question.questionContent;
});
}, [questions]);

return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="droppableQuestions">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
css={css`
display: flex;
flex-direction: column;
`}
>
{questions.map((item, index) => (
<Draggable key={`question-${index}`} draggableId={`question-${index}`} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
css={css`
position: relative;
background: ${DESIGN_TOKEN_COLOR.gray100};
padding: 1.5rem 1.2rem;
border-radius: 0.8rem;
display: flex;
align-items: center;
gap: 1.2rem;
margin-bottom: 1.2rem;
transition: box-shadow 0.2s ease;
height: 5rem;

${snapshot.isDragging &&
`
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
transform: rotate(2deg);
`}
`}
>
{/* ---------- 순서 번호 ---------- */}
<div
css={css`
background-color: ${DESIGN_TOKEN_COLOR.gray700};
color: white;
border-radius: 50%;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 600;
flex-shrink: 0;
`}
>
{index + 1}
</div>

{/* ---------- 입력 필드 ---------- */}
<input
value={item.questionContent}
onChange={(e) => handleContentChange(index, e.target.value)}
onFocus={() => handleContentFocus(index)}
onBlur={() => handleContentBlur(index)}
placeholder="질문을 입력해주세요"
css={css`
flex-grow: 1;
background: transparent;
border: none;
outline: none;
font-size: 1.4rem;
font-weight: 500;
color: ${DESIGN_TOKEN_COLOR.gray900};

&::placeholder {
color: ${DESIGN_TOKEN_COLOR.gray500};
}
`}
/>

{isDeleteMode ? (
/* ---------- 삭제 버튼 ---------- */
<Icon
icon="ic_delete"
color={DESIGN_TOKEN_COLOR.red400}
size={1.8}
onClick={() => handleDelete(index)}
css={css`
cursor: pointer;
&:hover {
opacity: 0.7;
transition: opacity 0.2s ease-in-out;
}
`}
/>
) : (
/* ---------- 드래그 핸들 ---------- */
<div
{...provided.dragHandleProps}
css={css`
cursor: grab;
display: flex;
align-items: center;
padding: 0.4rem;

&:active {
cursor: grabbing;
}
`}
>
<Icon icon="ic_handle" color={DESIGN_TOKEN_COLOR.gray400} size="1.8rem" />
</div>
)}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}
Loading