🧡 토요일
individual actioin add페이지 레이아웃 완성
community 레이아웃 대강 완성
community 디테일 페이지 고민
parallell routes
, intercepting routes
개념이 필요하다는걸 알게됨community 글쓰기 모달창 구현시도
next ui
도 provider
설정이 필요하다는걸 알게됨📝 next UI 초기 설정하기
// Provider-NextUi.tsx (root폴더에 생성)
"use client";
import * as React from "react";
import { NextUIProvider } from "@nextui-org/react";
const ProviderNextUi = ({ children }: { children: React.ReactNode }) => {
return <NextUIProvider>{children}</NextUIProvider>;
};
export default ProviderNextUi;
// layout.tsx (root)
<main>
<ProviderNextUi>
<QueryProvider>
{children}
{modal}
</QueryProvider>
</ProviderNextUi>
</main>
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
const { nextui } = require("@nextui-org/react");
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// ⭐️⭐️ 아래 "./node_modules~" 추가
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
btn: {
background: "hsl(var(--btn-background))",
"background-hover": "hsl(var(--btn-background-hover))",
},
},
},
darkMode: "class",
// plugins: [nextui()] 처음에 여기다가 잘못 적음 ㅠㅠ
},
// ⭐️⭐️ plugins에 nexiui 추가
plugins: [nextui()],
};
🧡 일요일
// ImgUpload.tsx (_components/individualAction-add)
"use client";
import { ImgUploadProps } from "@/app/_types/individualAction-add/individualAction-add";
import { Button } from "@nextui-org/react";
import React, { useState } from "react";
const ImgUpload = ({
uploadedFileUrls,
setUploadedFileUrls,
files,
setFiles,
}: ImgUploadProps) => {
// ⭐️⭐️ 이미지 미리보기 띄우기
const handleShowPreview = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
const imageUrl = URL.createObjectURL(file);
setUploadedFileUrls((prev) => [...prev, imageUrl]);
setFiles((prev) => [...prev, file]);
};
// ⭐️⭐️ 미리보기 이미지 삭제
const handleDeleteImage = (index: number) => {
setUploadedFileUrls((prev) => {
const updatedUrls = [...prev];
updatedUrls.splice(index, 1);
return updatedUrls;
});
setFiles((prev) => {
const updateFiles = [...prev];
updateFiles.splice(index, 1);
return updateFiles;
});
};
return (
<>
<div className="flex gap-2 w-full h-auto mb-8">
{[...Array(4)].map((_, index) => (
<div
key={index}
className="flex border-2 border-dashed border-gray-300 rounded-3xl w-1/4 h-[200px]"
>
{/* 이미지 업로드한 경우 */}
// 참고) uploadFileUrls는 이미지 url배열
{uploadedFileUrls[index] ? (
<div className="relative w-full h-full">
<img
src={uploadedFileUrls[index]}
alt={`Uploaded Image ${index}`}
className="w-full h-full rounded-3xl object-cover"
/>
<Button
onClick={() => handleDeleteImage(index)}
color="default"
className="absolute top-1 right-3 w-4"
>
x
</Button>
</div>
) : (
// 보여줄 이미지 없는 경우
<div className="flex flex-col w-full h-full justify-end items-center mt-auto">
<label
htmlFor={`fileInput-${index}`}
className="mb-4 text-4xl font-thin text-gray-500 cursor-pointer"
>
+
</label>
<input
id={`fileInput-${index}`}
type="file"
accept=".png, .jpg, .jpeg"
hidden
onChange={handleShowPreview}
/>
<p className="mb-px font-medium text-gray-500">Upload Image</p>
<p className="text-xs mb-8 text-gray-400">or drag & drop</p>
</div>
)}
</div>
))}
</div>
</>
);
};
export default ImgUpload;
이미지, 텍스트 formData를 어떻게 supabase에 업로드할지, 스토리지에는 어떻게 업로드할지 고민, 로직 연구
텍스트 formData insert
// add-api.ts (_api/ individualAction-add)
// 1. 텍스트 formData 삽입 함수
export const insertAction = async ({
formData,
currentUserUId,
}: {
formData: FormData;
currentUserUId: string;
}) => {
try {
// insert할 텍스트 데이터
const inputData = {
user_uid: currentUserUId,
title: formData.get("activityTitle"),
content: formData.get("activityDescription"),
start_date: formData.get("startDate"),
end_date: formData.get("endDate"),
location: formData.get("activityLocation"),
recruit_number: formData.get("maxParticipants"),
kakao_link: formData.get("openKakaoLink"),
};
// supabase에 insert하기
const { data, error } = await supabase
.from("individual_green_actions")
.insert(inputData);
if (error) {
throw error;
}
return data;
} catch (error) {
console.error("Error inserting data:", error);
throw error;
}
};
📝 인자로 받아온 formData에서 타입오류나는건 아직 해결 못함
- formData타입 FormData로 했는데 user_uid가 없다, title이 없다 이런 오류가 뜸...
(FormData가 아니라 user_uid, title,... 포함한 타입으로 만들어서 지정도 해봤는데 그 역시도 타입에러...)
// add-api.ts (_api/ individualAction-add)
// 2. insert한 값의 action_id 가져오기
export const getActionId = async (currentUserUId: string) => {
try {
// 삽입된 데이터의 Primary Key 값을 가져오기 위해 쿼리 실행
const { data: insertedData, error: selectError } = await supabase
.from("individual_green_actions")
.select("id")
.eq("user_uid", currentUserUId)
.order("created_at", { ascending: false }) // 해당 user가 등록한 데이터중 가장 최신데이터의 id 가져오기(방금 올린것)
.limit(1);
if (selectError) {
throw selectError;
}
// 삽입된 데이터의 Primary Key 값을 반환
const primaryKey = insertedData[0]?.id;
return primaryKey;
} catch (error) {
console.error("Error inserting data:", error);
throw error;
}
};
// add-api.ts (_api/ individualAction-add)
// 3. 스토리지에 이미지 저장하기
export const uploadFilesAndGetUrls = async ({
files,
action_id,
}: FileUpload) => {
try {
const uploadFiles = await Promise.all(
// map으로 배열의 각 파일 업로드
files.map(async (file) => {
if (file) {
const uuid = crypto.randomUUID();
// 'action_id' 폴더 생성, 파일이름은 uuid
const filePath = `${action_id}/${uuid}`;
const { data, error } = await supabase.storage
// 'green_action' 버켓에 업로드
.from("green_action")
.upload(filePath, file, {
cacheControl: "3600",
upsert: true,
});
if (error) {
console.error("Error uploading file:", error);
return null;
}
return data;
}
}),
);
console.log("uploadFiles", uploadFiles);
return uploadFiles;
} catch (error) {
console.error("Error uploading files and getting URLs:", error);
return [];
}
};