Compare commits
No commits in common. "346d6529f2d21194c0f27137458bec8c6e11efe1" and "52332babea0a3dd63ab145bfd099744652c985ea" have entirely different histories.
346d6529f2
...
52332babea
12 changed files with 9 additions and 333 deletions
1
.env
1
.env
|
|
@ -22,4 +22,3 @@ KAKAO_REST_KEY=cf21156ae6cc8f6e95b3a3150926cdf8
|
||||||
|
|
||||||
# YouTube API
|
# YouTube API
|
||||||
YOUTUBE_API_KEY=AIzaSyC6l3nFlcHgLc0d1Q9WPyYQjVKTv21ZqFs
|
YOUTUBE_API_KEY=AIzaSyC6l3nFlcHgLc0d1Q9WPyYQjVKTv21ZqFs
|
||||||
MEILI_MASTER_KEY=xMLNzlGX4xYji494JOb5IMlLHULcYw91
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,4 +18,3 @@ Thumbs.db
|
||||||
# Build
|
# Build
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
meilisearch_data/
|
|
||||||
|
|
|
||||||
7
backend/package-lock.json
generated
7
backend/package-lock.json
generated
|
|
@ -12,7 +12,6 @@
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"meilisearch": "^0.55.0",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
|
@ -2782,12 +2781,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/meilisearch": {
|
|
||||||
"version": "0.55.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.55.0.tgz",
|
|
||||||
"integrity": "sha512-qSMeiezfDgIqciIeYzh5E4pXDZZD7CtHeWDCs43kN3trLgl5FtfmBAIkljL3huFaOx08feYtC8FfIFUpVwq6rg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"meilisearch": "^0.55.0",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ import {
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { syncNewVideos, syncAllVideos } from "../services/youtube-bot.js";
|
import { syncNewVideos, syncAllVideos } from "../services/youtube-bot.js";
|
||||||
import { startBot, stopBot } from "../services/youtube-scheduler.js";
|
import { startBot, stopBot } from "../services/youtube-scheduler.js";
|
||||||
import {
|
|
||||||
addOrUpdateSchedule,
|
|
||||||
deleteSchedule as deleteScheduleFromSearch,
|
|
||||||
} from "../services/meilisearch.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -1325,33 +1321,6 @@ router.post(
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
// Meilisearch에 동기화
|
|
||||||
try {
|
|
||||||
const [categoryInfo] = await pool.query(
|
|
||||||
"SELECT name, color FROM schedule_categories WHERE id = ?",
|
|
||||||
[category || null]
|
|
||||||
);
|
|
||||||
const [memberInfo] = await pool.query(
|
|
||||||
"SELECT id, name FROM members WHERE id IN (?)",
|
|
||||||
[members?.length ? members : [0]]
|
|
||||||
);
|
|
||||||
await addOrUpdateSchedule({
|
|
||||||
id: scheduleId,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
time,
|
|
||||||
category_id: category,
|
|
||||||
category_name: categoryInfo[0]?.name || "",
|
|
||||||
category_color: categoryInfo[0]?.color || "",
|
|
||||||
source_name: sourceName,
|
|
||||||
source_url: url,
|
|
||||||
members: memberInfo,
|
|
||||||
});
|
|
||||||
} catch (searchError) {
|
|
||||||
console.error("Meilisearch 동기화 오류:", searchError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: "일정이 생성되었습니다.", scheduleId });
|
res.json({ message: "일정이 생성되었습니다.", scheduleId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
|
|
@ -1681,14 +1650,6 @@ router.delete("/schedules/:id", authenticateToken, async (req, res) => {
|
||||||
await connection.query("DELETE FROM schedules WHERE id = ?", [id]);
|
await connection.query("DELETE FROM schedules WHERE id = ?", [id]);
|
||||||
|
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
// Meilisearch에서도 삭제
|
|
||||||
try {
|
|
||||||
await deleteScheduleFromSearch(id);
|
|
||||||
} catch (searchError) {
|
|
||||||
console.error("Meilisearch 삭제 오류:", searchError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: "일정이 삭제되었습니다." });
|
res.json({ message: "일정이 삭제되었습니다." });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await connection.rollback();
|
await connection.rollback();
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { searchSchedules } from "../services/meilisearch.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 공개 일정 목록 조회 (검색 포함)
|
// 공개 일정 목록 조회
|
||||||
router.get("/", async (req, res) => {
|
router.get("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { search } = req.query;
|
|
||||||
|
|
||||||
// 검색어가 있으면 Meilisearch 사용
|
|
||||||
if (search && search.trim()) {
|
|
||||||
const results = await searchSchedules(search.trim());
|
|
||||||
return res.json(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색어 없으면 DB에서 전체 조회
|
|
||||||
const [schedules] = await pool.query(`
|
const [schedules] = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
|
|
@ -90,38 +80,4 @@ router.get("/:id", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Meilisearch 동기화 API
|
|
||||||
router.post("/sync-search", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { syncAllSchedules } = await import("../services/meilisearch.js");
|
|
||||||
|
|
||||||
// DB에서 모든 일정 조회
|
|
||||||
const [schedules] = await pool.query(`
|
|
||||||
SELECT
|
|
||||||
s.id,
|
|
||||||
s.title,
|
|
||||||
s.description,
|
|
||||||
s.date,
|
|
||||||
s.time,
|
|
||||||
s.category_id,
|
|
||||||
s.source_url,
|
|
||||||
s.source_name,
|
|
||||||
c.name as category_name,
|
|
||||||
c.color as category_color,
|
|
||||||
GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ' ') as member_names
|
|
||||||
FROM schedules s
|
|
||||||
LEFT JOIN schedule_categories c ON s.category_id = c.id
|
|
||||||
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
|
|
||||||
LEFT JOIN members m ON sm.member_id = m.id
|
|
||||||
GROUP BY s.id
|
|
||||||
`);
|
|
||||||
|
|
||||||
const count = await syncAllSchedules(schedules);
|
|
||||||
res.json({ success: true, synced: count });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Meilisearch 동기화 오류:", error);
|
|
||||||
res.status(500).json({ error: "동기화 중 오류가 발생했습니다." });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import statsRouter from "./routes/stats.js";
|
||||||
import adminRouter from "./routes/admin.js";
|
import adminRouter from "./routes/admin.js";
|
||||||
import schedulesRouter from "./routes/schedules.js";
|
import schedulesRouter from "./routes/schedules.js";
|
||||||
import { initScheduler } from "./services/youtube-scheduler.js";
|
import { initScheduler } from "./services/youtube-scheduler.js";
|
||||||
import { initMeilisearch } from "./services/meilisearch.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
@ -45,14 +44,6 @@ app.get("*", (req, res) => {
|
||||||
app.listen(PORT, async () => {
|
app.listen(PORT, async () => {
|
||||||
console.log(`🌸 fromis_9 서버가 포트 ${PORT}에서 실행 중입니다`);
|
console.log(`🌸 fromis_9 서버가 포트 ${PORT}에서 실행 중입니다`);
|
||||||
|
|
||||||
// Meilisearch 초기화
|
|
||||||
try {
|
|
||||||
await initMeilisearch();
|
|
||||||
console.log("🔍 Meilisearch 초기화 완료");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Meilisearch 초기화 오류:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// YouTube 봇 스케줄러 초기화
|
// YouTube 봇 스케줄러 초기화
|
||||||
try {
|
try {
|
||||||
await initScheduler();
|
await initScheduler();
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
import { MeiliSearch } from "meilisearch";
|
|
||||||
|
|
||||||
// Meilisearch 클라이언트 초기화
|
|
||||||
const client = new MeiliSearch({
|
|
||||||
host: "http://fromis9-meilisearch:7700",
|
|
||||||
apiKey: process.env.MEILI_MASTER_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
const SCHEDULE_INDEX = "schedules";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 인덱스 초기화 및 설정
|
|
||||||
*/
|
|
||||||
export async function initMeilisearch() {
|
|
||||||
try {
|
|
||||||
// 인덱스 생성 (이미 존재하면 무시)
|
|
||||||
await client.createIndex(SCHEDULE_INDEX, { primaryKey: "id" });
|
|
||||||
|
|
||||||
// 인덱스 설정
|
|
||||||
const index = client.index(SCHEDULE_INDEX);
|
|
||||||
|
|
||||||
// 검색 가능한 필드 설정
|
|
||||||
await index.updateSearchableAttributes([
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"member_names",
|
|
||||||
"category_name",
|
|
||||||
"source_name",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 필터링 가능한 필드 설정
|
|
||||||
await index.updateFilterableAttributes(["category_id", "date"]);
|
|
||||||
|
|
||||||
// 정렬 가능한 필드 설정
|
|
||||||
await index.updateSortableAttributes(["date", "time"]);
|
|
||||||
|
|
||||||
// 오타 허용 설정 (typo tolerance)
|
|
||||||
await index.updateTypoTolerance({
|
|
||||||
enabled: true,
|
|
||||||
minWordSizeForTypos: {
|
|
||||||
oneTypo: 2,
|
|
||||||
twoTypos: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[Meilisearch] 인덱스 초기화 완료");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Meilisearch] 초기화 오류:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 일정 문서 추가/업데이트
|
|
||||||
*/
|
|
||||||
export async function addOrUpdateSchedule(schedule) {
|
|
||||||
try {
|
|
||||||
const index = client.index(SCHEDULE_INDEX);
|
|
||||||
|
|
||||||
// 멤버 이름을 문자열로 변환
|
|
||||||
const memberNames = schedule.members
|
|
||||||
? schedule.members.map((m) => m.name).join(" ")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const document = {
|
|
||||||
id: schedule.id,
|
|
||||||
title: schedule.title,
|
|
||||||
description: schedule.description || "",
|
|
||||||
date: schedule.date,
|
|
||||||
time: schedule.time || "",
|
|
||||||
category_id: schedule.category_id,
|
|
||||||
category_name: schedule.category_name || "",
|
|
||||||
category_color: schedule.category_color || "",
|
|
||||||
source_name: schedule.source_name || "",
|
|
||||||
source_url: schedule.source_url || "",
|
|
||||||
member_names: memberNames,
|
|
||||||
};
|
|
||||||
|
|
||||||
await index.addDocuments([document]);
|
|
||||||
console.log(`[Meilisearch] 일정 추가/업데이트: ${schedule.id}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Meilisearch] 문서 추가 오류:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 일정 문서 삭제
|
|
||||||
*/
|
|
||||||
export async function deleteSchedule(scheduleId) {
|
|
||||||
try {
|
|
||||||
const index = client.index(SCHEDULE_INDEX);
|
|
||||||
await index.deleteDocument(scheduleId);
|
|
||||||
console.log(`[Meilisearch] 일정 삭제: ${scheduleId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Meilisearch] 문서 삭제 오류:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 일정 검색
|
|
||||||
*/
|
|
||||||
export async function searchSchedules(query, options = {}) {
|
|
||||||
try {
|
|
||||||
const index = client.index(SCHEDULE_INDEX);
|
|
||||||
|
|
||||||
const searchOptions = {
|
|
||||||
limit: options.limit || 50,
|
|
||||||
attributesToRetrieve: ["*"],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 카테고리 필터
|
|
||||||
if (options.categoryId) {
|
|
||||||
searchOptions.filter = `category_id = ${options.categoryId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정렬 (기본: 날짜 내림차순)
|
|
||||||
searchOptions.sort = options.sort || ["date:desc", "time:desc"];
|
|
||||||
|
|
||||||
const results = await index.search(query, searchOptions);
|
|
||||||
|
|
||||||
return results.hits;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Meilisearch] 검색 오류:", error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 모든 일정 동기화 (초기 데이터 로드용)
|
|
||||||
*/
|
|
||||||
export async function syncAllSchedules(schedules) {
|
|
||||||
try {
|
|
||||||
const index = client.index(SCHEDULE_INDEX);
|
|
||||||
|
|
||||||
// 기존 문서 모두 삭제
|
|
||||||
await index.deleteAllDocuments();
|
|
||||||
|
|
||||||
// 문서 변환
|
|
||||||
const documents = schedules.map((schedule) => ({
|
|
||||||
id: schedule.id,
|
|
||||||
title: schedule.title,
|
|
||||||
description: schedule.description || "",
|
|
||||||
date: schedule.date,
|
|
||||||
time: schedule.time || "",
|
|
||||||
category_id: schedule.category_id,
|
|
||||||
category_name: schedule.category_name || "",
|
|
||||||
category_color: schedule.category_color || "",
|
|
||||||
source_name: schedule.source_name || "",
|
|
||||||
source_url: schedule.source_url || "",
|
|
||||||
member_names: schedule.member_names || "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 일괄 추가
|
|
||||||
await index.addDocuments(documents);
|
|
||||||
console.log(`[Meilisearch] ${documents.length}개 일정 동기화 완료`);
|
|
||||||
|
|
||||||
return documents.length;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Meilisearch] 동기화 오류:", error.message);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { client };
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import Parser from "rss-parser";
|
import Parser from "rss-parser";
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { addOrUpdateSchedule } from "./meilisearch.js";
|
|
||||||
|
|
||||||
// YouTube API 키
|
// YouTube API 키
|
||||||
const YOUTUBE_API_KEY =
|
const YOUTUBE_API_KEY =
|
||||||
|
|
@ -294,33 +293,6 @@ export async function createScheduleFromVideo(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meilisearch에 동기화
|
|
||||||
try {
|
|
||||||
const [categoryInfo] = await pool.query(
|
|
||||||
"SELECT name, color FROM schedule_categories WHERE id = ?",
|
|
||||||
[categoryId]
|
|
||||||
);
|
|
||||||
const [memberInfo] = await pool.query(
|
|
||||||
"SELECT id, name FROM members WHERE id IN (?)",
|
|
||||||
[memberIds.length > 0 ? [...new Set(memberIds)] : [0]]
|
|
||||||
);
|
|
||||||
await addOrUpdateSchedule({
|
|
||||||
id: scheduleId,
|
|
||||||
title: video.title,
|
|
||||||
description: "",
|
|
||||||
date: video.date,
|
|
||||||
time: video.time,
|
|
||||||
category_id: categoryId,
|
|
||||||
category_name: categoryInfo[0]?.name || "",
|
|
||||||
category_color: categoryInfo[0]?.color || "",
|
|
||||||
source_name: sourceName,
|
|
||||||
source_url: video.videoUrl,
|
|
||||||
members: memberInfo,
|
|
||||||
});
|
|
||||||
} catch (searchError) {
|
|
||||||
console.error("Meilisearch 동기화 오류:", searchError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return scheduleId;
|
return scheduleId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("일정 생성 오류:", error);
|
console.error("일정 생성 오류:", error);
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,6 @@ services:
|
||||||
- db
|
- db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
meilisearch:
|
|
||||||
image: getmeili/meilisearch:v1.6
|
|
||||||
container_name: fromis9-meilisearch
|
|
||||||
environment:
|
|
||||||
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
|
|
||||||
volumes:
|
|
||||||
- ./meilisearch_data:/meili_data
|
|
||||||
networks:
|
|
||||||
- app
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app:
|
app:
|
||||||
external: true
|
external: true
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,13 @@ import { Clock, ChevronLeft, ChevronRight, ChevronDown, Tag, Search, ArrowLeft }
|
||||||
|
|
||||||
function Schedule() {
|
function Schedule() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// KST 기준 오늘 날짜 (YYYY-MM-DD)
|
|
||||||
const getTodayKST = () => {
|
|
||||||
const now = new Date();
|
|
||||||
const kstOffset = 9 * 60 * 60 * 1000; // 9시간
|
|
||||||
const kstDate = new Date(now.getTime() + kstOffset);
|
|
||||||
return kstDate.toISOString().split('T')[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST 기준 오늘
|
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); // 오늘 기본값
|
||||||
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false);
|
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState('yearMonth');
|
const [viewMode, setViewMode] = useState('yearMonth');
|
||||||
const [slideDirection, setSlideDirection] = useState(0);
|
const [slideDirection, setSlideDirection] = useState(0);
|
||||||
const pickerRef = useRef(null);
|
const pickerRef = useRef(null);
|
||||||
|
|
||||||
|
|
||||||
// 데이터 상태
|
// 데이터 상태
|
||||||
const [schedules, setSchedules] = useState([]);
|
const [schedules, setSchedules] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,17 @@ import Tooltip from '../../../components/Tooltip';
|
||||||
|
|
||||||
function AdminSchedule() {
|
function AdminSchedule() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// KST 기준 오늘 날짜 (YYYY-MM-DD)
|
|
||||||
const getTodayKST = () => {
|
|
||||||
const now = new Date();
|
|
||||||
const kstOffset = 9 * 60 * 60 * 1000;
|
|
||||||
const kstDate = new Date(now.getTime() + kstOffset);
|
|
||||||
return kstDate.toISOString().split('T')[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [toast, setToast] = useState(null);
|
const [toast, setToast] = useState(null);
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState(''); // 입력 상태
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState(''); // 실제 검색어 (엔터 시 적용)
|
||||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
const [isSearchMode, setIsSearchMode] = useState(false); // 검색 모드 활성화
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
const [searchResults, setSearchResults] = useState([]); // 검색 결과 (API 응답)
|
||||||
const [searchLoading, setSearchLoading] = useState(false);
|
const [searchLoading, setSearchLoading] = useState(false); // 검색 로딩
|
||||||
const [selectedCategories, setSelectedCategories] = useState([]);
|
const [selectedCategories, setSelectedCategories] = useState([]); // 빈 배열 = 전체
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST 기준 오늘
|
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
|
||||||
const [slideDirection, setSlideDirection] = useState(0);
|
const [slideDirection, setSlideDirection] = useState(0);
|
||||||
|
|
||||||
// 년월 선택 관련 (Schedule.jsx와 동일한 패턴)
|
// 년월 선택 관련 (Schedule.jsx와 동일한 패턴)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue