Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

훈돌라

2024. 7. 31. 줄노트 속지 제작, supabase 연동 본문

카테고리 없음

2024. 7. 31. 줄노트 속지 제작, supabase 연동

훈돌라 2024. 7. 31. 21:07

 

'use client';

import { useState, useRef, useCallback, useEffect } from 'react';
import { supabase } from '@/supabase/client';
import { Json } from '@/types/supabase';
import { isNoteLineArray } from '@/stores/noteline.store';

export interface NoteLine {
  text: string;
  fontSize: number;
  textColor: string;
}

interface LineNoteProps {
  userId: string;
}

const LineNote: React.FC<LineNoteProps> = ({ userId }) => {
  const [lines, setLines] = useState<NoteLine[]>(
    Array.from({ length: 15 }, () => ({ text: '', fontSize: 16, textColor: '#000000' }))
  );
  const [lineColor, setLineColor] = useState('#000000');
  const [lineThickness, setLineThickness] = useState(1);
  const [bgColor, setBgColor] = useState('#ffffff');
  const [globalTextColor, setGlobalTextColor] = useState('#000000');
  const inputRefs = useRef<(HTMLInputElement | null)[]>([]);

  const measureTextWidth = useCallback((text: string, fontSize: number) => {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    if (context) {
      context.font = `${fontSize}px sans-serif`;
      return context.measureText(text).width;
    }
    return 0;
  }, []);

  const handleTextChange = useCallback(
    (index: number, newText: string) => {
      if (!inputRefs.current[index]) return;

      const inputWidth = inputRefs.current[index]!.offsetWidth;
      let textWidth = measureTextWidth(newText, lines[index].fontSize);

      const newLines = [...lines];
      newLines[index].text = newText;

      while (textWidth > inputWidth) {
        const overflowChar = newText.slice(-1);
        newText = newText.slice(0, -1);
        newLines[index].text = newText;
        textWidth = measureTextWidth(newText, lines[index].fontSize);

        const nextLineIndex = index + 1;
        if (nextLineIndex < newLines.length) {
          newLines[nextLineIndex].text = overflowChar + newLines[nextLineIndex].text;
          if (inputRefs.current[nextLineIndex]) {
            inputRefs.current[nextLineIndex]!.setSelectionRange(0, 0);
            inputRefs.current[nextLineIndex]!.focus();
          }
        }
      }

      if (newText.length === 0 && index > 0) {
        const prevLineIndex = index - 1;
        newLines[prevLineIndex].text += newLines[index].text;
        newLines[index].text = '';
        if (inputRefs.current[prevLineIndex]) {
          inputRefs.current[prevLineIndex]!.focus();
          inputRefs.current[prevLineIndex]!.setSelectionRange(
            newLines[prevLineIndex].text.length,
            newLines[prevLineIndex].text.length
          );
        }
      }

      setLines(newLines);
    },
    [lines, measureTextWidth]
  );

  useEffect(() => {
    inputRefs.current.forEach((input, index) => {
      if (input) {
        const inputWidth = input.offsetWidth;
        const textWidth = measureTextWidth(lines[index].text, lines[index].fontSize);
        if (textWidth > inputWidth) {
          handleTextChange(index, lines[index].text);
        }
      }
    });
  }, [lines, measureTextWidth, handleTextChange]);

  const saveData = async () => {
    if (!userId) {
      console.error('userId is undefined');
      return;
    }

    console.log('Saving data with user_id:', userId);
    const { data, error } = await supabase.from('line_note').upsert([
      {
        user_id: userId,
        line_color: lineColor,
        line_thickness: lineThickness,
        bg_color: bgColor,
        global_text_color: globalTextColor,
        lines: lines as unknown as Json
      }
    ]);

    if (error) {
      console.error('Error saving data:', error);
    } else {
      console.log('Data saved:', data);
    }
  };

  const loadData = async () => {
    if (!userId) {
      console.error('userId is undefined');
      return;
    }

    const { data, error } = await supabase.from('line_note').select('*').eq('user_id', userId).maybeSingle();

    if (error) {
      console.error('Error loading data:', error);
    } else if (data) {
      setLineColor(data?.line_color || '#000000');
      setLineThickness(data?.line_thickness || 1);
      setBgColor(data?.bg_color || '#ffffff');
      setGlobalTextColor(data?.global_text_color || '#000000');
      if (isNoteLineArray(data?.lines)) {
        setLines(data?.lines);
      } else {
        console.error('Invalid data format for lines');
      }
    } else {
      console.log('No data found for this user.');
    }
  };

  useEffect(() => {
    loadData();
  }, [userId]);

  return (
    <div className="p-4">
      <div className="flex justify-between mb-4 bg-white">
        <div>
          <label className="block m-2">
            줄 색상:
            <input
              type="color"
              value={lineColor}
              onChange={(e) => setLineColor(e.target.value)}
              className="ml-2 p-1 border"
            />
          </label>
          <label className="block m-2">
            줄 굵기:
            <select
              value={lineThickness}
              onChange={(e) => setLineThickness(parseInt(e.target.value))}
              className="ml-2 p-1 border"
            >
              <option value="1">Thin</option>
              <option value="2">Medium</option>
              <option value="3">Thick</option>
            </select>
          </label>
        </div>
        <div>
          <label className="block m-2">
            줄노트 배경 색상:
            <input
              type="color"
              value={bgColor}
              onChange={(e) => setBgColor(e.target.value)}
              className="ml-2 p-1 border"
            />
          </label>
          <label className="block m-2">
            전체 텍스트 색상:
            <input
              type="color"
              value={globalTextColor}
              onChange={(e) => setGlobalTextColor(e.target.value)}
              className="ml-2 p-1 border"
            />
          </label>
        </div>
      </div>
      <div
        className="border p-4 w-[500px]"
        style={{
          backgroundColor: bgColor,
          minHeight: '480px',
          boxShadow: '0 4px 8px rgba(0, 0, 0, 1)',
          borderRadius: '8px'
        }}
      >
        <div className="relative w-full overflow-hidden" style={{ height: '450px' }}>
          {lines.map((line, index) => (
            <div
              key={index}
              className={`relative`}
              style={{ height: '30px', borderBottom: `${lineThickness}px solid ${lineColor}` }}
            >
              <input
                type="text"
                value={line.text}
                onChange={(e) => handleTextChange(index, e.target.value)}
                ref={(el) => {
                  inputRefs.current[index] = el;
                }}
                className="absolute top-0 left-0 w-full border-none outline-none bg-transparent"
                style={{
                  fontSize: `${line.fontSize}px`,
                  color: globalTextColor
                }}
              />
            </div>
          ))}
        </div>
      </div>
      <button onClick={saveData} className="mt-4 p-2 bg-blue-500 text-white rounded">
        저장
      </button>
    </div>
  );
};

export default LineNote;

 

 

 

 

 

 

Type Guard 유틸 함수

import { NoteLine } from '@/components/molecules/parchment/LineNote';

export function isNoteLineArray(data: any): data is NoteLine[] {
  return (
    Array.isArray(data) &&
    data.every(
      (item) => typeof item.text === 'string' && typeof item.fontSize === 'number' && typeof item.textColor === 'string'
    )
  );
}

 

주어진 데이터가 NoteLine 타입의 배열인지 확인하는 타입 가드(type guard) 함수.

타입 가드는 TypeScript에서 조건문 내에서 타입을 좁히는 데 사용되는 함수.

 

이를 통해 데이터가 NoteLine 배열임을 확신하고, TypeScript에서 타입 오류 없이 데이터를 사용할 수 있음.