오랜만에 쓰는 글

낚시하는 곰·2025년 6월 26일
1

jungle TIL

목록 보기
18/20

프레임워크 공부하면서 한참동안 블로그를 작성하지 못했다..
굳이 연습하는 코드를 블로그에 적으면 뭐하나 싶기도 했고..

chat Textarea

우선순위

  1. chat 서식에 img 첨부 가능하도록 기능 구현
  2. prettier extention 설치해서 은채가 올려준 guide보고 따라하기
  3. 팝업창 구현 어떻게 할 지 설계해보기

추가 구현해야 할 부분

  • window size에 따라 button 부분이 동적으로 동작되도록 구성해야 함.
  • back으로 보낼 api 추가
  • chat창에 서식 적용된 text를 어떻게 보여줄 지 고민해봐야 함.

수정해야 할 부분

우선순위

  • 미리보기 img 삭제 버튼
  • pdf file이 삽입되지 않는 문제 수정
  • chatting이 마우스 drag 시 drag된 부분이 보이지 않는 문제

file upload

우선순위

  • img 미리보기를 작은 size로 띄우기
  • file type과 file name 띄우기

chatArea 지금까지 구현한 부분

MyEditor()

export default function MyEditor() {
  // 텍스트 영역
  const [text, setText] = useState<string>("HelloWorld");
  const [files, setFiles] = useState<File[]>([]);

  // 파일 추가 시 확장되는 높이
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const editorRef = useRef<HTMLDivElement>(null);

  const editor = useEditor({
    immediatelyRender: false,
    extensions: [
      StarterKit,
      Link.configure({
        openOnClick: false,
      }),
    ],
    content: text,
    onUpdate: ({ editor }) => {
      setText(editor.getText());
    },
  });

  return (
    <div className="rounded-t-lg rounded-b-lg border-input border">
      <EditorContext.Provider value={{ editor }}>
        <div className="p-2 bg-muted/50">
          <div className="tiptap-button-group" data-orientation="horizontal">
            <MarkButton type="bold" />
            <MarkButton type="italic" />
            <MarkButton type="strike" />

            <LinkPopover />
            <ListButton type="orderedList" />
            <ListButton type="bulletList" />

            <BlockquoteButton />
            <MarkButton type="code" />
            <CodeBlockButton />
          </div>
        </div>
        <div className={`transition-all duration-300 ease-in`} ref={editorRef} style={{}}>
          <EditorContent
            editor={editor}
            role="presentation"
            className="bg-transparent rounded-b-md px-3 py-2 text-sm min-h-16 w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-none"
          />
        </div>
        <div>
          <CustomButton
            files={files}
            setFiles={setFiles}
            isOpen={isOpen}
            setIsOpen={setIsOpen}
          />
        </div>
      </EditorContext.Provider>
    </div>
  );
}

ImagePreview

export default function ImagePreview({ files }: ImagePreviewProps) {
  const [previews, setPreviews] = useState<string[]>([]);

  // 파일이 변경될 때마다 미리보기 생성
  useEffect(() => {
    const generatePreviews = async () => {
      const newPreviews = await Promise.all(
        files.map((file) => {
          return new Promise<string>((resolve) => {
            const reader = new FileReader();
            reader.onload = (e) => resolve(e.target?.result as string);
            reader.readAsDataURL(file);
          });
        }),
      );
      setPreviews(newPreviews);
    };

    generatePreviews();
  }, [files]);

  return (
    <div className="p-4">
      <div className="grid grid-cols-8 gap-4">
        {files.map((file, index) => (
          <div key={index} className="border rounded-lg">
            <img
              src={previews[index]}
              alt={file.name}
              className="w-20 h-20 object-cover rounded"
            />
          </div>
        ))}
      </div>
    </div>
  );
}

CustomButton

export default function CustomButton({
  files,
  setFiles,
  setIsOpen,
  isOpen,
}: {
  files: File[];
  setFiles: (files: File[]) => void;
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
}) {
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selected = Array.from(e.target.files || []);
    setFiles([...files, ...selected]);
    setIsOpen(true);
  };

  return (
    <div className="ml-5 mt-3">
      {/* 숨겨진 파일 입력 */}
      <input
        ref={fileInputRef}
        type="file"
        multiple
        accept="image/*"
        onChange={handleFileChange}
        className="hidden"
      />

      {/* 동그란 회색 버튼 */}
      <div>
        <button
          onClick={() => fileInputRef.current?.click()}
          className="w-[25px] h-[25px] rounded-full bg-gray-300 hover:bg-gray-500 active:bg-gray-700 transition-colors duration-200 shadow-md"
          title="파일 선택"
        >
          <Plus className="h-6 w-6 text-white" />
        </button>
      </div>

      {/* 이미지 미리보기 */}
      {files.length > 0 && <ImagePreview files={files} />}
    </div>
  );
}

결과

profile
취업 준비생 낚곰입니다!! 반갑습니다!!

0개의 댓글