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

'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에서 타입 오류 없이 데이터를 사용할 수 있음.