refactor: API 응답에서 datetime을 date와 time으로 분리
- datetime 필드를 date와 time 필드로 분리하여 00:00 시간도 정상 표시되도록 수정 - 백엔드: formatSchedule, Meilisearch 검색 결과, 스키마 업데이트 - 프론트엔드: datetime 파싱 로직 제거, date/time 직접 사용 - 문서: API 응답 예시 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5fa9c2a9d0
commit
87a69c0cbd
17 changed files with 98 additions and 92 deletions
|
|
@ -25,7 +25,8 @@ export const scheduleResponse = {
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'integer' },
|
id: { type: 'integer' },
|
||||||
title: { type: 'string' },
|
title: { type: 'string' },
|
||||||
datetime: { type: 'string' },
|
date: { type: 'string', format: 'date' },
|
||||||
|
time: { type: 'string', nullable: true },
|
||||||
category: scheduleCategory,
|
category: scheduleCategory,
|
||||||
members: { type: 'array', items: scheduleMember },
|
members: { type: 'array', items: scheduleMember },
|
||||||
createdAt: { type: 'string', format: 'date-time' },
|
createdAt: { type: 'string', format: 'date-time' },
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
import Inko from 'inko';
|
import Inko from 'inko';
|
||||||
import config, { CATEGORY_IDS } from '../../config/index.js';
|
import config, { CATEGORY_IDS } from '../../config/index.js';
|
||||||
import { createLogger } from '../../utils/logger.js';
|
import { createLogger } from '../../utils/logger.js';
|
||||||
import { buildDatetime } from '../schedule.js';
|
|
||||||
|
|
||||||
const inko = new Inko();
|
const inko = new Inko();
|
||||||
const logger = createLogger('Meilisearch');
|
const logger = createLogger('Meilisearch');
|
||||||
|
|
@ -166,7 +165,8 @@ function formatScheduleResponse(hit) {
|
||||||
return {
|
return {
|
||||||
id: hit.id,
|
id: hit.id,
|
||||||
title: hit.title,
|
title: hit.title,
|
||||||
datetime: buildDatetime(hit.date, hit.time),
|
date: hit.date,
|
||||||
|
time: hit.time || null,
|
||||||
category: {
|
category: {
|
||||||
id: hit.category_id,
|
id: hit.category_id,
|
||||||
name: hit.category_name,
|
name: hit.category_name,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ export function formatSchedule(rawSchedule, members = []) {
|
||||||
return {
|
return {
|
||||||
id: rawSchedule.id,
|
id: rawSchedule.id,
|
||||||
title: rawSchedule.title,
|
title: rawSchedule.title,
|
||||||
datetime: buildDatetime(rawSchedule.date, rawSchedule.time),
|
date: normalizeDate(rawSchedule.date),
|
||||||
|
time: rawSchedule.time || null,
|
||||||
category: {
|
category: {
|
||||||
id: rawSchedule.category_id,
|
id: rawSchedule.category_id,
|
||||||
name: rawSchedule.category_name,
|
name: rawSchedule.category_name,
|
||||||
|
|
@ -220,7 +221,8 @@ export async function getScheduleDetail(db, id, getXProfile = null) {
|
||||||
const result = {
|
const result = {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
datetime: buildDatetime(s.date, s.time),
|
date: normalizeDate(s.date),
|
||||||
|
time: s.time || null,
|
||||||
category: {
|
category: {
|
||||||
id: s.category_id,
|
id: s.category_id,
|
||||||
name: s.category_name,
|
name: s.category_name,
|
||||||
|
|
@ -324,7 +326,8 @@ export async function getMonthlySchedules(db, year, month) {
|
||||||
schedules.push({
|
schedules.push({
|
||||||
id: `birthday-${member.id}`,
|
id: `birthday-${member.id}`,
|
||||||
title: `HAPPY ${member.name_en} DAY`,
|
title: `HAPPY ${member.name_en} DAY`,
|
||||||
datetime: birthdayDate.toISOString().split('T')[0],
|
date: birthdayDate.toISOString().split('T')[0],
|
||||||
|
time: null,
|
||||||
category: {
|
category: {
|
||||||
id: CATEGORY_IDS.BIRTHDAY,
|
id: CATEGORY_IDS.BIRTHDAY,
|
||||||
name: '생일',
|
name: '생일',
|
||||||
|
|
@ -338,7 +341,7 @@ export async function getMonthlySchedules(db, year, month) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 날짜순 정렬
|
// 날짜순 정렬
|
||||||
schedules.sort((a, b) => a.datetime.localeCompare(b.datetime));
|
schedules.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
return { schedules };
|
return { schedules };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
docs/api.md
30
docs/api.md
|
|
@ -50,24 +50,23 @@ Base URL: `/api`
|
||||||
**월별 조회 응답:**
|
**월별 조회 응답:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"2026-01-18": {
|
|
||||||
"categories": [
|
|
||||||
{ "id": 2, "name": "유튜브", "color": "#ff0033", "count": 3 }
|
|
||||||
],
|
|
||||||
"schedules": [
|
"schedules": [
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
"title": "...",
|
"title": "...",
|
||||||
|
"date": "2026-01-18",
|
||||||
"time": "19:00:00",
|
"time": "19:00:00",
|
||||||
"category": { "id": 2, "name": "유튜브", "color": "#ff0033" },
|
"category": { "id": 2, "name": "유튜브", "color": "#ff0033" },
|
||||||
"source": {
|
"source": {
|
||||||
"name": "fromis_9",
|
"name": "fromis_9",
|
||||||
"url": "https://www.youtube.com/watch?v=VIDEO_ID"
|
"url": "https://www.youtube.com/watch?v=VIDEO_ID"
|
||||||
}
|
},
|
||||||
|
"members": ["송하영"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
```
|
||||||
|
※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환
|
||||||
```
|
```
|
||||||
|
|
||||||
**source 객체 (카테고리별):**
|
**source 객체 (카테고리별):**
|
||||||
|
|
@ -77,20 +76,22 @@ Base URL: `/api`
|
||||||
|
|
||||||
**다가오는 일정 응답 (startDate):**
|
**다가오는 일정 응답 (startDate):**
|
||||||
```json
|
```json
|
||||||
[
|
{
|
||||||
|
"schedules": [
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
"title": "...",
|
"title": "...",
|
||||||
"date": "2026-01-18",
|
"date": "2026-01-18",
|
||||||
"time": "19:00:00",
|
"time": "19:00:00",
|
||||||
"category_id": 2,
|
"category": { "id": 2, "name": "유튜브", "color": "#ff0033" },
|
||||||
"category_name": "유튜브",
|
"source": { "name": "fromis_9", "url": "https://..." },
|
||||||
"category_color": "#ff0033",
|
"members": ["송하영"]
|
||||||
"members": [{ "name": "송하영" }]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
※ 현재 활동 멤버 전원인 경우 `[{ "name": "프로미스나인" }]` 반환 (탈퇴 멤버 제외)
|
※ 현재 활동 멤버 전원인 경우 `["프로미스나인"]` 반환 (탈퇴 멤버 제외)
|
||||||
|
※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환
|
||||||
|
|
||||||
**검색 응답:**
|
**검색 응답:**
|
||||||
```json
|
```json
|
||||||
|
|
@ -99,7 +100,8 @@ Base URL: `/api`
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
"title": "...",
|
"title": "...",
|
||||||
"datetime": "2026-01-18T19:00:00",
|
"date": "2026-01-18",
|
||||||
|
"time": "19:00:00",
|
||||||
"category": { "id": 2, "name": "유튜브", "color": "#ff0033" },
|
"category": { "id": 2, "name": "유튜브", "color": "#ff0033" },
|
||||||
"source": { "name": "fromis_9", "url": "https://..." },
|
"source": { "name": "fromis_9", "url": "https://..." },
|
||||||
"members": ["송하영"],
|
"members": ["송하영"],
|
||||||
|
|
@ -112,6 +114,8 @@ Base URL: `/api`
|
||||||
"hasMore": true
|
"hasMore": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환
|
||||||
|
```
|
||||||
|
|
||||||
### GET /schedules/categories
|
### GET /schedules/categories
|
||||||
카테고리 목록 조회
|
카테고리 목록 조회
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,17 @@ import { fetchAuthApi, fetchFormData } from '@/api/client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 응답을 프론트엔드 형식으로 변환
|
* API 응답을 프론트엔드 형식으로 변환
|
||||||
* - datetime → date, time 분리
|
|
||||||
* - category 객체 → category_id, category_name, category_color 플랫화
|
* - category 객체 → category_id, category_name, category_color 플랫화
|
||||||
* - members 배열 → member_names 문자열
|
* - members 배열 → member_names 문자열
|
||||||
*/
|
*/
|
||||||
function transformSchedule(schedule) {
|
function transformSchedule(schedule) {
|
||||||
const category = schedule.category || {};
|
const category = schedule.category || {};
|
||||||
|
|
||||||
// datetime에서 date와 time 분리
|
|
||||||
let date = '';
|
|
||||||
let time = null;
|
|
||||||
if (schedule.datetime) {
|
|
||||||
const parts = schedule.datetime.split('T');
|
|
||||||
date = parts[0];
|
|
||||||
time = parts[1] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// members 배열을 문자열로 (기존 코드 호환성)
|
// members 배열을 문자열로 (기존 코드 호환성)
|
||||||
const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') : '';
|
const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...schedule,
|
...schedule,
|
||||||
date,
|
|
||||||
time,
|
|
||||||
category_id: category.id,
|
category_id: category.id,
|
||||||
category_name: category.name,
|
category_name: category.name,
|
||||||
category_color: category.color,
|
category_color: category.color,
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,16 @@
|
||||||
* 스케줄 API
|
* 스케줄 API
|
||||||
*/
|
*/
|
||||||
import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client';
|
import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client';
|
||||||
import { getTodayKST, dayjs } from '@/utils';
|
import { getTodayKST } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 응답을 프론트엔드 형식으로 변환
|
* API 응답을 프론트엔드 형식으로 변환
|
||||||
* - datetime → date, time 분리
|
|
||||||
* - category 객체 → category_id, category_name, category_color 플랫화
|
* - category 객체 → category_id, category_name, category_color 플랫화
|
||||||
* - members 배열 → member_names 문자열
|
* - members 배열 → member_names 문자열
|
||||||
*/
|
*/
|
||||||
function transformSchedule(schedule) {
|
function transformSchedule(schedule) {
|
||||||
const category = schedule.category || {};
|
const category = schedule.category || {};
|
||||||
|
|
||||||
// datetime에서 date와 time 분리
|
|
||||||
let date = '';
|
|
||||||
let time = null;
|
|
||||||
if (schedule.datetime) {
|
|
||||||
const dt = dayjs(schedule.datetime);
|
|
||||||
date = dt.format('YYYY-MM-DD');
|
|
||||||
// datetime에 T가 포함되어 있으면 시간이 있는 것
|
|
||||||
time = schedule.datetime.includes('T') ? dt.format('HH:mm:ss') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// members 배열을 문자열로 (기존 코드 호환성)
|
// members 배열을 문자열로 (기존 코드 호환성)
|
||||||
const memberNames = Array.isArray(schedule.members)
|
const memberNames = Array.isArray(schedule.members)
|
||||||
? schedule.members.join(',')
|
? schedule.members.join(',')
|
||||||
|
|
@ -30,8 +19,6 @@ function transformSchedule(schedule) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...schedule,
|
...schedule,
|
||||||
date,
|
|
||||||
time,
|
|
||||||
category_id: category.id,
|
category_id: category.id,
|
||||||
category_name: category.name,
|
category_name: category.name,
|
||||||
category_color: category.color,
|
category_color: category.color,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const ScheduleSearchCard = memo(function ScheduleSearchCard({
|
||||||
delay = 0,
|
delay = 0,
|
||||||
className = '',
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
const scheduleDate = new Date(schedule.date || schedule.datetime);
|
const scheduleDate = new Date(schedule.date);
|
||||||
const categoryInfo = getCategoryInfo(schedule);
|
const categoryInfo = getCategoryInfo(schedule);
|
||||||
const timeStr = getScheduleTime(schedule);
|
const timeStr = getScheduleTime(schedule);
|
||||||
const displayMembers = getDisplayMembers(schedule);
|
const displayMembers = getDisplayMembers(schedule);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ function AdminScheduleCard({
|
||||||
onDelete,
|
onDelete,
|
||||||
className = '',
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
const scheduleDate = new Date(schedule.date || schedule.datetime);
|
const scheduleDate = new Date(schedule.date);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const currentYear = today.getFullYear();
|
const currentYear = today.getFullYear();
|
||||||
const currentMonth = today.getMonth();
|
const currentMonth = today.getMonth();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react';
|
import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react';
|
||||||
import Linkify from 'react-linkify';
|
import Linkify from 'react-linkify';
|
||||||
import { getSchedule } from '@/api';
|
import { getSchedule } from '@/api';
|
||||||
import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTime } from '@/utils';
|
import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만)
|
* 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만)
|
||||||
|
|
@ -88,7 +88,7 @@ function MobileYoutubeSection({ schedule }) {
|
||||||
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 mb-3">
|
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 mb-3">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar size={12} />
|
<Calendar size={12} />
|
||||||
<span>{formatXDateTime(schedule.datetime)}</span>
|
<span>{formatXDateTimeWithTime(schedule.date, schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
{schedule.channelName && (
|
{schedule.channelName && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|
@ -274,7 +274,7 @@ function MobileXSection({ schedule }) {
|
||||||
|
|
||||||
{/* 날짜/시간 */}
|
{/* 날짜/시간 */}
|
||||||
<div className="px-4 py-3 border-t border-gray-100">
|
<div className="px-4 py-3 border-t border-gray-100">
|
||||||
<span className="text-gray-500 text-sm">{formatXDateTime(schedule.datetime)}</span>
|
<span className="text-gray-500 text-sm">{formatXDateTimeWithTime(schedule.date, schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* X에서 보기 버튼 */}
|
{/* X에서 보기 버튼 */}
|
||||||
|
|
@ -371,12 +371,12 @@ function MobileDefaultSection({ schedule }) {
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar size={12} />
|
<Calendar size={12} />
|
||||||
<span>{formatFullDate(schedule.datetime)}</span>
|
<span>{formatFullDate(schedule.date)}</span>
|
||||||
</div>
|
</div>
|
||||||
{schedule.datetime?.includes('T') && schedule.datetime.split('T')[1] !== '00:00:00' && (
|
{schedule.time && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
<span>{formatTime(schedule.datetime?.split('T')[1])}</span>
|
<span>{formatTime(schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -235,19 +235,17 @@ function YouTubeEditForm() {
|
||||||
? `https://www.youtube.com/shorts/${schedule.videoId}`
|
? `https://www.youtube.com/shorts/${schedule.videoId}`
|
||||||
: `https://www.youtube.com/watch?v=${schedule.videoId}`;
|
: `https://www.youtube.com/watch?v=${schedule.videoId}`;
|
||||||
|
|
||||||
// 날짜 포맷팅 함수 (datetime 문자열 파싱)
|
// 날짜 포맷팅 함수
|
||||||
const formatDatetime = (datetime) => {
|
const formatDatetime = (dateStr, timeStr) => {
|
||||||
if (!datetime) return "";
|
if (!dateStr) return "";
|
||||||
// datetime: "2025-01-20T14:00:00" 또는 "2025-01-20"
|
|
||||||
const dateStr = datetime.split("T")[0];
|
|
||||||
const timeStr = datetime.includes("T") ? datetime.split("T")[1]?.slice(0, 5) : "";
|
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
const dayNames = ["일", "월", "화", "수", "목", "금", "토"];
|
const dayNames = ["일", "월", "화", "수", "목", "금", "토"];
|
||||||
const dayName = dayNames[date.getDay()];
|
const dayName = dayNames[date.getDay()];
|
||||||
return `${year}년 ${month}월 ${day}일 (${dayName}) ${timeStr}`;
|
const time = timeStr ? timeStr.slice(0, 5) : "";
|
||||||
|
return `${year}년 ${month}월 ${day}일 (${dayName}) ${time}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -324,7 +322,7 @@ function YouTubeEditForm() {
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">업로드:</span>{" "}
|
<span className="text-gray-400">업로드:</span>{" "}
|
||||||
{formatDatetime(schedule.datetime)}
|
{formatDatetime(schedule.date, schedule.time)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -456,7 +454,7 @@ function YouTubeEditForm() {
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">업로드:</span>{" "}
|
<span className="text-gray-400">업로드:</span>{" "}
|
||||||
{formatDatetime(schedule.datetime)}
|
{formatDatetime(schedule.date, schedule.time)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-gray-400">유형:</span>
|
<span className="text-gray-400">유형:</span>
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ function DefaultSection({ schedule }) {
|
||||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-6">
|
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar size={16} />
|
<Calendar size={16} />
|
||||||
<span>{formatFullDate(schedule.datetime)}</span>
|
<span>{formatFullDate(schedule.date)}</span>
|
||||||
</div>
|
</div>
|
||||||
{schedule.datetime?.includes('T') && schedule.datetime.split('T')[1] !== '00:00:00' && (
|
{schedule.time && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock size={16} />
|
<Clock size={16} />
|
||||||
<span>{formatTime(schedule.datetime?.split('T')[1])}</span>
|
<span>{formatTime(schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Linkify from 'react-linkify';
|
import Linkify from 'react-linkify';
|
||||||
import { decodeHtmlEntities, formatXDateTime } from './utils';
|
import { decodeHtmlEntities, formatXDateTimeWithTime } from './utils';
|
||||||
import { Lightbox } from '@/components/common';
|
import { Lightbox } from '@/components/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,7 +130,7 @@ function XSection({ schedule }) {
|
||||||
|
|
||||||
{/* 날짜/시간 */}
|
{/* 날짜/시간 */}
|
||||||
<div className="px-5 py-4 border-t border-gray-100">
|
<div className="px-5 py-4 border-t border-gray-100">
|
||||||
<span className="text-gray-500 text-[15px]">{formatXDateTime(schedule.datetime)}</span>
|
<span className="text-gray-500 text-[15px]">{formatXDateTimeWithTime(schedule.date, schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* X에서 보기 버튼 */}
|
{/* X에서 보기 버튼 */}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Calendar, Link2 } from 'lucide-react';
|
import { Calendar, Link2 } from 'lucide-react';
|
||||||
import { decodeHtmlEntities, formatXDateTime } from './utils';
|
import { decodeHtmlEntities, formatXDateTimeWithTime } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 영상 정보 컴포넌트 (공통)
|
* 영상 정보 컴포넌트 (공통)
|
||||||
|
|
@ -21,7 +21,7 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
{/* 날짜/시간 */}
|
{/* 날짜/시간 */}
|
||||||
<div className="flex items-center gap-1.5 text-gray-500">
|
<div className="flex items-center gap-1.5 text-gray-500">
|
||||||
<Calendar size={14} />
|
<Calendar size={14} />
|
||||||
<span>{formatXDateTime(schedule.datetime)}</span>
|
<span>{formatXDateTimeWithTime(schedule.date, schedule.time)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 채널명 */}
|
{/* 채널명 */}
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@
|
||||||
|
|
||||||
// @/utils에서 re-export
|
// @/utils에서 re-export
|
||||||
export { decodeHtmlEntities, formatTime } from '@/utils';
|
export { decodeHtmlEntities, formatTime } from '@/utils';
|
||||||
export { formatFullDate, formatXDateTime } from '@/utils';
|
export { formatFullDate, formatXDateTime, formatXDateTimeWithTime } from '@/utils';
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,31 @@ export const formatXDateTime = (datetime) => {
|
||||||
return datePart;
|
return datePart;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X(트위터) 스타일 날짜/시간 포맷팅 (time 필드 분리 버전)
|
||||||
|
* @param {string} date - 날짜 문자열 (YYYY-MM-DD)
|
||||||
|
* @param {string|null} time - 시간 문자열 (HH:mm:ss) 또는 null
|
||||||
|
* @returns {string} "오후 7:00 · 2026년 1월 18일" 또는 "2026년 1월 18일"
|
||||||
|
*/
|
||||||
|
export const formatXDateTimeWithTime = (date, time) => {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
const d = dayjs(date).tz(TIMEZONE);
|
||||||
|
const datePart = `${d.year()}년 ${d.month() + 1}월 ${d.date()}일`;
|
||||||
|
|
||||||
|
// time이 있으면 시간 표시
|
||||||
|
if (time) {
|
||||||
|
const [hourStr, minuteStr] = time.split(':');
|
||||||
|
const hours = parseInt(hourStr, 10);
|
||||||
|
const minutes = parseInt(minuteStr, 10);
|
||||||
|
const period = hours < 12 ? '오전' : '오후';
|
||||||
|
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||||
|
return `${period} ${hour12}:${String(minutes).padStart(2, '0')} · ${datePart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return datePart;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* datetime 문자열에서 date 추출
|
* datetime 문자열에서 date 추출
|
||||||
* @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DD"
|
* @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DD"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export {
|
||||||
isToday,
|
isToday,
|
||||||
formatFullDate,
|
formatFullDate,
|
||||||
formatXDateTime,
|
formatXDateTime,
|
||||||
|
formatXDateTimeWithTime,
|
||||||
extractDate,
|
extractDate,
|
||||||
extractTime,
|
extractTime,
|
||||||
dayjs,
|
dayjs,
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,11 @@ export function getCategoryInfo(schedule) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 스케줄에서 날짜 추출
|
* 스케줄에서 날짜 추출
|
||||||
* datetime 형식과 date 형식 모두 처리
|
|
||||||
* @param {object} schedule - 스케줄 객체
|
* @param {object} schedule - 스케줄 객체
|
||||||
* @returns {string} YYYY-MM-DD 형식 날짜
|
* @returns {string} YYYY-MM-DD 형식 날짜
|
||||||
*/
|
*/
|
||||||
export function getScheduleDate(schedule) {
|
export function getScheduleDate(schedule) {
|
||||||
return schedule.date || extractDate(schedule.datetime);
|
return schedule.date || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,7 +44,7 @@ export function getScheduleTime(schedule) {
|
||||||
if (schedule.time) {
|
if (schedule.time) {
|
||||||
return schedule.time.slice(0, 5);
|
return schedule.time.slice(0, 5);
|
||||||
}
|
}
|
||||||
return extractTime(schedule.datetime);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue