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
Expand Up @@ -59,7 +59,16 @@ export default function AddQuestionView({ onAddQuestion, onAddMultipleQuestions,
margin-top: 2.3rem;
`}
>
<TextArea placeholder="질문을 작성해주세요." value={customQuestion} onChange={handleCustomChange} maxLength={20} count />
<TextArea
placeholder="질문을 작성해주세요."
value={customQuestion}
onChange={handleCustomChange}
maxLength={100}
count
autoResize
minHeight="5.4rem"
maxHeight="20rem"
/>
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function AdvanceQuestions() {
padding: 1.5rem 1.2rem;
background-color: ${DESIGN_TOKEN_COLOR.gray100};
border-radius: 0.8rem;
height: 5rem;
`}
>
<Icon icon="ic_star_with_cirecle" />
Expand All @@ -39,6 +40,7 @@ export default function AdvanceQuestions() {
padding: 1.5rem 1.2rem;
background-color: ${DESIGN_TOKEN_COLOR.gray100};
border-radius: 0.8rem;
height: 5rem;
`}
>
<Icon icon="ic_star_with_cirecle" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
const { toast } = useToast();

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

/**
Expand All @@ -29,7 +30,14 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
*/
const handleContentChange = (index: number, newContent: string) => {
const updatedQuestions = questions.map((item, i) => (i === index ? { ...item, questionContent: newContent } : item));
setRetroCreateData((prev) => ({ ...prev, questions: updatedQuestions, isNewForm: true }));
setRetroCreateData((prev) => ({ ...prev, questions: updatedQuestions, isNewForm: true, formName: `커스텀 템플릿` }));

// textarea 높이 자동 조절
const textarea = textareaRefs.current[index];
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
};

/**
Expand Down Expand Up @@ -68,8 +76,14 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
useEffect(() => {
questions.forEach((question, index) => {
originalContentRef.current[index] = question.questionContent;

const textarea = textareaRefs.current[index];
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
});
}, []);
}, [questions]);

return (
<DragDropContext onDragEnd={handleDragEnd}>
Expand Down Expand Up @@ -99,7 +113,6 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
gap: 1.2rem;
margin-bottom: 1.2rem;
transition: box-shadow 0.2s ease;
height: 5rem;

${snapshot.isDragging &&
`
Expand Down Expand Up @@ -128,12 +141,16 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
</div>

{/* ---------- 입력 필드 ---------- */}
<input
<textarea
ref={(el) => {
textareaRefs.current[index] = el;
}}
value={item.questionContent}
onChange={(e) => handleContentChange(index, e.target.value)}
onFocus={() => handleContentFocus(index)}
onBlur={() => handleContentBlur(index)}
placeholder="질문을 입력해주세요"
rows={1}
css={css`
flex-grow: 1;
background: transparent;
Expand All @@ -142,6 +159,10 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
font-size: 1.4rem;
font-weight: 500;
color: ${DESIGN_TOKEN_COLOR.gray900};
resize: none;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;

&::placeholder {
color: ${DESIGN_TOKEN_COLOR.gray500};
Expand Down
54 changes: 48 additions & 6 deletions apps/web/src/component/common/input/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css } from "@emotion/react";
import { forwardRef, useContext, useState } from "react";
import { forwardRef, useContext, useState, useRef, useEffect, useCallback } from "react";

import { InputContext } from "./InputLabelContainer";
import { patterns } from "./patterns.const";
Expand All @@ -12,20 +12,59 @@ type TextAreaProps = {
value: string;
width?: string;
height?: string;
minHeight?: string;
maxHeight?: string;
autoResize?: boolean;
count?: boolean;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
validations?: (keyof typeof patterns)[];
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;

export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function (
{ id, width = "100%", height = "7.4rem", count, validations, onChange, ...props },
{ id, width = "100%", height = "7.4rem", minHeight = "5.4rem", maxHeight = "20rem", autoResize = false, count, validations, onChange, ...props },
ref,
) {
const { maxLength, value } = props;
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const textareaContext = useContext(InputContext);
const { errorMsg, onInputValidate } = useValidation({ validations, maxLength });

// * 두 개의 ref를 결합하는 callback ref
const setRefs = useCallback(
(element: HTMLTextAreaElement | null) => {
textareaRef.current = element;

if (typeof ref === "function") {
ref(element);
} else if (ref) {
ref.current = element;
}
},
[ref],
);

// * 텍스트에 따른 높이 자동 조절
useEffect(() => {
if (autoResize && textareaRef.current) {
const textarea = textareaRef.current;

// 높이를 리셋하여 scrollHeight를 정확히 측정
textarea.style.height = "inherit";

const scrollHeight = textarea.scrollHeight;

// rem을 px로 변환
const minHeightPx = parseFloat(minHeight.replace("rem", "")) * 10;
const maxHeightPx = parseFloat(maxHeight.replace("rem", "")) * 10;

// scrollHeight와 최소/최대 높이를 비교
const calculatedHeight = Math.max(minHeightPx, Math.min(scrollHeight, maxHeightPx));

textarea.style.height = `${calculatedHeight}px`;
}
}, [value, autoResize, minHeight, maxHeight]);

return (
<div>
<div
Expand All @@ -37,16 +76,19 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
padding: 1.6rem 1.4rem;
display: flex;
flex-direction: column;
height: ${height};
transition: 0.2s all;
min-height: ${autoResize ? minHeight : height};
max-height: ${autoResize ? maxHeight : "none"};
transition: border-color 0.2s;
`}
>
<textarea
ref={ref}
ref={setRefs}
id={id || textareaContext?.id}
css={css`
flex-grow: 1;
overflow: auto;
overflow-y: auto;
resize: none;
height: ${autoResize ? "auto" : "100%"};
::placeholder {
color: ${DESIGN_TOKEN_COLOR["gray500"]};
${DESIGN_TOKEN_TEXT["body15Medium"]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@ export function AddQuestionsBottomSheet({ onClose, handleAddQuestions, maxCount
margin-top: 2.3rem;
`}
>
<TextArea placeholder="질문을 작성해주세요." value={customQuestion} onChange={handleCustomChange} maxLength={20} count />
<TextArea
placeholder="질문을 작성해주세요."
value={customQuestion}
onChange={handleCustomChange}
maxLength={100}
count
autoResize
minHeight="5.4rem"
maxHeight="20rem"
/>
<ButtonProvider>
<ButtonProvider.Primary onClick={handleCustomSave} disabled={!customQuestion}>
추가하기
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from "@emotion/react";
import { useSetAtom } from "jotai";
import { useContext, useMemo, useState } from "react";
import { useContext, useMemo, useState, useRef, useEffect } from "react";
import { DragDropContext } from "react-beautiful-dnd";

import { REQUIRED_QUESTIONS } from "./questions.const";
Expand All @@ -27,6 +27,7 @@ import { DESIGN_TOKEN_COLOR, DESIGN_TOKEN_TEXT } from "@/style/designTokens";
import { DESIGN_SYSTEM_COLOR } from "@/style/variable";

const MAX_QUESTIONS_COUNT = 10;
const SHEET_ID = "addQuestionSheet";

type EditQuestionsProps = Pick<ReturnType<typeof useMultiStepForm>, "goNext" | "goPrev">;

Expand All @@ -51,7 +52,8 @@ export function EditQuestions({ goNext, goPrev }: EditQuestionsProps) {

const { questions: originalQuestions } = useContext(TemplateContext);
const setRetroCreateData = useSetAtom(retrospectCreateAtom);
const SHEET_ID = "addQuestionSheet";

const textareaRefs = useRef<{ [key: number]: HTMLTextAreaElement | null }>({});

const [isTemporarySaveModalOpen, setIsTemporarySaveModalOpen] = useState(false);
const isEdited = useMemo(
Expand Down Expand Up @@ -93,6 +95,20 @@ export function EditQuestions({ goNext, goPrev }: EditQuestionsProps) {
}
};

const adjustTextareaHeight = (index: number) => {
const textarea = textareaRefs.current[index];
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
};

useEffect(() => {
newQuestions.forEach((_, index) => {
adjustTextareaHeight(index);
});
}, [newQuestions]);

return (
<div
css={css`
Expand Down Expand Up @@ -185,13 +201,27 @@ export function EditQuestions({ goNext, goPrev }: EditQuestionsProps) {
}
}}
>
<input
<textarea
ref={(el) => {
textareaRefs.current[index] = el;
}}
value={question}
onChange={(e) => handleQuestionInputChange(e, index)}
onChange={(e) => {
handleQuestionInputChange(e, index);
adjustTextareaHeight(index);
}}
rows={1}
css={css`
flex-grow: 1;
${DESIGN_TOKEN_TEXT.body14Medium};
color: ${DESIGN_TOKEN_COLOR.gray900};
background: transparent;
border: none;
outline: none;
resize: none;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
`}
onBlur={handleInputChangeConfirm}
/>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/hooks/useEditQuestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useEditQuestions = () => {
setTemporarilyDeletedIndexes([]);
};

const handleQuestionInputChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
const handleQuestionInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, index: number) => {
setNewQuestions((prev) => {
const newQuestions: Questions = [...prev];
newQuestions[index] = {
Expand Down